feat: instant feedback button — creates Gitea issues with auto-labels
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 3s

iOS:
- Subtle floating feedback button (bottom-left, speech bubble icon)
- Quick sheet: type → send → auto-creates Gitea issue
- Shows checkmark on success, auto-dismisses
- No friction — tap, type, done

Gateway:
- POST /api/feedback endpoint
- Auto-labels: bug/feature/enhancement + ios/web + fitness/brain/reader/podcasts
- Keyword detection for label assignment
- Creates issue via Gitea API with user name and source

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 12:44:10 -05:00
parent 60b28097ad
commit 687d6c5f12
16 changed files with 1274 additions and 359 deletions

108
gateway/feedback.py Normal file
View File

@@ -0,0 +1,108 @@
"""
Platform Gateway — Feedback handler.
Creates Gitea issues from user feedback with auto-labeling.
"""
import json
import re
import urllib.request
import urllib.error
GITEA_URL = "http://localhost:3300"
GITEA_TOKEN = "7abae0245e49e130e1b6752b7fa381a57607d8c7"
REPO = "yusiboyz/platform"
# Label IDs from Gitea
LABELS = {
"bug": 11,
"feature": 12,
"ios": 13,
"web": 14,
"fitness": 15,
"brain": 16,
"reader": 17,
"podcasts": 18,
"enhancement": 19,
}
def auto_label(text: str, source: str = "ios") -> list[int]:
"""Detect labels from feedback text."""
text_lower = text.lower()
label_ids = []
# Source platform
if source == "ios":
label_ids.append(LABELS["ios"])
elif source == "web":
label_ids.append(LABELS["web"])
# Bug or feature
bug_words = ["bug", "broken", "crash", "error", "wrong", "fix", "issue", "doesn't work", "not working", "can't"]
feature_words = ["want", "add", "wish", "would be nice", "can we", "can you", "feature", "idea", "suggest"]
if any(w in text_lower for w in bug_words):
label_ids.append(LABELS["bug"])
elif any(w in text_lower for w in feature_words):
label_ids.append(LABELS["feature"])
else:
label_ids.append(LABELS["enhancement"])
# App detection
if any(w in text_lower for w in ["fitness", "calorie", "food", "meal", "macro", "protein"]):
label_ids.append(LABELS["fitness"])
elif any(w in text_lower for w in ["brain", "note", "save", "bookmark"]):
label_ids.append(LABELS["brain"])
elif any(w in text_lower for w in ["reader", "rss", "feed", "article"]):
label_ids.append(LABELS["reader"])
elif any(w in text_lower for w in ["podcast", "episode", "listen", "player"]):
label_ids.append(LABELS["podcasts"])
return label_ids
def handle_feedback(handler, body, user):
"""Create a Gitea issue from user feedback."""
try:
data = json.loads(body)
except Exception:
handler._send_json({"error": "Invalid JSON"}, 400)
return
text = (data.get("text") or "").strip()
source = data.get("source", "ios")
if not text:
handler._send_json({"error": "Feedback text is required"}, 400)
return
user_name = user.get("display_name") or user.get("username", "Unknown")
label_ids = auto_label(text, source)
# Create Gitea issue
issue_body = {
"title": text[:80] + ("..." if len(text) > 80 else ""),
"body": f"**From:** {user_name} ({source})\n\n{text}",
"labels": label_ids,
}
try:
req = urllib.request.Request(
f"{GITEA_URL}/api/v1/repos/{REPO}/issues",
data=json.dumps(issue_body).encode(),
headers={
"Authorization": f"token {GITEA_TOKEN}",
"Content-Type": "application/json",
},
method="POST",
)
resp = urllib.request.urlopen(req, timeout=10)
result = json.loads(resp.read())
handler._send_json({
"ok": True,
"issue_number": result.get("number"),
"url": result.get("html_url"),
})
except Exception as e:
handler._send_json({"error": f"Failed to create issue: {e}"}, 500)