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

View File

@@ -3,7 +3,7 @@ WORKDIR /app
RUN pip install --no-cache-dir bcrypt
RUN adduser --disabled-password --no-create-home appuser
RUN mkdir -p /app/data && chown -R appuser /app/data
COPY --chown=appuser server.py config.py database.py sessions.py proxy.py responses.py auth.py dashboard.py command.py assistant.py ./
COPY --chown=appuser server.py config.py database.py sessions.py proxy.py responses.py auth.py dashboard.py command.py assistant.py feedback.py ./
COPY --chown=appuser integrations/ ./integrations/
EXPOSE 8100
ENV PYTHONUNBUFFERED=1

View File

@@ -12,6 +12,7 @@ import sqlite3
import bcrypt
from config import SESSION_COOKIE_SECURE
from database import get_db
from sessions import create_session, delete_session
@@ -58,7 +59,10 @@ def handle_logout(handler):
delete_session(token)
handler.send_response(200)
handler.send_header("Content-Type", "application/json")
handler.send_header("Set-Cookie", "platform_session=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0")
cookie = "platform_session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"
if SESSION_COOKIE_SECURE:
cookie += "; Secure"
handler.send_header("Set-Cookie", cookie)
resp = b'{"success": true}'
handler.send_header("Content-Length", len(resp))
handler.end_headers()

View File

@@ -62,6 +62,7 @@ OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-5.2")
# ── Session config ──
SESSION_MAX_AGE = int(os.environ.get("SESSION_MAX_AGE", 30 * 86400)) # 30 days
SESSION_COOKIE_SECURE = os.environ.get("SESSION_COOKIE_SECURE", "").lower() in {"1", "true", "yes", "on"}
DEV_AUTO_LOGIN = os.environ.get("DEV_AUTO_LOGIN", "").lower() in {"1", "true", "yes", "on"}
DEV_AUTO_LOGIN_USERNAME = os.environ.get("DEV_AUTO_LOGIN_USERNAME", "dev")
DEV_AUTO_LOGIN_DISPLAY_NAME = os.environ.get("DEV_AUTO_LOGIN_DISPLAY_NAME", "Dev User")

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)

View File

@@ -5,7 +5,7 @@ Platform Gateway — Response helpers mixed into GatewayHandler.
import json
from http.cookies import SimpleCookie
from config import SESSION_MAX_AGE, DEV_AUTO_LOGIN
from config import SESSION_MAX_AGE, SESSION_COOKIE_SECURE, DEV_AUTO_LOGIN
from sessions import get_session_user, get_or_create_dev_user
@@ -43,8 +43,10 @@ class ResponseMixin:
return user
def _set_session_cookie(self, token):
self.send_header("Set-Cookie",
f"platform_session={token}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age={SESSION_MAX_AGE}")
cookie = f"platform_session={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_MAX_AGE}"
if SESSION_COOKIE_SECURE:
cookie += "; Secure"
self.send_header("Set-Cookie", cookie)
def _read_body(self):
length = int(self.headers.get("Content-Length", 0))

View File

@@ -25,6 +25,7 @@ from dashboard import (
handle_set_connection, handle_pin, handle_unpin, handle_get_pinned,
)
from command import handle_command
from feedback import handle_feedback
from assistant import handle_assistant, handle_fitness_assistant, handle_brain_assistant
from integrations.booklore import (
handle_booklore_libraries, handle_booklore_import,
@@ -243,6 +244,12 @@ class GatewayHandler(ResponseMixin, BaseHTTPRequestHandler):
handle_command(self, user, body)
return
if path == "/api/feedback":
user = self._require_auth()
if user:
handle_feedback(self, body, user)
return
if path == "/api/assistant":
user = self._require_auth()
if user: