diff --git a/docs/api/README.md b/docs/api/README.md index 0613cd95b..8578bb868 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -41,8 +41,8 @@ intentional: - **`tags`** — domain groupings (`Auth`, `Measurements`, `Medications`, `Mood`, `Dashboard`, `Insights`, `Achievements`, `DoctorReport`, `Integrations`, `User`, `Tokens`, `Notifications`, `Onboarding`, - `Health`, `Settings`, `Analytics`, `Export`, `AuditLog`, `Feedback`, - `BugReport`, `Monitoring`, `Webhooks`, `Ingest`, `Admin`). + `Health`, `Settings`, `Analytics`, `Export`, `AuditLog`, + `Monitoring`, `Webhooks`, `Ingest`, `Admin`). - **`securitySchemes`**: - `bearerAuth` — long-lived `hlk_*` API tokens issued via `POST /api/tokens`. Required on `/api/ingest/medication`. diff --git a/messages/de.json b/messages/de.json index 523158b31..ee4d5a1d6 100644 --- a/messages/de.json +++ b/messages/de.json @@ -32,7 +32,6 @@ "copy": "Kopieren", "copyDetails": "Details kopieren", "retry": "Erneut versuchen", - "reportIssue": "Fehler melden", "showPassword": "Passwort anzeigen", "hidePassword": "Passwort verbergen", "learnMore": "Mehr erfahren" @@ -204,7 +203,6 @@ "insights": "Insights", "achievements": "Erfolge", "settings": "Einstellungen", - "bugreport": "Bugreport", "admin": "Admin-Konsole", "logout": "Abmelden", "login": "Anmelden", @@ -5064,7 +5062,6 @@ "webPushGlobal": "Browser-Push-Benachrichtigungen erlauben.", "moodLogGlobal": "moodLog-Erinnerungen erlauben.", "integrationWebPush": "Web Push", - "integrationBugReport": "Bug-Reports", "offlineGeoLabel": "Geodaten", "offlineGeoEnabled": "Offline (GeoLite2)", "offlineGeoFallback": "Fallback (ipwho.is) — MAXMIND_LICENSE_KEY in CI setzen", @@ -5101,12 +5098,6 @@ "configured": "Konfiguriert", "monitoringTestSuccess": "Testevent wurde gesendet", "monitoringTestFailed": "Monitoring-Test fehlgeschlagen", - "bugReportGithub": "Bugreport (GitHub Issues)", - "bugReportGithubDescription": "Hinterlege GitHub-Issue-Token und Repository für eingehende Bugreports.", - "bugReportEnabledLabel": "Bug-Reports aktiviert", - "bugReportEnabledDescription": "Wenn deaktiviert, ist der Report-Button im Frontend nicht mehr sichtbar — Token und Repository bleiben gespeichert.", - "bugReportDeprecatedBadge": "Veraltet", - "bugReportDeprecatedNote": "Diese Funktion ist veraltet und wird in einer künftigen Version entfernt.", "assistant": { "title": "Assistenz-Oberflächen", "description": "Jeder Toggle versteckt eine modellgestützte Oberfläche app-weit, ohne die Provider-Konfiguration zu entfernen. Der Hauptschalter erzwingt jede Unter-Option aus.", @@ -5140,10 +5131,6 @@ "description": "Schalte ein Modul für den gesamten Server ab. Ein hier deaktiviertes Modul ist für jedes Konto aus – unabhängig von den persönlichen Einstellungen. Kerndaten (Gewicht, Blutdruck, Puls, Medikamente) sind immer aktiv und lassen sich nicht abschalten.", "saveError": "Modulverfügbarkeit konnte nicht aktualisiert werden." }, - "bugReportRepo": "Repository", - "bugReportRepoPlaceholder": "owner/repo", - "bugReportToken": "Issue-Token", - "bugReportTokenPlaceholder": "ghp_...", "invites": { "title": "Einladungen", "description": "Einladungslinks für die Registrierung — funktionieren auch bei geschlossener Registrierung. Der Link wird nur einmal angezeigt.", @@ -5297,47 +5284,6 @@ "reminderCheckNotifOff": "Benachrichtigungen aus", "reminderCheckEventsToday": "Heutige Events", "reminderCheckNotifSent": "Gesendet", - "feedback": { - "tabOpen": "Offen", - "tabAcknowledged": "Bestätigt", - "tabResolved": "Erledigt", - "tabArchived": "Archiviert", - "subject": "Betreff", - "category": "Kategorie", - "user": "Benutzer", - "createdAt": "Eingegangen", - "actions": "Aktionen", - "details": "Details", - "emptyTitle": "Alles erledigt", - "emptyDescription": "Kein Nutzer-Feedback in diesem Status. Bug-Reports, Fragen und Feature-Wünsche erscheinen hier.", - "loading": "Lade Feedback...", - "anonymous": "anonym", - "categoryBug": "Bug", - "categoryFeature": "Feature", - "categoryQuestion": "Frage", - "categoryOther": "Sonstiges", - "metadataHeading": "Metadaten", - "metaUrl": "URL", - "metaLocale": "Sprache", - "metaUserAgent": "User-Agent", - "metaAppVersion": "App-Version", - "screenshotHeading": "Screenshot", - "adminNote": "Admin-Notiz", - "adminNotePlaceholder": "Interne Notiz (nicht für Benutzer sichtbar)", - "saveNote": "Notiz speichern", - "noteSaved": "Notiz gespeichert", - "actionAcknowledge": "Bestätigen", - "actionResolve": "Erledigen", - "actionArchive": "Archivieren", - "actionPublishGithub": "Auf GitHub veröffentlichen", - "publishedToGithub": "Auf GitHub veröffentlicht", - "viewIssue": "Issue ansehen", - "publishSuccess": "Issue erstellt", - "publishFailed": "GitHub-Veröffentlichung fehlgeschlagen", - "updateFailed": "Aktualisierung fehlgeschlagen", - "archiveConfirm": "Diesen Feedback-Eintrag archivieren?", - "submittedBy": "Eingereicht von" - }, "shell": { "overview": "Übersicht", "sectionsNav": "Admin-Bereiche" @@ -5390,10 +5336,6 @@ "title": "Coach", "subtitle": "Assistenz-Oberflächen, Server-Schlüssel sowie die Feedback-Qualitäts-Tabellen für Coach und Auswertungen an einem Ort." }, - "feedback": { - "title": "Feedback", - "subtitle": "Feedback, Bug-Reports und Feature-Wünsche der Nutzer sichten." - }, "reminders": { "title": "Erinnerungen", "subtitle": "Schwellwerte für Medikamentenerinnerungen anpassen." @@ -5583,10 +5525,6 @@ "title": "Oldschool", "description": "Melde dich mindestens einmal mit Username und Passwort an." }, - "bugReport1": { - "title": "Fehlerjäger", - "description": "Reiche einen Bugreport ein." - }, "loginStreak7": { "title": "Login-Serie 7T", "description": "Logge dich an 7 Tagen in Folge ein." @@ -5775,10 +5713,6 @@ "title": "Polyglott", "description": "Du hast die App-Sprache mindestens einmal gewechselt." }, - "hiddenBugBuddy": { - "title": "Bug-Kumpel", - "description": "Mindestens 5 Bugreports eingereicht — das Projekt liebt dich." - }, "missFree7": { "title": "Lückenlos · 7T", "description": "7 Tage in Folge ohne eine verpasste Dosis." @@ -5831,26 +5765,6 @@ "empty": "Noch keine Erfolge — trage weiter ein, um deinen ersten freizuschalten." } }, - "bugreport": { - "title": "Bugreport", - "subtitle": "Sende eine Fehlermeldung, einen Feature-Wunsch oder eine Frage. Der Server-Administrator sieht deine Einreichung.", - "loginRequired": "Bitte anmelden, um einen Bug zu melden.", - "bugTitle": "Titel", - "description": "Beschreibung", - "submit": "Senden", - "success": "Vielen Dank! Dein Bug Report wurde erfolgreich übermittelt.", - "errorCreating": "Fehler beim Erstellen", - "category": "Art", - "categoryBug": "Fehlermeldung", - "categoryFeature": "Feature-Wunsch", - "categoryQuestion": "Frage", - "categoryOther": "Sonstiges Feedback", - "bugTitlePlaceholder": "Kurze Zusammenfassung (optional)", - "githubEscalationNote": "Dein Feedback wird lokal gespeichert. Wenn der Administrator die GitHub-Integration aktiviert, kann es auf Wunsch als öffentliches Issue veröffentlicht werden.", - "internalOnlyNote": "Dein Feedback wird lokal gespeichert und ist nur für den Administrator dieses Servers sichtbar.", - "disabledTitle": "Bug-Reports sind deaktiviert", - "disabledBody": "Der Administrator hat Bug-Reports auf diesem Server deaktiviert." - }, "notifications": { "title": "Benachrichtigungen", "eventColumn": "Ereignis", @@ -5955,7 +5869,8 @@ "eventMeasurementReminder": "Mess-Erinnerung", "eventMeasurementReminderDesc": "Wird gesendet, wenn eine erfasste Messung fällig ist.", "eventMedicationIntakeSync": "Einnahme-Synchronisierung", - "eventMedicationIntakeSyncDesc": "Geräteübergreifende Aktualisierung, wenn eine Dosis protokolliert, eingenommen oder übersprungen wird." + "eventMedicationIntakeSyncDesc": "Geräteübergreifende Aktualisierung, wenn eine Dosis protokolliert, eingenommen oder übersprungen wird.", + "loginRequired": "Bitte melde dich an, um Benachrichtigungen zu verwalten." }, "i18n": { "maintainershipBanner": { diff --git a/messages/en.json b/messages/en.json index 16ec52b02..3bc79e698 100644 --- a/messages/en.json +++ b/messages/en.json @@ -32,7 +32,6 @@ "copy": "Copy", "copyDetails": "Copy details", "retry": "Retry", - "reportIssue": "Report issue", "showPassword": "Show password", "hidePassword": "Hide password", "learnMore": "Learn more" @@ -204,7 +203,6 @@ "insights": "Insights", "achievements": "Achievements", "settings": "Settings", - "bugreport": "Bug Report", "admin": "Admin Console", "logout": "Sign out", "login": "Sign in", @@ -5064,7 +5062,6 @@ "webPushGlobal": "Allow browser push notifications.", "moodLogGlobal": "Allow mood-log reminders.", "integrationWebPush": "Web Push", - "integrationBugReport": "Bug Report", "offlineGeoLabel": "Geo data", "offlineGeoEnabled": "Offline (GeoLite2)", "offlineGeoFallback": "Fallback (ipwho.is) — set MAXMIND_LICENSE_KEY in CI", @@ -5101,12 +5098,6 @@ "configured": "Configured", "monitoringTestSuccess": "Test event sent", "monitoringTestFailed": "Monitoring test failed", - "bugReportGithub": "Bug report (GitHub Issues)", - "bugReportGithubDescription": "Configure GitHub issue token and repository for incoming bug reports.", - "bugReportEnabledLabel": "Bug reports enabled", - "bugReportEnabledDescription": "When off, the in-app report button hides — token and repository stay configured.", - "bugReportDeprecatedBadge": "Deprecated", - "bugReportDeprecatedNote": "This feature is deprecated and will be removed in a future version.", "assistant": { "title": "Assistant Surfaces", "description": "Each toggle hides a model-driven surface app-wide without removing the provider configuration. The master kills every sub-flag at once.", @@ -5140,10 +5131,6 @@ "description": "Turn a module off for the whole server. A module disabled here is off for every account regardless of personal preference. Core domains (weight, blood pressure, pulse, medications) are always on and cannot be disabled.", "saveError": "Could not update module availability." }, - "bugReportRepo": "Repository", - "bugReportRepoPlaceholder": "owner/repo", - "bugReportToken": "Issue token", - "bugReportTokenPlaceholder": "ghp_...", "invites": { "title": "Invites", "description": "Registration invite links — they work even while registration is closed. The link is shown only once.", @@ -5297,47 +5284,6 @@ "reminderCheckNotifOff": "Notifications off", "reminderCheckEventsToday": "Events today", "reminderCheckNotifSent": "Sent", - "feedback": { - "tabOpen": "Open", - "tabAcknowledged": "Acknowledged", - "tabResolved": "Resolved", - "tabArchived": "Archived", - "subject": "Subject", - "category": "Category", - "user": "User", - "createdAt": "Submitted", - "actions": "Actions", - "details": "Details", - "emptyTitle": "All caught up", - "emptyDescription": "No user feedback in this status. Submitted bugs, questions, and feature requests will appear here.", - "loading": "Loading feedback...", - "anonymous": "anonymous", - "categoryBug": "Bug", - "categoryFeature": "Feature", - "categoryQuestion": "Question", - "categoryOther": "Other", - "metadataHeading": "Metadata", - "metaUrl": "URL", - "metaLocale": "Locale", - "metaUserAgent": "User agent", - "metaAppVersion": "App version", - "screenshotHeading": "Screenshot", - "adminNote": "Admin note", - "adminNotePlaceholder": "Internal note (not visible to user)", - "saveNote": "Save note", - "noteSaved": "Note saved", - "actionAcknowledge": "Acknowledge", - "actionResolve": "Resolve", - "actionArchive": "Archive", - "actionPublishGithub": "Publish to GitHub", - "publishedToGithub": "Published to GitHub", - "viewIssue": "View issue", - "publishSuccess": "Issue created", - "publishFailed": "GitHub publish failed", - "updateFailed": "Update failed", - "archiveConfirm": "Archive this feedback entry?", - "submittedBy": "Submitted by" - }, "shell": { "overview": "Overview", "sectionsNav": "Admin sections" @@ -5390,10 +5336,6 @@ "title": "Coach", "subtitle": "Assistant surfaces, server key, and the Coach and Insights feedback-quality tables in one place." }, - "feedback": { - "title": "Feedback", - "subtitle": "Triage user feedback, bug reports, and feature requests." - }, "reminders": { "title": "Reminders", "subtitle": "Tune medication reminder thresholds and run a manual check." @@ -5583,10 +5525,6 @@ "title": "Old school", "description": "Sign in at least once with username and password." }, - "bugReport1": { - "title": "Bug hunter", - "description": "Submit a bug report." - }, "loginStreak7": { "title": "Login streak 7d", "description": "Sign in on 7 consecutive days." @@ -5775,10 +5713,6 @@ "title": "Polyglot", "description": "Switched the app language at least once." }, - "hiddenBugBuddy": { - "title": "Bug buddy", - "description": "Submitted at least 5 bug reports — the project loves you." - }, "missFree7": { "title": "No dose missed · 7d", "description": "7 consecutive days without a missed dose." @@ -5831,26 +5765,6 @@ "empty": "No achievements yet — keep logging to unlock your first." } }, - "bugreport": { - "title": "Bug Report", - "subtitle": "Send a bug report, feature request, or question. The server administrator will see your submission.", - "loginRequired": "Please sign in to report a bug.", - "bugTitle": "Title", - "description": "Description", - "submit": "Send", - "success": "Thank you! Your bug report has been submitted successfully.", - "errorCreating": "Error creating report", - "category": "Type", - "categoryBug": "Bug report", - "categoryFeature": "Feature request", - "categoryQuestion": "Question", - "categoryOther": "Other feedback", - "bugTitlePlaceholder": "Short summary (optional)", - "githubEscalationNote": "Your feedback is stored locally. If the admin enables GitHub integration, it can be promoted to a public issue on request.", - "internalOnlyNote": "Your feedback is stored locally and only visible to the administrator of this server.", - "disabledTitle": "Bug reports are turned off", - "disabledBody": "The administrator has disabled bug reports on this server." - }, "notifications": { "title": "Notifications", "eventColumn": "Event", @@ -5955,7 +5869,8 @@ "eventMeasurementReminder": "Measurement reminder", "eventMeasurementReminderDesc": "Sent when a tracked measurement is due.", "eventMedicationIntakeSync": "Medication intake sync", - "eventMedicationIntakeSyncDesc": "Cross-device update when a dose is logged, taken or skipped." + "eventMedicationIntakeSyncDesc": "Cross-device update when a dose is logged, taken or skipped.", + "loginRequired": "Please sign in to manage notifications." }, "i18n": { "maintainershipBanner": { diff --git a/messages/es.json b/messages/es.json index 8bea74277..9aff55383 100644 --- a/messages/es.json +++ b/messages/es.json @@ -32,7 +32,6 @@ "copy": "Copiar", "copyDetails": "Copiar detalles", "retry": "Reintentar", - "reportIssue": "Reportar problema", "showPassword": "Mostrar contraseña", "hidePassword": "Ocultar contraseña", "learnMore": "Más información" @@ -204,7 +203,6 @@ "insights": "Insights", "achievements": "Logros", "settings": "Ajustes", - "bugreport": "Reporte de bug", "admin": "Consola de administración", "logout": "Cerrar sesión", "login": "Iniciar sesión", @@ -5064,7 +5062,6 @@ "webPushGlobal": "Permitir notificaciones push del navegador.", "moodLogGlobal": "Permitir recordatorios del registro de ánimo.", "integrationWebPush": "Web Push", - "integrationBugReport": "Informe de errores", "offlineGeoLabel": "Datos geográficos", "offlineGeoEnabled": "Sin conexión (GeoLite2)", "offlineGeoFallback": "Respaldo (ipwho.is) — configura MAXMIND_LICENSE_KEY en CI", @@ -5101,12 +5098,6 @@ "configured": "Configurado", "monitoringTestSuccess": "Evento de prueba enviado", "monitoringTestFailed": "La prueba de monitorización falló", - "bugReportGithub": "Informe de errores (GitHub Issues)", - "bugReportGithubDescription": "Configura el token de issues de GitHub y el repositorio para los informes de errores entrantes.", - "bugReportEnabledLabel": "Informes de errores activados", - "bugReportEnabledDescription": "Cuando está desactivado, el botón de informe en la app se oculta; el token y el repositorio permanecen configurados.", - "bugReportDeprecatedBadge": "Obsoleto", - "bugReportDeprecatedNote": "Esta función está obsoleta y se eliminará en una versión futura.", "assistant": { "title": "Superficies del asistente", "description": "Cada conmutador oculta una superficie impulsada por el modelo en toda la aplicación sin retirar la configuración del proveedor. El interruptor maestro fuerza cada opción secundaria a off.", @@ -5140,10 +5131,6 @@ "description": "Desactiva un módulo para todo el servidor. Un módulo desactivado aquí queda apagado para todas las cuentas, sin importar la preferencia personal. Los dominios principales (peso, presión arterial, pulso, medicación) están siempre activos y no se pueden desactivar.", "saveError": "No se pudo actualizar la disponibilidad de módulos." }, - "bugReportRepo": "Repositorio", - "bugReportRepoPlaceholder": "owner/repo", - "bugReportToken": "Token de issues", - "bugReportTokenPlaceholder": "ghp_...", "invites": { "title": "Invitaciones", "description": "Enlaces de invitación para el registro: funcionan incluso con el registro cerrado. El enlace se muestra solo una vez.", @@ -5297,47 +5284,6 @@ "reminderCheckNotifOff": "Notificaciones desactivadas", "reminderCheckEventsToday": "Eventos de hoy", "reminderCheckNotifSent": "Enviadas", - "feedback": { - "tabOpen": "Abierto", - "tabAcknowledged": "Confirmado", - "tabResolved": "Resuelto", - "tabArchived": "Archivado", - "subject": "Asunto", - "category": "Categoría", - "user": "Usuario", - "createdAt": "Enviado", - "actions": "Acciones", - "details": "Detalles", - "emptyTitle": "Todo al día", - "emptyDescription": "No hay comentarios de usuarios en este estado. Los errores, preguntas y solicitudes de funciones enviados aparecerán aquí.", - "loading": "Cargando comentarios...", - "anonymous": "anónimo", - "categoryBug": "Error", - "categoryFeature": "Función", - "categoryQuestion": "Pregunta", - "categoryOther": "Otro", - "metadataHeading": "Metadatos", - "metaUrl": "URL", - "metaLocale": "Idioma", - "metaUserAgent": "Agente de usuario", - "metaAppVersion": "Versión de la app", - "screenshotHeading": "Screenshot", - "adminNote": "Nota de administración", - "adminNotePlaceholder": "Nota interna (no visible para el usuario)", - "saveNote": "Guardar nota", - "noteSaved": "Nota guardada", - "actionAcknowledge": "Confirmar", - "actionResolve": "Resolver", - "actionArchive": "Archivar", - "actionPublishGithub": "Publicar en GitHub", - "publishedToGithub": "Publicado en GitHub", - "viewIssue": "Ver issue", - "publishSuccess": "Issue creada", - "publishFailed": "La publicación en GitHub falló", - "updateFailed": "La actualización falló", - "archiveConfirm": "¿Archivar esta entrada de comentarios?", - "submittedBy": "Enviado por" - }, "shell": { "overview": "Resumen", "sectionsNav": "Secciones de administración" @@ -5390,10 +5336,6 @@ "title": "Coach", "subtitle": "Superficies del asistente, clave del servidor y las tablas de calidad de comentarios del Coach y de los análisis, en un solo lugar." }, - "feedback": { - "title": "Comentarios", - "subtitle": "Clasifica los comentarios de usuarios, los informes de errores y las solicitudes de funciones." - }, "reminders": { "title": "Recordatorios", "subtitle": "Ajusta los umbrales de los recordatorios de medicación y ejecuta una comprobación manual." @@ -5583,10 +5525,6 @@ "title": "A la antigua", "description": "Inicia sesión al menos una vez con usuario y contraseña." }, - "bugReport1": { - "title": "Cazador de errores", - "description": "Envía un informe de error." - }, "loginStreak7": { "title": "Racha de inicios 7d", "description": "Inicia sesión 7 días seguidos." @@ -5775,10 +5713,6 @@ "title": "Políglota", "description": "Cambiaste el idioma de la app al menos una vez." }, - "hiddenBugBuddy": { - "title": "Colega de errores", - "description": "Enviaste al menos 5 informes de error — el proyecto te quiere." - }, "missFree7": { "title": "Sin dosis perdidas · 7d", "description": "7 días consecutivos sin una dosis perdida." @@ -5831,26 +5765,6 @@ "empty": "Aún no hay logros — sigue registrando para desbloquear el primero." } }, - "bugreport": { - "title": "Reporte de bug", - "subtitle": "Envía un fallo, una sugerencia o una pregunta. El administrador del servidor verá tu envío.", - "loginRequired": "Inicia sesión para reportar un bug.", - "bugTitle": "Título", - "description": "Descripción", - "submit": "Enviar", - "success": "¡Gracias! Tu reporte se ha enviado correctamente.", - "errorCreating": "Error al crear", - "category": "Tipo", - "categoryBug": "Fallo", - "categoryFeature": "Sugerencia", - "categoryQuestion": "Pregunta", - "categoryOther": "Otro feedback", - "bugTitlePlaceholder": "Resumen breve (opcional)", - "githubEscalationNote": "Tu feedback se guarda localmente. Si el administrador activa la integración con GitHub, puede publicarse como issue público a petición.", - "internalOnlyNote": "Tu feedback se guarda localmente y solo es visible para el administrador de este servidor.", - "disabledTitle": "Los informes de errores están desactivados", - "disabledBody": "El administrador ha desactivado los informes de errores en este servidor." - }, "notifications": { "title": "Notificaciones", "eventColumn": "Evento", @@ -5955,7 +5869,8 @@ "eventMeasurementReminder": "Recordatorio de medición", "eventMeasurementReminderDesc": "Se envía cuando una medición registrada está pendiente.", "eventMedicationIntakeSync": "Sincronización de tomas", - "eventMedicationIntakeSyncDesc": "Actualización entre dispositivos cuando se registra, toma u omite una dosis." + "eventMedicationIntakeSyncDesc": "Actualización entre dispositivos cuando se registra, toma u omite una dosis.", + "loginRequired": "Inicia sesión para gestionar las notificaciones." }, "i18n": { "maintainershipBanner": { diff --git a/messages/fr.json b/messages/fr.json index d943773ff..8d799c471 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -32,7 +32,6 @@ "copy": "Copier", "copyDetails": "Copier les détails", "retry": "Réessayer", - "reportIssue": "Signaler un problème", "showPassword": "Afficher le mot de passe", "hidePassword": "Masquer le mot de passe", "learnMore": "En savoir plus" @@ -204,7 +203,6 @@ "insights": "Insights", "achievements": "Succès", "settings": "Paramètres", - "bugreport": "Rapport de bug", "admin": "Console d’administration", "logout": "Se déconnecter", "login": "Se connecter", @@ -5064,7 +5062,6 @@ "webPushGlobal": "Autoriser les notifications push du navigateur.", "moodLogGlobal": "Autoriser les rappels du journal d'humeur.", "integrationWebPush": "Web Push", - "integrationBugReport": "Rapport de bug", "offlineGeoLabel": "Données géographiques", "offlineGeoEnabled": "Hors ligne (GeoLite2)", "offlineGeoFallback": "Fallback (ipwho.is) — définissez MAXMIND_LICENSE_KEY dans la CI", @@ -5101,12 +5098,6 @@ "configured": "Configuré", "monitoringTestSuccess": "Événement de test envoyé", "monitoringTestFailed": "Échec du test de surveillance", - "bugReportGithub": "Rapport de bug (GitHub Issues)", - "bugReportGithubDescription": "Configurez le jeton d'issues GitHub et le dépôt pour les rapports de bug entrants.", - "bugReportEnabledLabel": "Rapports de bug activés", - "bugReportEnabledDescription": "Lorsque cette option est désactivée, le bouton de signalement dans l'app est masqué — le jeton et le dépôt restent configurés.", - "bugReportDeprecatedBadge": "Obsolète", - "bugReportDeprecatedNote": "Cette fonctionnalité est obsolète et sera supprimée dans une version future.", "assistant": { "title": "Surfaces d'assistance", "description": "Chaque bascule masque une surface pilotée par le modèle dans toute l'application sans retirer la configuration du fournisseur. Le commutateur principal force chaque option secondaire à off.", @@ -5140,10 +5131,6 @@ "description": "Désactivez un module pour tout le serveur. Un module désactivé ici est coupé pour chaque compte, quelle que soit la préférence personnelle. Les domaines essentiels (poids, tension, pouls, médicaments) restent toujours actifs et ne peuvent pas être désactivés.", "saveError": "Impossible de mettre à jour la disponibilité des modules." }, - "bugReportRepo": "Dépôt", - "bugReportRepoPlaceholder": "owner/repo", - "bugReportToken": "Jeton d'issue", - "bugReportTokenPlaceholder": "ghp_...", "invites": { "title": "Invitations", "description": "Liens d'invitation pour l'inscription — ils fonctionnent même lorsque les inscriptions sont fermées. Le lien n'est affiché qu'une seule fois.", @@ -5297,47 +5284,6 @@ "reminderCheckNotifOff": "Notifications désactivées", "reminderCheckEventsToday": "Événements aujourd'hui", "reminderCheckNotifSent": "Envoyées", - "feedback": { - "tabOpen": "Ouvert", - "tabAcknowledged": "Pris en compte", - "tabResolved": "Résolu", - "tabArchived": "Archivé", - "subject": "Sujet", - "category": "Catégorie", - "user": "Utilisateur", - "createdAt": "Soumis", - "actions": "Actions", - "details": "Détails", - "emptyTitle": "Tout est à jour", - "emptyDescription": "Aucun retour utilisateur dans ce statut. Les bugs, questions et demandes de fonctionnalités soumis apparaîtront ici.", - "loading": "Chargement des retours...", - "anonymous": "anonyme", - "categoryBug": "Bug", - "categoryFeature": "Fonctionnalité", - "categoryQuestion": "Question", - "categoryOther": "Autre", - "metadataHeading": "Métadonnées", - "metaUrl": "URL", - "metaLocale": "Langue", - "metaUserAgent": "Agent utilisateur", - "metaAppVersion": "Version de l'app", - "screenshotHeading": "Screenshot", - "adminNote": "Note d'administration", - "adminNotePlaceholder": "Note interne (non visible par l'utilisateur)", - "saveNote": "Enregistrer la note", - "noteSaved": "Note enregistrée", - "actionAcknowledge": "Prendre en compte", - "actionResolve": "Résoudre", - "actionArchive": "Archiver", - "actionPublishGithub": "Publier sur GitHub", - "publishedToGithub": "Publié sur GitHub", - "viewIssue": "Voir l'issue", - "publishSuccess": "Issue créée", - "publishFailed": "Échec de la publication sur GitHub", - "updateFailed": "Échec de la mise à jour", - "archiveConfirm": "Archiver cette entrée de retour ?", - "submittedBy": "Soumis par" - }, "shell": { "overview": "Vue d'ensemble", "sectionsNav": "Sections d'administration" @@ -5390,10 +5336,6 @@ "title": "Coach", "subtitle": "Surfaces d'assistance, clé du serveur et les tableaux de qualité des retours du Coach et des analyses, au même endroit." }, - "feedback": { - "title": "Retours", - "subtitle": "Triez les retours utilisateurs, les rapports de bug et les demandes de fonctionnalités." - }, "reminders": { "title": "Rappels", "subtitle": "Réglez les seuils des rappels de médicaments et lancez une vérification manuelle." @@ -5583,10 +5525,6 @@ "title": "À l’ancienne", "description": "Connectez-vous au moins une fois avec un identifiant et un mot de passe." }, - "bugReport1": { - "title": "Chasseur de bugs", - "description": "Soumettez un rapport de bug." - }, "loginStreak7": { "title": "Série de connexions 7j", "description": "Connectez-vous 7 jours d’affilée." @@ -5775,10 +5713,6 @@ "title": "Polyglotte", "description": "Vous avez changé la langue de l’application au moins une fois." }, - "hiddenBugBuddy": { - "title": "Compagnon des bugs", - "description": "Vous avez soumis au moins 5 rapports de bug — le projet vous adore." - }, "missFree7": { "title": "Aucune dose oubliée · 7j", "description": "7 jours consécutifs sans dose oubliée." @@ -5831,26 +5765,6 @@ "empty": "Pas encore de réussites — continuez à suivre pour débloquer la première." } }, - "bugreport": { - "title": "Rapport de bug", - "subtitle": "Envoie un bug, une suggestion ou une question. L’administrateur du serveur verra ton envoi.", - "loginRequired": "Connecte-toi pour signaler un bug.", - "bugTitle": "Titre", - "description": "Description", - "submit": "Envoyer", - "success": "Merci ! Ton rapport a bien été envoyé.", - "errorCreating": "Erreur lors de la création", - "category": "Type", - "categoryBug": "Bug", - "categoryFeature": "Suggestion", - "categoryQuestion": "Question", - "categoryOther": "Autre retour", - "bugTitlePlaceholder": "Résumé court (optionnel)", - "githubEscalationNote": "Ton retour est stocké localement. Si l’administrateur active l’intégration GitHub, il pourra être publié comme issue public à la demande.", - "internalOnlyNote": "Ton retour est stocké localement et n’est visible que par l’administrateur de ce serveur.", - "disabledTitle": "Les rapports de bug sont désactivés", - "disabledBody": "L'administrateur a désactivé les rapports de bug sur ce serveur." - }, "notifications": { "title": "Notifications", "eventColumn": "Événement", @@ -5955,7 +5869,8 @@ "eventMeasurementReminder": "Rappel de mesure", "eventMeasurementReminderDesc": "Envoyé lorsqu'une mesure suivie est due.", "eventMedicationIntakeSync": "Synchronisation des prises", - "eventMedicationIntakeSyncDesc": "Mise à jour entre appareils quand une dose est enregistrée, prise ou ignorée." + "eventMedicationIntakeSyncDesc": "Mise à jour entre appareils quand une dose est enregistrée, prise ou ignorée.", + "loginRequired": "Connecte-toi pour gérer les notifications." }, "i18n": { "maintainershipBanner": { diff --git a/messages/it.json b/messages/it.json index 38c204fbb..c166e09cf 100644 --- a/messages/it.json +++ b/messages/it.json @@ -32,7 +32,6 @@ "copy": "Copia", "copyDetails": "Copia dettagli", "retry": "Riprova", - "reportIssue": "Segnala un problema", "showPassword": "Mostra password", "hidePassword": "Nascondi password", "learnMore": "Scopri di più" @@ -204,7 +203,6 @@ "insights": "Insights", "achievements": "Traguardi", "settings": "Impostazioni", - "bugreport": "Segnalazione bug", "admin": "Console di amministrazione", "logout": "Esci", "login": "Accedi", @@ -5064,7 +5062,6 @@ "webPushGlobal": "Consentire le notifiche push del browser.", "moodLogGlobal": "Consentire i promemoria del diario dell'umore.", "integrationWebPush": "Web Push", - "integrationBugReport": "Segnalazione di bug", "offlineGeoLabel": "Dati geografici", "offlineGeoEnabled": "Offline (GeoLite2)", "offlineGeoFallback": "Fallback (ipwho.is) — imposta MAXMIND_LICENSE_KEY in CI", @@ -5101,12 +5098,6 @@ "configured": "Configurato", "monitoringTestSuccess": "Evento di prova inviato", "monitoringTestFailed": "Test di monitoraggio non riuscito", - "bugReportGithub": "Segnalazione di bug (GitHub Issues)", - "bugReportGithubDescription": "Configura il token delle issue di GitHub e il repository per le segnalazioni di bug in arrivo.", - "bugReportEnabledLabel": "Segnalazioni di bug attive", - "bugReportEnabledDescription": "Se disattivato, il pulsante di segnalazione nell'app viene nascosto: token e repository restano configurati.", - "bugReportDeprecatedBadge": "Obsoleto", - "bugReportDeprecatedNote": "Questa funzione è obsoleta e verrà rimossa in una versione futura.", "assistant": { "title": "Superfici dell'assistente", "description": "Ogni interruttore nasconde una superficie guidata dal modello in tutta l'app senza rimuovere la configurazione del provider. Il master forza ogni opzione secondaria su off.", @@ -5140,10 +5131,6 @@ "description": "Disattiva un modulo per l'intero server. Un modulo disattivato qui è spento per ogni account, indipendentemente dalle preferenze personali. I domini principali (peso, pressione, polso, farmaci) sono sempre attivi e non possono essere disattivati.", "saveError": "Impossibile aggiornare la disponibilità dei moduli." }, - "bugReportRepo": "Repository", - "bugReportRepoPlaceholder": "owner/repo", - "bugReportToken": "Token delle issue", - "bugReportTokenPlaceholder": "ghp_...", "invites": { "title": "Inviti", "description": "Link di invito per la registrazione: funzionano anche quando la registrazione è chiusa. Il link viene mostrato una sola volta.", @@ -5297,47 +5284,6 @@ "reminderCheckNotifOff": "Notifiche disattivate", "reminderCheckEventsToday": "Eventi di oggi", "reminderCheckNotifSent": "Inviate", - "feedback": { - "tabOpen": "Aperto", - "tabAcknowledged": "Preso in carico", - "tabResolved": "Risolto", - "tabArchived": "Archiviato", - "subject": "Oggetto", - "category": "Categoria", - "user": "Utente", - "createdAt": "Inviato", - "actions": "Azioni", - "details": "Dettagli", - "emptyTitle": "Tutto in ordine", - "emptyDescription": "Nessun feedback degli utenti in questo stato. Bug, domande e richieste di funzionalità inviati compariranno qui.", - "loading": "Caricamento feedback...", - "anonymous": "anonimo", - "categoryBug": "Bug", - "categoryFeature": "Funzionalità", - "categoryQuestion": "Domanda", - "categoryOther": "Altro", - "metadataHeading": "Metadati", - "metaUrl": "URL", - "metaLocale": "Lingua", - "metaUserAgent": "User agent", - "metaAppVersion": "Versione dell'app", - "screenshotHeading": "Screenshot", - "adminNote": "Nota amministrativa", - "adminNotePlaceholder": "Nota interna (non visibile all'utente)", - "saveNote": "Salva nota", - "noteSaved": "Nota salvata", - "actionAcknowledge": "Prendi in carico", - "actionResolve": "Risolvi", - "actionArchive": "Archivia", - "actionPublishGithub": "Pubblica su GitHub", - "publishedToGithub": "Pubblicato su GitHub", - "viewIssue": "Vedi issue", - "publishSuccess": "Issue creata", - "publishFailed": "Pubblicazione su GitHub non riuscita", - "updateFailed": "Aggiornamento non riuscito", - "archiveConfirm": "Archiviare questa voce di feedback?", - "submittedBy": "Inviato da" - }, "shell": { "overview": "Panoramica", "sectionsNav": "Sezioni di amministrazione" @@ -5390,10 +5336,6 @@ "title": "Coach", "subtitle": "Superfici dell'assistente, chiave del server e le tabelle di qualità del feedback di Coach e analisi, in un unico posto." }, - "feedback": { - "title": "Feedback", - "subtitle": "Smista i feedback degli utenti, le segnalazioni di bug e le richieste di funzionalità." - }, "reminders": { "title": "Promemoria", "subtitle": "Regola le soglie dei promemoria dei farmaci ed esegui un controllo manuale." @@ -5583,10 +5525,6 @@ "title": "Vecchia scuola", "description": "Accedi almeno una volta con nome utente e password." }, - "bugReport1": { - "title": "Cacciatore di bug", - "description": "Invia una segnalazione di bug." - }, "loginStreak7": { "title": "Serie di accessi 7g", "description": "Accedi per 7 giorni consecutivi." @@ -5775,10 +5713,6 @@ "title": "Poliglotta", "description": "Hai cambiato la lingua dell’app almeno una volta." }, - "hiddenBugBuddy": { - "title": "Amico dei bug", - "description": "Hai inviato almeno 5 segnalazioni di bug — il progetto ti ama." - }, "missFree7": { "title": "Nessuna dose persa · 7g", "description": "7 giorni consecutivi senza una dose persa." @@ -5831,26 +5765,6 @@ "empty": "Ancora nessun obiettivo — continua a registrare per sbloccare il primo." } }, - "bugreport": { - "title": "Segnalazione bug", - "subtitle": "Invia un bug, un’idea o una domanda. L’amministratore del server vedrà la tua segnalazione.", - "loginRequired": "Accedi per segnalare un bug.", - "bugTitle": "Titolo", - "description": "Descrizione", - "submit": "Invia", - "success": "Grazie! La tua segnalazione è stata inviata.", - "errorCreating": "Errore durante la creazione", - "category": "Tipo", - "categoryBug": "Bug", - "categoryFeature": "Suggerimento", - "categoryQuestion": "Domanda", - "categoryOther": "Altro feedback", - "bugTitlePlaceholder": "Riassunto breve (opzionale)", - "githubEscalationNote": "Il feedback viene salvato localmente. Se l’amministratore attiva l’integrazione GitHub, può essere pubblicato come issue pubblico su richiesta.", - "internalOnlyNote": "Il feedback viene salvato localmente ed è visibile solo all’amministratore di questo server.", - "disabledTitle": "Le segnalazioni di bug sono disattivate", - "disabledBody": "L'amministratore ha disattivato le segnalazioni di bug su questo server." - }, "notifications": { "title": "Notifiche", "eventColumn": "Evento", @@ -5955,7 +5869,8 @@ "eventMeasurementReminder": "Promemoria misurazione", "eventMeasurementReminderDesc": "Inviato quando una misurazione tracciata è in scadenza.", "eventMedicationIntakeSync": "Sincronizzazione assunzioni", - "eventMedicationIntakeSyncDesc": "Aggiornamento tra dispositivi quando una dose viene registrata, assunta o saltata." + "eventMedicationIntakeSyncDesc": "Aggiornamento tra dispositivi quando una dose viene registrata, assunta o saltata.", + "loginRequired": "Accedi per gestire le notifiche." }, "i18n": { "maintainershipBanner": { diff --git a/messages/pl.json b/messages/pl.json index 566625aa1..ad71a49b0 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -32,7 +32,6 @@ "copy": "Kopiuj", "copyDetails": "Kopiuj szczegóły", "retry": "Spróbuj ponownie", - "reportIssue": "Zgłoś problem", "showPassword": "Pokaż hasło", "hidePassword": "Ukryj hasło", "learnMore": "Dowiedz się więcej" @@ -204,7 +203,6 @@ "insights": "Insights", "achievements": "Osiągnięcia", "settings": "Ustawienia", - "bugreport": "Zgłoszenie błędu", "admin": "Konsola administratora", "logout": "Wyloguj", "login": "Zaloguj", @@ -5064,7 +5062,6 @@ "webPushGlobal": "Zezwól na powiadomienia push przeglądarki.", "moodLogGlobal": "Zezwól na przypomnienia dziennika nastroju.", "integrationWebPush": "Web Push", - "integrationBugReport": "Zgłoszenie błędu", "offlineGeoLabel": "Dane geograficzne", "offlineGeoEnabled": "Offline (GeoLite2)", "offlineGeoFallback": "Awaryjne (ipwho.is) — ustaw MAXMIND_LICENSE_KEY w CI", @@ -5101,12 +5098,6 @@ "configured": "Skonfigurowano", "monitoringTestSuccess": "Wysłano zdarzenie testowe", "monitoringTestFailed": "Test monitorowania nie powiódł się", - "bugReportGithub": "Zgłoszenie błędu (GitHub Issues)", - "bugReportGithubDescription": "Skonfiguruj token zgłoszeń GitHub i repozytorium dla przychodzących zgłoszeń błędów.", - "bugReportEnabledLabel": "Zgłaszanie błędów włączone", - "bugReportEnabledDescription": "Gdy wyłączone, przycisk zgłaszania w aplikacji jest ukryty — token i repozytorium pozostają skonfigurowane.", - "bugReportDeprecatedBadge": "Wycofane", - "bugReportDeprecatedNote": "Ta funkcja jest wycofywana i zostanie usunięta w przyszłej wersji.", "assistant": { "title": "Powierzchnie asystenta", "description": "Każdy przełącznik ukrywa powierzchnię opartą na modelu w całej aplikacji bez usuwania konfiguracji dostawcy. Główny wyłącznik wymusza wyłączenie każdej opcji podrzędnej.", @@ -5140,10 +5131,6 @@ "description": "Wyłącz moduł dla całego serwera. Moduł wyłączony tutaj jest nieaktywny dla każdego konta, niezależnie od preferencji użytkownika. Domeny podstawowe (waga, ciśnienie, tętno, leki) są zawsze włączone i nie można ich wyłączyć.", "saveError": "Nie udało się zaktualizować dostępności modułów." }, - "bugReportRepo": "Repozytorium", - "bugReportRepoPlaceholder": "owner/repo", - "bugReportToken": "Token zgłoszeń", - "bugReportTokenPlaceholder": "ghp_...", "invites": { "title": "Zaproszenia", "description": "Linki z zaproszeniem do rejestracji — działają nawet przy zamkniętej rejestracji. Link jest wyświetlany tylko raz.", @@ -5297,47 +5284,6 @@ "reminderCheckNotifOff": "Powiadomienia wyłączone", "reminderCheckEventsToday": "Zdarzenia dzisiaj", "reminderCheckNotifSent": "Wysłane", - "feedback": { - "tabOpen": "Otwarte", - "tabAcknowledged": "Potwierdzone", - "tabResolved": "Rozwiązane", - "tabArchived": "Zarchiwizowane", - "subject": "Temat", - "category": "Kategoria", - "user": "Użytkownik", - "createdAt": "Przesłano", - "actions": "Akcje", - "details": "Szczegóły", - "emptyTitle": "Wszystko na bieżąco", - "emptyDescription": "Brak opinii użytkowników w tym statusie. Przesłane błędy, pytania i prośby o funkcje pojawią się tutaj.", - "loading": "Ładowanie opinii...", - "anonymous": "anonimowy", - "categoryBug": "Błąd", - "categoryFeature": "Funkcja", - "categoryQuestion": "Pytanie", - "categoryOther": "Inne", - "metadataHeading": "Metadane", - "metaUrl": "URL", - "metaLocale": "Język", - "metaUserAgent": "Agent użytkownika", - "metaAppVersion": "Wersja aplikacji", - "screenshotHeading": "Screenshot", - "adminNote": "Notatka administratora", - "adminNotePlaceholder": "Notatka wewnętrzna (niewidoczna dla użytkownika)", - "saveNote": "Zapisz notatkę", - "noteSaved": "Notatka zapisana", - "actionAcknowledge": "Potwierdź", - "actionResolve": "Rozwiąż", - "actionArchive": "Archiwizuj", - "actionPublishGithub": "Opublikuj na GitHub", - "publishedToGithub": "Opublikowano na GitHub", - "viewIssue": "Zobacz zgłoszenie", - "publishSuccess": "Utworzono zgłoszenie", - "publishFailed": "Publikacja na GitHub nie powiodła się", - "updateFailed": "Aktualizacja nie powiodła się", - "archiveConfirm": "Zarchiwizować ten wpis opinii?", - "submittedBy": "Przesłane przez" - }, "shell": { "overview": "Przegląd", "sectionsNav": "Sekcje administracyjne" @@ -5390,10 +5336,6 @@ "title": "Coach", "subtitle": "Powierzchnie asystenta, klucz serwera oraz tabele jakości opinii Coacha i analiz w jednym miejscu." }, - "feedback": { - "title": "Opinie", - "subtitle": "Segreguj opinie użytkowników, zgłoszenia błędów i prośby o funkcje." - }, "reminders": { "title": "Przypomnienia", "subtitle": "Dostrój progi przypomnień o lekach i uruchom ręczne sprawdzenie." @@ -5583,10 +5525,6 @@ "title": "Stara szkoła", "description": "Zaloguj się co najmniej raz nazwą użytkownika i hasłem." }, - "bugReport1": { - "title": "Łowca błędów", - "description": "Wyślij zgłoszenie błędu." - }, "loginStreak7": { "title": "Seria logowań 7d", "description": "Zaloguj się przez 7 kolejnych dni." @@ -5775,10 +5713,6 @@ "title": "Poliglota", "description": "Zmieniłeś język aplikacji co najmniej raz." }, - "hiddenBugBuddy": { - "title": "Towarzysz błędów", - "description": "Wysłałeś co najmniej 5 zgłoszeń błędów — projekt Cię kocha." - }, "missFree7": { "title": "Bez pominiętej dawki · 7 dni", "description": "7 kolejnych dni bez pominiętej dawki." @@ -5831,26 +5765,6 @@ "empty": "Brak osiągnięć — zapisuj dalej, aby odblokować pierwsze." } }, - "bugreport": { - "title": "Zgłoszenie błędu", - "subtitle": "Wyślij zgłoszenie błędu, sugestię funkcji lub pytanie. Administrator serwera zobaczy twoje zgłoszenie.", - "loginRequired": "Zaloguj się, aby zgłosić błąd.", - "bugTitle": "Tytuł", - "description": "Opis", - "submit": "Wyślij", - "success": "Dziękujemy! Twoje zgłoszenie zostało wysłane.", - "errorCreating": "Błąd tworzenia", - "category": "Rodzaj", - "categoryBug": "Błąd", - "categoryFeature": "Propozycja funkcji", - "categoryQuestion": "Pytanie", - "categoryOther": "Inna opinia", - "bugTitlePlaceholder": "Krótkie podsumowanie (opcjonalnie)", - "githubEscalationNote": "Twoja opinia zapisuje się lokalnie. Jeśli administrator włączy integrację z GitHubem, może zostać opublikowana jako publiczne issue na życzenie.", - "internalOnlyNote": "Twoja opinia zapisuje się lokalnie i jest widoczna tylko dla administratora tego serwera.", - "disabledTitle": "Zgłoszenia błędów są wyłączone", - "disabledBody": "Administrator wyłączył zgłoszenia błędów na tym serwerze." - }, "notifications": { "title": "Powiadomienia", "eventColumn": "Zdarzenie", @@ -5955,7 +5869,8 @@ "eventMeasurementReminder": "Przypomnienie o pomiarze", "eventMeasurementReminderDesc": "Wysyłane, gdy śledzony pomiar jest wymagany.", "eventMedicationIntakeSync": "Synchronizacja przyjęć leku", - "eventMedicationIntakeSyncDesc": "Aktualizacja między urządzeniami, gdy dawka zostanie zapisana, przyjęta lub pominięta." + "eventMedicationIntakeSyncDesc": "Aktualizacja między urządzeniami, gdy dawka zostanie zapisana, przyjęta lub pominięta.", + "loginRequired": "Zaloguj się, aby zarządzać powiadomieniami." }, "i18n": { "maintainershipBanner": { diff --git a/prisma/migrations/0205_v123_drop_bugreport/migration.sql b/prisma/migrations/0205_v123_drop_bugreport/migration.sql new file mode 100644 index 000000000..00e4a2333 --- /dev/null +++ b/prisma/migrations/0205_v123_drop_bugreport/migration.sql @@ -0,0 +1,17 @@ +-- v1.23 — remove the bug-report integration. +-- +-- The in-app bug-report / feedback surface (user form, admin inbox, optional +-- GitHub-issue escalation) is retired. This migration drops the dedicated +-- feedback table, its enums, and the three AppSettings columns that held the +-- GitHub-issue credentials and the enable toggle. +-- +-- Destructive by design: the feature is gone, so its stored data goes with it. +DROP TABLE IF EXISTS "feedback"; + +DROP TYPE IF EXISTS "feedback_status"; +DROP TYPE IF EXISTS "feedback_category"; + +ALTER TABLE "app_settings" + DROP COLUMN IF EXISTS "github_issue_token_encrypted", + DROP COLUMN IF EXISTS "github_issue_repo", + DROP COLUMN IF EXISTS "bug_report_enabled"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bf517cb88..789ef9619 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -606,7 +606,6 @@ model User { pushSubscriptions PushSubscription[] dataBackups DataBackup[] achievements UserAchievement[] - feedback Feedback[] recommendationFeedback RecommendationFeedback[] idempotencyKeys IdempotencyKey[] devices Device[] @@ -3204,13 +3203,6 @@ model AppSettings { glitchtipEnabled Boolean @default(false) @map("glitchtip_enabled") glitchtipDsn String? @map("glitchtip_dsn") glitchtipEnvironment String? @map("glitchtip_environment") - githubIssueTokenEncrypted String? @map("github_issue_token_encrypted") - githubIssueRepo String? @map("github_issue_repo") - // v1.4.3: explicit toggle so an admin can hide the bug-report - // surface without nuking the token+repo config. Default true so - // existing instances with credentials don't accidentally lose - // the feature on upgrade. - bugReportEnabled Boolean @default(true) @map("bug_report_enabled") // Medication reminder thresholds (minutes after windowEnd) reminderLateMinutes Int @default(120) @map("reminder_late_minutes") @@ -3281,52 +3273,6 @@ model AppSettings { @@map("app_settings") } -// ─── Built-in Feedback ─────────────────────────────────────── - -enum FeedbackCategory { - BUG - FEATURE_REQUEST - QUESTION - OTHER - - @@map("feedback_category") -} - -enum FeedbackStatus { - OPEN - ACKNOWLEDGED - RESOLVED - ARCHIVED - - @@map("feedback_status") -} - -/// Stores user-submitted feedback without requiring external GitHub config. -/// Admin inbox lives at /admin#feedback; optional escalation publishes to a -/// GitHub issue reusing the existing integration when configured. -model Feedback { - id String @id @default(cuid()) - userId String? @map("user_id") // Null after user deletion; email snapshot remains. - email String? // Snapshot at submission time for triage continuity. - category FeedbackCategory @default(BUG) - subject String - description String - screenshotBase64 String? @map("screenshot_base64") // Optional, <5MB, PNG/JPG. - metadata Json? // URL, userAgent, locale, appVersion, … - status FeedbackStatus @default(OPEN) - adminNote String? @map("admin_note") - gitHubIssueUrl String? @map("github_issue_url") - notifiedEmail Boolean @default(false) @map("notified_email") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - - @@index([status, createdAt]) - @@index([userId]) - @@map("feedback") -} - // ─── Audit Log ───────────────────────────────────────────── model AuditLog { diff --git a/scripts/rotate-encryption-key.ts b/scripts/rotate-encryption-key.ts index 437ef7362..79e11d648 100644 --- a/scripts/rotate-encryption-key.ts +++ b/scripts/rotate-encryption-key.ts @@ -227,11 +227,9 @@ async function main() { // ───── AppSettings — operator credentials (String) ───── // Columns: "adminAiKeyEncrypted" "webPushVapidPrivateKeyEncrypted" - // "githubIssueTokenEncrypted" for (const field of [ "adminAiKeyEncrypted", "webPushVapidPrivateKeyEncrypted", - "githubIssueTokenEncrypted", ]) { results.push( await rotateStringColumn("AppSettings", field, prisma.appSettings), diff --git a/src/app/achievements/__tests__/page.test.tsx b/src/app/achievements/__tests__/page.test.tsx index 840ecadc5..93f3149c0 100644 --- a/src/app/achievements/__tests__/page.test.tsx +++ b/src/app/achievements/__tests__/page.test.tsx @@ -137,7 +137,6 @@ const mockData = { passkeyLoginCount: 0, passwordLoginCount: 0, loginDayStreak: 0, - bugReportCount: 0, moodEntryCount: 0, moodDayStreak: 0, moodImprovementHit: 0, diff --git a/src/app/admin/[section]/renderer.tsx b/src/app/admin/[section]/renderer.tsx index 4e87add9e..86d815bf3 100644 --- a/src/app/admin/[section]/renderer.tsx +++ b/src/app/admin/[section]/renderer.tsx @@ -25,7 +25,6 @@ import { ApiTokenOverviewSection } from "@/components/admin/api-token-overview-s import { AppLogPreviewSection } from "@/components/admin/app-log-preview-section"; import { BackupsSection } from "@/components/admin/backups-section"; import { DangerZoneSection } from "@/components/admin/danger-zone-section"; -import { FeedbackInboxSection } from "@/components/admin/feedback-inbox-section"; import { GeneralSettingsSection } from "@/components/admin/general-settings-section"; import { IntegrationsGroupSection } from "@/components/admin/integrations-group-section"; import { LoginOverviewSection } from "@/components/admin/login-overview-section"; @@ -120,15 +119,6 @@ export function AdminSectionRenderer({ ); - case "feedback": - return ( - - - - ); case "reminders": return ( ({ - prisma: { - feedback: { - findUnique: vi.fn(), - update: vi.fn(), - }, - }, -})); - -vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() })); -vi.mock("@/lib/auth/audit", () => ({ - auditLog: vi.fn().mockResolvedValue(undefined), -})); -vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() })); -vi.mock("@/lib/db-compat", () => ({ - ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), -})); -vi.mock("next/headers", () => ({ - headers: vi.fn(async () => ({ get: () => null })), - cookies: vi.fn(async () => ({ - get: () => undefined, - set: () => {}, - delete: () => {}, - })), -})); - -import { PATCH } from "../route"; -import { getSession } from "@/lib/auth/session"; - -const SESSION_ADMIN = { - session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, - user: { id: "admin-1", username: "admin", role: "ADMIN" as const }, -}; - -function patchReq(body: unknown): NextRequest { - return new NextRequest("http://localhost/api/admin/feedback/f1", { - method: "PATCH", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }); -} - -const ROUTE_CTX = { params: Promise.resolve({ id: "f1" }) }; - -beforeEach(() => { - vi.resetAllMocks(); - vi.mocked(getSession).mockResolvedValue(SESSION_ADMIN as never); -}); - -describe("PATCH /api/admin/feedback/[id] — 422 multi-issue (v1.4.43 W6)", () => { - it("surfaces TWO simultaneous validation errors", async () => { - // Bad `status` enum + adminNote too long. - const res = await PATCH( - patchReq({ status: "junk", adminNote: "x".repeat(10000) }), - ROUTE_CTX, - ); - expect(res.status).toBe(422); - const body = (await res.json()) as { - data: null; - error: string; - details: { - issues: Array<{ path: string; code: string; message: string }>; - }; - }; - expect(body.data).toBeNull(); - expect(body.error).toBe("Validation failed"); - expect(body.details.issues.length).toBeGreaterThanOrEqual(2); - for (const issue of body.details.issues) { - expect(Object.keys(issue).sort()).toEqual(["code", "message", "path"]); - } - }); - - it("surfaces multiple simultaneous validation errors (≥ 2)", async () => { - // The schema has only two knobs (status + adminNote), so the strict - // 3-issue ceiling isn't reachable. Multi-issue contract is pinned at - // ≥ 2 by the case above. - const res = await PATCH( - patchReq({ status: 999, adminNote: 123 }), - ROUTE_CTX, - ); - expect(res.status).toBe(422); - const body = (await res.json()) as { - details: { issues: Array }; - }; - expect(body.details.issues.length).toBeGreaterThanOrEqual(2); - }); -}); diff --git a/src/app/api/admin/feedback/[id]/github/route.ts b/src/app/api/admin/feedback/[id]/github/route.ts deleted file mode 100644 index bf4f6111b..000000000 --- a/src/app/api/admin/feedback/[id]/github/route.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * POST /api/admin/feedback/[id]/github — promote feedback to a GitHub Issue. - * - * Reuses publishFeedbackToGithub() so the issue body matches the legacy - * /api/bugreport flow. Idempotent: if gitHubIssueUrl is already set we return - * the existing URL instead of creating a duplicate. - */ -import { apiHandler, requireAdmin } from "@/lib/api-handler"; -import { apiError, apiSuccess, getClientIp } from "@/lib/api-response"; -import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; -import { prisma } from "@/lib/db"; -import { - publishFeedbackToGithub, - GithubPublishError, -} from "@/lib/feedback/publish-github"; -import type { NextRequest } from "next/server"; - -export const POST = apiHandler( - async (request: NextRequest, ctx: { params: Promise<{ id: string }> }) => { - const { user: admin } = await requireAdmin(); - const { id } = await ctx.params; - - const feedback = await prisma.feedback.findUnique({ - where: { id }, - select: { - id: true, - category: true, - subject: true, - description: true, - gitHubIssueUrl: true, - screenshotBase64: true, - metadata: true, - user: { select: { username: true } }, - }, - }); - if (!feedback) return apiError("Feedback not found", 404); - - if (feedback.gitHubIssueUrl) { - return apiError(`Already published: ${feedback.gitHubIssueUrl}`, 409); - } - - // Atomic claim to prevent concurrent admin clicks from creating two - // duplicate GitHub issues. Only the first caller whose updateMany - // matches (gitHubIssueUrl still null) proceeds. We write a PENDING - // sentinel now and replace it with the real URL after the API call. - const PENDING = "pending://creating"; - const claim = await prisma.feedback.updateMany({ - where: { id, gitHubIssueUrl: null }, - data: { gitHubIssueUrl: PENDING }, - }); - if (claim.count !== 1) { - return apiError("Already being published by another request", 409); - } - - try { - const result = await publishFeedbackToGithub({ - category: feedback.category, - subject: feedback.subject, - description: feedback.description, - username: feedback.user?.username ?? "anonymous", - metadata: feedback.metadata, - }); - - await prisma.feedback.update({ - where: { id }, - data: { gitHubIssueUrl: result.issueUrl }, - }); - - await auditLog("admin.feedback.github_publish", { - userId: admin.id, - ipAddress: getClientIp(request), - details: { feedbackId: id, issueNumber: result.issueNumber }, - }); - - annotate({ - action: { name: "admin.feedback.github_publish" }, - meta: { feedback_id: id, issue_number: result.issueNumber }, - }); - - return apiSuccess({ issueUrl: result.issueUrl }); - } catch (err) { - // Release the PENDING claim so a retry can succeed. - await prisma.feedback - .updateMany({ - where: { id, gitHubIssueUrl: PENDING }, - data: { gitHubIssueUrl: null }, - }) - .catch(() => undefined); - if (err instanceof GithubPublishError) { - return apiError(err.message, err.status); - } - throw err; - } - }, -); diff --git a/src/app/api/admin/feedback/[id]/route.ts b/src/app/api/admin/feedback/[id]/route.ts deleted file mode 100644 index ede8425f6..000000000 --- a/src/app/api/admin/feedback/[id]/route.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * PATCH /api/admin/feedback/[id] — change status or admin note. - * DELETE /api/admin/feedback/[id] — archive (soft: sets status to ARCHIVED). - */ -import { apiHandler, requireAdmin } from "@/lib/api-handler"; -import { - apiError, - apiSuccess, - returnAllZodIssues, - safeJson, - getClientIp, -} from "@/lib/api-response"; -import { annotate } from "@/lib/logging/context"; -import { auditLog } from "@/lib/auth/audit"; -import { prisma } from "@/lib/db"; -import { updateFeedbackSchema } from "@/lib/validations/feedback"; -import type { NextRequest } from "next/server"; - -export const PATCH = apiHandler( - async (request: NextRequest, ctx: { params: Promise<{ id: string }> }) => { - const { user } = await requireAdmin(); - const { id } = await ctx.params; - - const { data: body, error: jsonError } = await safeJson(request, { - maxBytes: 64 * 1024, - }); - if (jsonError) return jsonError; - - const parsed = updateFeedbackSchema.safeParse(body); - if (!parsed.success) { - // v1.4.43 W6 — multi-issue 422. - return returnAllZodIssues(parsed.error, 422); - } - - const before = await prisma.feedback.findUnique({ where: { id } }); - if (!before) return apiError("Feedback not found", 404); - - const updated = await prisma.feedback.update({ - where: { id }, - data: parsed.data, - select: { - id: true, - status: true, - adminNote: true, - updatedAt: true, - }, - }); - - await auditLog("admin.feedback.update", { - userId: user.id, - ipAddress: getClientIp(request), - details: { - feedbackId: id, - before: { status: before.status, adminNote: before.adminNote }, - after: parsed.data, - }, - }); - - annotate({ - action: { name: "admin.feedback.update" }, - meta: { feedback_id: id }, - }); - - return apiSuccess(updated); - }, -); - -export const DELETE = apiHandler( - async (request: NextRequest, ctx: { params: Promise<{ id: string }> }) => { - const { user } = await requireAdmin(); - const { id } = await ctx.params; - - const updated = await prisma.feedback.update({ - where: { id }, - data: { status: "ARCHIVED" }, - select: { id: true, status: true }, - }); - - await auditLog("admin.feedback.archive", { - userId: user.id, - ipAddress: getClientIp(request), - details: { feedbackId: id }, - }); - - return apiSuccess(updated); - }, -); diff --git a/src/app/api/admin/feedback/route.ts b/src/app/api/admin/feedback/route.ts deleted file mode 100644 index cf5f55f8a..000000000 --- a/src/app/api/admin/feedback/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Admin feedback inbox. Lists all feedback with status filter. - */ -import { apiHandler, requireAdmin } from "@/lib/api-handler"; -import { apiSuccess } from "@/lib/api-response"; -import { annotate } from "@/lib/logging/context"; -import { prisma } from "@/lib/db"; -import type { NextRequest } from "next/server"; -import type { FeedbackStatus } from "@/generated/prisma/client"; - -export const GET = apiHandler(async (request: NextRequest) => { - await requireAdmin(); - - const url = new URL(request.url); - const status = url.searchParams.get("status") as FeedbackStatus | null; - const limit = Math.min( - 100, - Math.max(1, Number(url.searchParams.get("limit") ?? 50)), - ); - const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0)); - - const where = status ? { status } : {}; - - const [items, total, counts] = await Promise.all([ - prisma.feedback.findMany({ - where, - orderBy: { createdAt: "desc" }, - take: limit, - skip: offset, - select: { - id: true, - userId: true, - email: true, - category: true, - subject: true, - description: true, - status: true, - adminNote: true, - gitHubIssueUrl: true, - metadata: true, - screenshotBase64: true, - createdAt: true, - updatedAt: true, - user: { select: { username: true } }, - }, - }), - prisma.feedback.count({ where }), - prisma.feedback.groupBy({ - by: ["status"], - _count: { _all: true }, - }), - ]); - - annotate({ - action: { name: "admin.feedback.list" }, - meta: { total, filtered_status: status }, - }); - - const countsByStatus = Object.fromEntries( - counts.map((c) => [c.status, c._count._all]), - ) as Record; - - return apiSuccess({ - items, - meta: { total, limit, offset, countsByStatus }, - }); -}); diff --git a/src/app/api/admin/settings/__tests__/route.test.ts b/src/app/api/admin/settings/__tests__/route.test.ts index 7c8dd66e8..599f02ab4 100644 --- a/src/app/api/admin/settings/__tests__/route.test.ts +++ b/src/app/api/admin/settings/__tests__/route.test.ts @@ -98,7 +98,6 @@ describe("GET /api/admin/settings", () => { expect(body.data.registrationEnabled).toBe(true); expect(body.data.defaultLocale).toBe("de"); expect(body.data.webPushVapidConfigured).toBe(false); - expect(body.data.bugReportConfigured).toBe(false); expect(body.data.glitchtipEnabled).toBe(false); }); @@ -120,8 +119,6 @@ describe("GET /api/admin/settings", () => { glitchtipEnabled: false, glitchtipDsn: null, glitchtipEnvironment: "production", - githubIssueRepo: "owner/repo", - githubIssueTokenEncrypted: "token-blob", reminderLateMinutes: 120, reminderMissedMinutes: 240, moodLogGlobal: true, @@ -129,15 +126,12 @@ describe("GET /api/admin/settings", () => { const res = await GET(); const body = (await res.json()) as { data: Record }; - // The two configured-flags must be true … + // The configured flag must be true … expect(body.data.webPushVapidConfigured).toBe(true); - expect(body.data.bugReportConfigured).toBe(true); // … but raw encrypted blobs must never reach the client. const serialized = JSON.stringify(body.data); expect(serialized).not.toContain("secret-blob"); - expect(serialized).not.toContain("token-blob"); expect(body.data).not.toHaveProperty("webPushVapidPrivateKeyEncrypted"); - expect(body.data).not.toHaveProperty("githubIssueTokenEncrypted"); }); }); @@ -185,8 +179,6 @@ describe("PUT /api/admin/settings", () => { glitchtipEnabled: false, glitchtipDsn: null, glitchtipEnvironment: "production", - githubIssueRepo: null, - githubIssueTokenEncrypted: null, reminderLateMinutes: 120, reminderMissedMinutes: 240, moodLogGlobal: true, @@ -227,8 +219,6 @@ describe("PUT /api/admin/settings", () => { glitchtipEnabled: false, glitchtipDsn: null, glitchtipEnvironment: null, - githubIssueRepo: null, - githubIssueTokenEncrypted: null, reminderLateMinutes: 120, reminderMissedMinutes: 240, moodLogGlobal: true, @@ -257,8 +247,6 @@ describe("PUT /api/admin/settings", () => { glitchtipEnabled: false, glitchtipDsn: null, glitchtipEnvironment: null, - githubIssueRepo: null, - githubIssueTokenEncrypted: null, reminderLateMinutes: 120, reminderMissedMinutes: 240, moodLogGlobal: true, @@ -292,8 +280,6 @@ describe("PUT /api/admin/settings", () => { glitchtipEnabled: true, glitchtipDsn: "https://abc@glitchtip.example.com/1", glitchtipEnvironment: "staging", - githubIssueRepo: "owner/repo", - githubIssueTokenEncrypted: "enc(gh-token)", reminderLateMinutes: 30, reminderMissedMinutes: 90, moodLogGlobal: false, @@ -317,8 +303,6 @@ describe("PUT /api/admin/settings", () => { umamiWebsiteId: "site-uuid", glitchtipDsn: "https://abc@glitchtip.example.com/1", glitchtipEnvironment: "staging", - bugReportRepo: "owner/repo", - bugReportToken: "gh-token", reminderLateMinutes: 30, reminderMissedMinutes: 90, }), @@ -336,8 +320,6 @@ describe("PUT /api/admin/settings", () => { umamiWebsiteId: "site-uuid", glitchtipDsn: "https://abc@glitchtip.example.com/1", glitchtipEnvironment: "staging", - githubIssueRepo: "owner/repo", - githubIssueTokenEncrypted: "enc(gh-token)", webPushVapidPublicKey: "vapid-pub", webPushVapidSubject: "mailto:ops@healthlog.dev", webPushVapidPrivateKeyEncrypted: "enc(secret)", @@ -345,7 +327,6 @@ describe("PUT /api/admin/settings", () => { reminderMissedMinutes: 90, }); expect(encrypt).toHaveBeenCalledWith("secret"); - expect(encrypt).toHaveBeenCalledWith("gh-token"); }); it("clears nullable string fields when an empty string is submitted", async () => { @@ -366,8 +347,6 @@ describe("PUT /api/admin/settings", () => { glitchtipEnabled: false, glitchtipDsn: null, glitchtipEnvironment: null, - githubIssueRepo: null, - githubIssueTokenEncrypted: null, reminderLateMinutes: 120, reminderMissedMinutes: 240, moodLogGlobal: true, @@ -381,7 +360,6 @@ describe("PUT /api/admin/settings", () => { umamiWebsiteId: "", glitchtipDsn: "", glitchtipEnvironment: "", - bugReportRepo: "", }), ); const upsert = vi.mocked(prisma.appSettings.upsert).mock.calls[0]?.[0]; @@ -392,39 +370,9 @@ describe("PUT /api/admin/settings", () => { umamiWebsiteId: null, glitchtipDsn: null, glitchtipEnvironment: null, - githubIssueRepo: null, }); }); - it("clears the encrypted bug-report token when clear flag is set", async () => { - vi.mocked(prisma.appSettings.upsert).mockResolvedValue({ - id: "singleton", - registrationEnabled: true, - defaultLocale: "en", - telegramGlobal: true, - ntfyGlobal: true, - webPushGlobal: true, - webPushVapidPublicKey: null, - webPushVapidSubject: null, - webPushVapidPrivateKeyEncrypted: null, - apiGlobal: true, - umamiEnabled: false, - umamiScriptUrl: null, - umamiWebsiteId: null, - glitchtipEnabled: false, - glitchtipDsn: null, - glitchtipEnvironment: null, - githubIssueRepo: null, - githubIssueTokenEncrypted: null, - reminderLateMinutes: 120, - reminderMissedMinutes: 240, - moodLogGlobal: true, - } as never); - await PUT(jsonReq({ clearBugReportToken: true })); - const upsert = vi.mocked(prisma.appSettings.upsert).mock.calls[0]?.[0]; - expect(upsert?.update).toEqual({ githubIssueTokenEncrypted: null }); - }); - it("returns 415 when content-type is not JSON", async () => { const r = new NextRequest("http://localhost/api/admin/settings", { method: "PUT", @@ -454,9 +402,6 @@ describe("PUT /api/admin/settings", () => { glitchtipEnabled: false, glitchtipDsn: null, glitchtipEnvironment: null, - githubIssueRepo: null, - githubIssueTokenEncrypted: null, - bugReportEnabled: true, reminderLateMinutes: 120, reminderMissedMinutes: 240, moodLogGlobal: true, @@ -486,9 +431,6 @@ describe("PUT /api/admin/settings", () => { glitchtipEnabled: false, glitchtipDsn: null, glitchtipEnvironment: null, - githubIssueRepo: null, - githubIssueTokenEncrypted: null, - bugReportEnabled: true, reminderLateMinutes: 120, reminderMissedMinutes: 240, moodLogGlobal: true, diff --git a/src/app/api/admin/settings/route.ts b/src/app/api/admin/settings/route.ts index ac276b313..2dc0acb19 100644 --- a/src/app/api/admin/settings/route.ts +++ b/src/app/api/admin/settings/route.ts @@ -16,7 +16,6 @@ import { invalidateServerDefaultTimezone, isValidTimezone, } from "@/lib/tz/resolver"; -import { invalidateAppSettings } from "@/lib/cache/invalidate"; export const dynamic = "force-dynamic"; @@ -48,11 +47,6 @@ export const GET = apiHandler(async () => { glitchtipEnabled: settings?.glitchtipEnabled ?? false, glitchtipDsn: settings?.glitchtipDsn ?? null, glitchtipEnvironment: settings?.glitchtipEnvironment ?? "production", - bugReportRepo: settings?.githubIssueRepo ?? null, - bugReportConfigured: Boolean( - settings?.githubIssueRepo && settings?.githubIssueTokenEncrypted, - ), - bugReportEnabled: settings?.bugReportEnabled ?? true, reminderLateMinutes: settings?.reminderLateMinutes ?? 120, reminderMissedMinutes: settings?.reminderMissedMinutes ?? 240, moodLogGlobal: settings?.moodLogGlobal ?? true, @@ -92,7 +86,6 @@ export const PUT = apiHandler(async (request: NextRequest) => { "umamiEnabled", "glitchtipEnabled", "moodLogGlobal", - "bugReportEnabled", ] as const; for (const field of booleanFields) { if (data[field] !== undefined) { @@ -171,24 +164,6 @@ export const PUT = apiHandler(async (request: NextRequest) => { auditDetails.glitchtipEnvironment = value || null; } - // Bug report - if (data.bugReportRepo !== undefined) { - const repo = data.bugReportRepo.trim(); - updates.githubIssueRepo = repo || null; - auditDetails.bugReportRepo = repo || null; - } - if (data.bugReportToken !== undefined) { - const token = data.bugReportToken.trim(); - if (token) { - updates.githubIssueTokenEncrypted = encrypt(token); - auditDetails.bugReportTokenUpdated = true; - } - } - if (data.clearBugReportToken === true) { - updates.githubIssueTokenEncrypted = null; - auditDetails.bugReportTokenUpdated = false; - } - // Numeric thresholds if (data.reminderLateMinutes !== undefined) { updates.reminderLateMinutes = data.reminderLateMinutes; @@ -233,11 +208,6 @@ export const PUT = apiHandler(async (request: NextRequest) => { invalidateServerDefaultTimezone(); } - // v1.4.34 IW-G — bust the bug-report status cache (global singleton) - // so the next read reflects the new GitHub-token / bug-report-enabled - // shape. Cheap call — the cache holds at most 10 entries. - invalidateAppSettings(); - await auditLog("admin.settings.update", { userId: user.id, ipAddress: getClientIp(request), @@ -264,11 +234,6 @@ export const PUT = apiHandler(async (request: NextRequest) => { glitchtipEnabled: settings.glitchtipEnabled, glitchtipDsn: settings.glitchtipDsn, glitchtipEnvironment: settings.glitchtipEnvironment ?? "production", - bugReportRepo: settings.githubIssueRepo, - bugReportConfigured: Boolean( - settings.githubIssueRepo && settings.githubIssueTokenEncrypted, - ), - bugReportEnabled: settings.bugReportEnabled, reminderLateMinutes: settings.reminderLateMinutes, reminderMissedMinutes: settings.reminderMissedMinutes, moodLogGlobal: settings.moodLogGlobal, diff --git a/src/app/api/admin/settings/web-push-vapid/generate/route.ts b/src/app/api/admin/settings/web-push-vapid/generate/route.ts index 2e7556dcd..db647d982 100644 --- a/src/app/api/admin/settings/web-push-vapid/generate/route.ts +++ b/src/app/api/admin/settings/web-push-vapid/generate/route.ts @@ -9,7 +9,6 @@ import { } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; import { encrypt } from "@/lib/crypto"; -import { invalidateAppSettings } from "@/lib/cache/invalidate"; import { z } from "zod/v4"; import { NextRequest } from "next/server"; @@ -114,8 +113,6 @@ export const POST = apiHandler(async (request: NextRequest) => { }, }); - invalidateAppSettings(); - await auditLog("admin.web_push_vapid.generate", { userId: user.id, ipAddress: getClientIp(request), diff --git a/src/app/api/admin/status/__tests__/route.test.ts b/src/app/api/admin/status/__tests__/route.test.ts index 7bcf4b8f2..3406c07a1 100644 --- a/src/app/api/admin/status/__tests__/route.test.ts +++ b/src/app/api/admin/status/__tests__/route.test.ts @@ -89,11 +89,10 @@ describe("GET /api/admin/status (integration health summary)", () => { umami: null, glitchtip: null, webPush: null, - bugReport: null, }); }); - it("aggregates configured + enabled flags for umami / glitchtip / web-push / bugReport", async () => { + it("aggregates configured + enabled flags for umami / glitchtip / web-push", async () => { vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({ umamiScriptUrl: "https://x/script.js", umamiWebsiteId: "site-1", @@ -103,8 +102,6 @@ describe("GET /api/admin/status (integration health summary)", () => { webPushVapidPublicKey: "pub", webPushVapidPrivateKeyEncrypted: "secret-blob", webPushVapidSubject: "mailto:a@b.com", - githubIssueRepo: "owner/repo", - githubIssueTokenEncrypted: "token-blob", } as never); const res = await GET(); @@ -121,7 +118,6 @@ describe("GET /api/admin/status (integration health summary)", () => { enabled: false, }); expect(body.data.integrations.webPush).toEqual({ configured: true }); - expect(body.data.integrations.bugReport).toEqual({ configured: true }); // Regression: encrypted blobs MUST never appear in the response envelope. const serialized = JSON.stringify(body); diff --git a/src/app/api/admin/status/route.ts b/src/app/api/admin/status/route.ts index 02389f0e9..15ae3331b 100644 --- a/src/app/api/admin/status/route.ts +++ b/src/app/api/admin/status/route.ts @@ -56,10 +56,6 @@ export const GET = apiHandler(async () => { appSettings?.webPushVapidPrivateKeyEncrypted && appSettings?.webPushVapidSubject, ); - const bugReportConfigured = Boolean( - appSettings?.githubIssueRepo && appSettings?.githubIssueTokenEncrypted, - ); - const workerStatus = getWorkerStatus(); return apiSuccess({ @@ -89,7 +85,6 @@ export const GET = apiHandler(async () => { } : null, webPush: webPushConfigured ? { configured: true } : null, - bugReport: bugReportConfigured ? { configured: true } : null, }, }); }); diff --git a/src/app/api/bugreport/__tests__/route.test.ts b/src/app/api/bugreport/__tests__/route.test.ts deleted file mode 100644 index f1ef98855..000000000 --- a/src/app/api/bugreport/__tests__/route.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * v1.4.43 W6 — multi-issue 422 envelope on POST /api/bugreport. - */ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { NextRequest } from "next/server"; - -vi.mock("@/lib/db", () => ({ - prisma: { - appSettings: { - findUnique: vi.fn(), - }, - }, -})); - -vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() })); -vi.mock("@/lib/auth/audit", () => ({ - auditLog: vi.fn().mockResolvedValue(undefined), -})); -vi.mock("@/lib/rate-limit", () => ({ - checkRateLimit: vi.fn(), -})); -vi.mock("@/lib/crypto", () => ({ - decrypt: (s: string) => s.replace(/^enc-/, ""), -})); -vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() })); -vi.mock("@/lib/db-compat", () => ({ - ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), -})); -vi.mock("next/headers", () => ({ - headers: vi.fn(async () => ({ get: () => null })), - cookies: vi.fn(async () => ({ - get: () => undefined, - set: () => {}, - delete: () => {}, - })), -})); - -import { POST } from "../route"; -import { prisma } from "@/lib/db"; -import { getSession } from "@/lib/auth/session"; -import { checkRateLimit } from "@/lib/rate-limit"; - -const SESSION_OK = { - session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, - user: { id: "user-1", username: "tester", role: "USER" as const }, -}; - -function postReq(body: unknown): NextRequest { - return new NextRequest("http://localhost/api/bugreport", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }); -} - -beforeEach(() => { - vi.resetAllMocks(); - vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); - vi.mocked(checkRateLimit).mockResolvedValue({ allowed: true } as never); - vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({ - githubIssueTokenEncrypted: "enc-token", - githubIssueRepo: "owner/repo", - bugReportEnabled: true, - } as never); -}); - -describe("POST /api/bugreport — 422 multi-issue (v1.4.43 W6)", () => { - it("surfaces TWO simultaneous validation errors", async () => { - // description too short + screenshot doesn't match data-URL regex. - const res = await POST( - postReq({ description: "tiny", screenshot: "not-a-data-url" }), - ); - expect(res.status).toBe(422); - const body = (await res.json()) as { - data: null; - error: string; - details: { - issues: Array<{ path: string; code: string; message: string }>; - }; - }; - expect(body.data).toBeNull(); - expect(body.error).toBe("Validation failed"); - expect(body.details.issues.length).toBeGreaterThanOrEqual(2); - for (const issue of body.details.issues) { - expect(Object.keys(issue).sort()).toEqual(["code", "message", "path"]); - } - }); - - it("surfaces multiple simultaneous validation errors (≥ 2)", async () => { - // Schema has only two knobs (description, screenshot). Pin contract - // on ≥ 2. - const res = await POST(postReq({ description: 123, screenshot: 999 })); - expect(res.status).toBe(422); - const body = (await res.json()) as { - details: { issues: Array }; - }; - expect(body.details.issues.length).toBeGreaterThanOrEqual(2); - }); -}); diff --git a/src/app/api/bugreport/route.ts b/src/app/api/bugreport/route.ts deleted file mode 100644 index 9f2c24fa2..000000000 --- a/src/app/api/bugreport/route.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { apiHandler, requireAuth } from "@/lib/api-handler"; -import { annotate, getEvent } from "@/lib/logging/context"; -import { - apiSuccess, - apiError, - getClientIp, - returnAllZodIssues, - safeJson, -} from "@/lib/api-response"; -import { checkRateLimit } from "@/lib/rate-limit"; -import { auditLog } from "@/lib/auth/audit"; -import { prisma } from "@/lib/db"; -import { decrypt } from "@/lib/crypto"; -import { safeFetch } from "@/lib/safe-fetch"; -import { NextRequest } from "next/server"; -import { z } from "zod/v4"; - -// Strict image/* data URL (no SVG — it can carry inline JS). -const SCREENSHOT_DATA_URL = - /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/=]+$/; - -const bugReportSchema = z.object({ - description: z.string().min(10, "Description too short").max(5000), - screenshot: z - .string() - .max(7_000_000, "Screenshot too large (max 5 MB)") - .regex( - SCREENSHOT_DATA_URL, - "Screenshot must be a base64-encoded PNG/JPEG/WEBP/GIF data URL", - ) - .optional(), -}); - -export const POST = apiHandler(async (request: NextRequest) => { - const { user } = await requireAuth(); - - const rl = await checkRateLimit(`bugreport:${user.id}`, 3, 60 * 60 * 1000); - if (!rl.allowed) { - return apiError("Maximum 3 bug reports per hour", 429); - } - - annotate({ action: { name: "bugreport.submit" } }); - - const appSettings = await prisma.appSettings.findUnique({ - where: { id: "singleton" }, - select: { - githubIssueTokenEncrypted: true, - githubIssueRepo: true, - bugReportEnabled: true, - }, - }); - - // v1.4.3: explicit toggle. When the admin disables bug reports we - // return 503 immediately — the client already renders a "not - // configured" UI for that status, so the report button gracefully - // disappears without needing additional client-side wiring. - if (appSettings && appSettings.bugReportEnabled === false) { - return apiError("Bug reports are disabled by the administrator", 503); - } - - let configuredToken: string | null = null; - if (appSettings?.githubIssueTokenEncrypted) { - try { - configuredToken = decrypt(appSettings.githubIssueTokenEncrypted); - } catch { - getEvent()?.addWarning("Failed to decrypt GitHub issue token"); - } - } - - const ghToken = configuredToken || process.env.GITHUB_ISSUE_TOKEN; - const ghRepo = appSettings?.githubIssueRepo || process.env.GITHUB_ISSUE_REPO; // e.g. "owner/repo" - - if (!ghToken || !ghRepo) { - // 503 instead of 500: the client keys off status to render the "not - // configured" screen. The user also sees a clear message rather than - // "Internal server error". - return apiError( - "Bug report not configured (GitHub issue token/repository missing in admin settings)", - 503, - ); - } - - const { data: body, error: jsonError } = await safeJson(request, { - maxBytes: 8 * 1024 * 1024, - }); - - if (jsonError) return jsonError; - const parsed = bugReportSchema.safeParse(body); - if (!parsed.success) { - // v1.4.43 W6 — multi-issue 422. - return returnAllZodIssues(parsed.error, 422); - } - - const { description, screenshot } = parsed.data; - - // GitHub issues are maintainer-facing and the repo's primary language is English. - // Use an ISO-8601 timestamp so titles are unambiguous and sortable. - const now = new Date(); - const dateStr = now.toISOString().replace("T", " ").slice(0, 16) + " UTC"; - const title = `Bug Report – ${dateStr}`; - - // Sanitize user-provided content for GitHub markdown. Strip HTML tags - // entirely (prevents `` etc.) and neutralise markdown link - // targets that start with dangerous schemes like javascript: or data:. - const safeUsername = user.username.replace(/[`*_~<>\[\]|]/g, ""); - const safeDescription = description - .replace(/<[^>]*>/g, "") // strip HTML tags - .replace(/\]\(\s*(javascript|data|vbscript|file):/gi, "](about:blank#") // neutralise dangerous markdown link schemes - .replace(/```/g, "\\`\\`\\`") - .slice(0, 5000); - - // Wrap the user's description in a fenced code block so any residual - // markdown renders literally rather than as executable markup. - let issueBody = `**Reported by:** ${safeUsername}\n`; - issueBody += `**Date:** ${dateStr}\n\n`; - issueBody += `## Description\n\n\`\`\`\n${safeDescription}\n\`\`\`\n`; - issueBody += `\n---\n*Created via HealthLog bug report*`; - - // If screenshot provided, upload to GitHub as a gist or include as base64 - // For simplicity, we'll note there's a screenshot but GitHub issues don't support inline base64 - if (screenshot) { - issueBody += `\n\n**Screenshot:** A screenshot was attached (see comment).`; - } - - // Create the issue - const issueRes = await safeFetch( - `https://api.github.com/repos/${ghRepo}/issues`, - { - method: "POST", - headers: { - Authorization: `Bearer ${ghToken}`, - "Content-Type": "application/json", - Accept: "application/vnd.github.v3+json", - }, - body: JSON.stringify({ - title: `[Bug] ${title}`, - body: issueBody, - labels: ["bug", "user-reported"], - }), - }, - ); - - if (!issueRes.ok) { - const errBody = await issueRes.text(); - getEvent()?.addWarning("GitHub issue creation failed: " + errBody); - return apiError("Failed to create issue", 500); - } - - const issue = (await issueRes.json()) as { - number: number; - html_url: string; - }; - - // If screenshot, upload as a comment - if (screenshot) { - // Extract just the base64 part - const base64Data = screenshot.replace(/^data:image\/\w+;base64,/, ""); - const ext = screenshot.startsWith("data:image/png") ? "png" : "jpg"; - - // Upload screenshot as issue comment with embedded image - // GitHub doesn't support base64 in comments, so we note the limitation - const commentBody = `### Screenshot\n\n![Screenshot](data:image/${ext};base64,${base64Data.slice(0, 100)}...)\n\n*Note: The full screenshot was truncated due to size limits. Please contact the user directly.*`; - - await safeFetch( - `https://api.github.com/repos/${ghRepo}/issues/${issue.number}/comments`, - { - method: "POST", - headers: { - Authorization: `Bearer ${ghToken}`, - "Content-Type": "application/json", - Accept: "application/vnd.github.v3+json", - }, - body: JSON.stringify({ body: commentBody }), - }, - ); - } - - await auditLog("bugreport.submit", { - userId: user.id, - ipAddress: getClientIp(request), - details: { - issueNumber: issue.number, - hasScreenshot: Boolean(screenshot), - }, - }); - - annotate({ meta: { issue_number: issue.number } }); - - return apiSuccess({ - issueNumber: issue.number, - issueUrl: issue.html_url, - }); -}); diff --git a/src/app/api/bugreport/status/route.ts b/src/app/api/bugreport/status/route.ts deleted file mode 100644 index c54ce11b0..000000000 --- a/src/app/api/bugreport/status/route.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Returns whether bug report submission is configured (GitHub token + repo). - * - * The bug report page uses this to gate its UI so users don't hit a silent - * 500 when the admin hasn't set up the GitHub integration. Only two booleans - * escape: a single `configured` flag and `isAdmin` so the page can render - * the admin-specific copy + admin link. Token/repo granularity is kept - * server-side (log annotation only) so we don't hand regular users a probe. - */ -import { apiHandler, requireAuth } from "@/lib/api-handler"; -import { apiSuccess } from "@/lib/api-response"; -import { annotate } from "@/lib/logging/context"; -import { prisma } from "@/lib/db"; -import { cached, caches, type ServerCache } from "@/lib/cache/server-cache"; - -interface BugreportStatusCacheValue { - configured: boolean; - enabled: boolean; - hasToken: boolean; - hasRepo: boolean; -} - -async function buildBugreportStatusValue(): Promise { - const appSettings = await prisma.appSettings.findUnique({ - where: { id: "singleton" }, - select: { - githubIssueTokenEncrypted: true, - githubIssueRepo: true, - bugReportEnabled: true, - }, - }); - - const hasToken = Boolean( - appSettings?.githubIssueTokenEncrypted || process.env.GITHUB_ISSUE_TOKEN, - ); - const hasRepo = Boolean( - appSettings?.githubIssueRepo || process.env.GITHUB_ISSUE_REPO, - ); - const configured = hasToken && hasRepo; - // Default ON when the column has never been written. - const enabled = appSettings?.bugReportEnabled !== false; - - return { configured, enabled, hasToken, hasRepo }; -} - -export const GET = apiHandler(async () => { - const { user } = await requireAuth(); - - // No per-request rate limit: the payload rides the singleton server - // cache below, so a cache hit costs zero Postgres round-trips — while - // the former rate-limit bucket was an unconditional Postgres UPSERT - // per request, i.e. the limiter WAS the per-request DB load it claimed - // to prevent. The route stays authenticated; the only uncached work is - // one `appSettings` read per 10 min (the bucket's TTL) process-wide. - - // Cache the global app-settings shape on a singleton key (per blueprint §3). - // `isAdmin` lives outside the cache because it varies per request. The - // `invalidateAppSettings()` helper called from - // `PATCH /api/admin/app-settings` evicts this entry when bug-report - // configuration changes. - const status = await cached( - caches.bugreportStatus as ServerCache, - "singleton", - () => buildBugreportStatusValue(), - annotate, - ); - - annotate({ - action: { name: "bugreport.status" }, - meta: { - configured: status.configured, - enabled: status.enabled, - has_token: status.hasToken, - has_repo: status.hasRepo, - }, - }); - - return apiSuccess({ - configured: status.configured, - enabled: status.enabled, - isAdmin: user.role === "ADMIN", - }); -}); diff --git a/src/app/api/feedback/__tests__/route.test.ts b/src/app/api/feedback/__tests__/route.test.ts deleted file mode 100644 index 080359d80..000000000 --- a/src/app/api/feedback/__tests__/route.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -vi.mock("@/lib/api-handler", () => ({ - apiHandler: unknown>(fn: T) => fn, - requireAuth: vi.fn(async () => ({ - user: { id: "u-1", email: "test@example.com", role: "USER" }, - session: { id: "s-1" }, - })), -})); - -vi.mock("@/lib/db", () => ({ - prisma: { - appSettings: { findUnique: vi.fn() }, - feedback: { create: vi.fn() }, - }, -})); - -vi.mock("@/lib/rate-limit", () => ({ - checkRateLimit: vi.fn(async () => ({ allowed: true })), -})); - -vi.mock("@/lib/logging/context", () => ({ - annotate: vi.fn(), -})); - -vi.mock("@/lib/auth/audit", () => ({ - auditLog: vi.fn(async () => undefined), -})); - -import { POST } from "../route"; -import { prisma } from "@/lib/db"; - -beforeEach(() => { - vi.resetAllMocks(); -}); - -function jsonRequest(body: unknown): Request { - return new Request("http://localhost/api/feedback", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }); -} - -describe("POST /api/feedback bug-report toggle gate", () => { - it("returns 503 when bugReportEnabled is false", async () => { - vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({ - bugReportEnabled: false, - } as never); - - const res = await POST( - jsonRequest({ - category: "BUG", - subject: "test", - description: "Anything at least ten characters long.", - }) as never, - ); - expect(res.status).toBe(503); - const body = (await res.json()) as { error: string }; - expect(body.error).toMatch(/disabled/i); - expect(prisma.feedback.create).not.toHaveBeenCalled(); - }); - - it("creates feedback when bugReportEnabled is true", async () => { - vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({ - bugReportEnabled: true, - } as never); - vi.mocked(prisma.feedback.create).mockResolvedValue({ - id: "fb-1", - createdAt: new Date(), - category: "BUG", - status: "NEW", - } as never); - - const res = await POST( - jsonRequest({ - category: "BUG", - subject: "test", - description: "Anything at least ten characters long.", - }) as never, - ); - expect(res.status).toBe(201); - expect(prisma.feedback.create).toHaveBeenCalledTimes(1); - }); - - it("creates feedback when bugReportEnabled has never been written (default ON)", async () => { - vi.mocked(prisma.appSettings.findUnique).mockResolvedValue(null); - vi.mocked(prisma.feedback.create).mockResolvedValue({ - id: "fb-2", - createdAt: new Date(), - category: "BUG", - status: "NEW", - } as never); - - const res = await POST( - jsonRequest({ - category: "BUG", - subject: "test", - description: "Anything at least ten characters long.", - }) as never, - ); - expect(res.status).toBe(201); - expect(prisma.feedback.create).toHaveBeenCalledTimes(1); - }); -}); - -describe("POST /api/feedback — 422 multi-issue (v1.4.43 W6)", () => { - it("surfaces TWO simultaneous validation errors", async () => { - vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({ - bugReportEnabled: true, - } as never); - // subject too short + description too short. - const res = await POST( - jsonRequest({ - category: "BUG", - subject: "x", - description: "short", - }) as never, - ); - expect(res.status).toBe(422); - const body = (await res.json()) as { - data: null; - error: string; - details: { - issues: Array<{ path: string; code: string; message: string }>; - }; - }; - expect(body.data).toBeNull(); - expect(body.error).toBe("Validation failed"); - expect(body.details.issues.length).toBeGreaterThanOrEqual(2); - for (const issue of body.details.issues) { - expect(Object.keys(issue).sort()).toEqual(["code", "message", "path"]); - } - }); - - it("surfaces THREE simultaneous validation errors", async () => { - vi.mocked(prisma.appSettings.findUnique).mockResolvedValue({ - bugReportEnabled: true, - } as never); - // bad subject + bad description + bad screenshot. - const res = await POST( - jsonRequest({ - category: "BUG", - subject: "x", - description: "short", - screenshot: "not-a-data-url", - }) as never, - ); - expect(res.status).toBe(422); - const body = (await res.json()) as { - details: { issues: Array }; - }; - expect(body.details.issues.length).toBeGreaterThanOrEqual(3); - }); -}); diff --git a/src/app/api/feedback/route.ts b/src/app/api/feedback/route.ts deleted file mode 100644 index 2547bb3af..000000000 --- a/src/app/api/feedback/route.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * POST /api/feedback — user submits feedback. Always works, no GitHub needed. - * GET /api/feedback — user's own feedback history (paginated). - * - * Rate-limited at 5 per hour per user. Screenshots are stored as base64 in - * the database (≤5 MB each). - */ -import { apiHandler, requireAuth } from "@/lib/api-handler"; -import { - apiError, - apiSuccess, - returnAllZodIssues, - safeJson, - getClientIp, -} from "@/lib/api-response"; -import { annotate } from "@/lib/logging/context"; -import { checkRateLimit } from "@/lib/rate-limit"; -import { auditLog } from "@/lib/auth/audit"; -import { prisma } from "@/lib/db"; -import { createFeedbackSchema } from "@/lib/validations/feedback"; -import { Prisma } from "@/generated/prisma/client"; -import type { NextRequest } from "next/server"; - -export const POST = apiHandler(async (request: NextRequest) => { - const { user } = await requireAuth(); - - const rl = await checkRateLimit(`feedback:${user.id}`, 5, 60 * 60 * 1000); - if (!rl.allowed) { - return apiError("Too many submissions — please try again later", 429); - } - - // The admin "Bug reports" toggle gates *every* feedback submission, not - // just the GitHub-promotion path. Previously the gate only lived in the - // legacy `/api/bugreport` route which the form does not even call — - // so flipping the toggle off had no effect on user-visible behaviour. - const settings = await prisma.appSettings.findUnique({ - where: { id: "singleton" }, - select: { bugReportEnabled: true }, - }); - if (settings && settings.bugReportEnabled === false) { - return apiError("Bug reports are disabled by the administrator", 503); - } - - const { data: body, error: jsonError } = await safeJson(request, { - maxBytes: 64 * 1024, - }); - if (jsonError) return jsonError; - - const parsed = createFeedbackSchema.safeParse(body); - if (!parsed.success) { - // v1.4.43 W6 — multi-issue 422. - return returnAllZodIssues(parsed.error, 422); - } - - const feedback = await prisma.feedback.create({ - data: { - userId: user.id, - email: user.email ?? null, - category: parsed.data.category, - subject: parsed.data.subject, - description: parsed.data.description, - screenshotBase64: parsed.data.screenshot ?? null, - metadata: - parsed.data.metadata !== undefined - ? (parsed.data.metadata as Prisma.InputJsonValue) - : Prisma.JsonNull, - }, - select: { id: true, createdAt: true, category: true, status: true }, - }); - - await auditLog("feedback.submit", { - userId: user.id, - ipAddress: getClientIp(request), - details: { - feedbackId: feedback.id, - category: feedback.category, - hasScreenshot: Boolean(parsed.data.screenshot), - }, - }); - - annotate({ - action: { name: "feedback.submit" }, - meta: { - feedback_id: feedback.id, - category: feedback.category, - }, - }); - - return apiSuccess(feedback, 201); -}); - -export const GET = apiHandler(async (request: NextRequest) => { - const { user } = await requireAuth(); - - const url = new URL(request.url); - const limit = Math.min( - 50, - Math.max(1, Number(url.searchParams.get("limit") ?? 25)), - ); - const offset = Math.max(0, Number(url.searchParams.get("offset") ?? 0)); - - const [items, total] = await Promise.all([ - prisma.feedback.findMany({ - where: { userId: user.id }, - orderBy: { createdAt: "desc" }, - take: limit, - skip: offset, - select: { - id: true, - category: true, - subject: true, - status: true, - adminNote: true, - gitHubIssueUrl: true, - createdAt: true, - updatedAt: true, - }, - }), - prisma.feedback.count({ where: { userId: user.id } }), - ]); - - return apiSuccess({ items, meta: { total, limit, offset } }); -}); diff --git a/src/app/api/gamification/achievements/__tests__/hidden-redaction.test.ts b/src/app/api/gamification/achievements/__tests__/hidden-redaction.test.ts index 0504280dd..deb5464b0 100644 --- a/src/app/api/gamification/achievements/__tests__/hidden-redaction.test.ts +++ b/src/app/api/gamification/achievements/__tests__/hidden-redaction.test.ts @@ -190,7 +190,7 @@ describe("GET /api/gamification/achievements — hidden achievement redaction", for (const entry of hiddenLocked) { // Trigger semantics scrubbed expect(entry.metric ?? "").not.toMatch( - /(nightOwl|earlyBird|leapDay|doctorPdf|localeFlip|bugReport)Count/, + /(nightOwl|earlyBird|leapDay|doctorPdf|localeFlip)Count/, ); // i18n keys scrubbed expect(entry.titleKey ?? "").not.toContain("hiddenNightOwl"); diff --git a/src/app/api/settings/account/route.ts b/src/app/api/settings/account/route.ts index d5f0d3da3..28ac74637 100644 --- a/src/app/api/settings/account/route.ts +++ b/src/app/api/settings/account/route.ts @@ -57,12 +57,11 @@ export const DELETE = apiHandler(async (request: NextRequest) => { // Destroy all sessions first await destroyAllSessions(userId); - // Audit V3 NEW-V3-2 / GDPR Art. 17 fix: Feedback and AuditLog rows have - // `onDelete: SetNull` in the schema, which keeps PII (free-text symptom - // descriptions, IP addresses, login city geo) attached to the record after - // the user is deleted. We explicitly purge them inside the same logical - // operation so account deletion is genuinely complete erasure. - await prisma.feedback.deleteMany({ where: { userId } }); + // Audit V3 NEW-V3-2 / GDPR Art. 17 fix: AuditLog rows have + // `onDelete: SetNull` in the schema, which keeps PII (IP addresses, login + // city geo) attached to the record after the user is deleted. We explicitly + // purge them inside the same logical operation so account deletion is + // genuinely complete erasure. await prisma.auditLog.deleteMany({ where: { userId } }); // Delete user — all other related data is removed via onDelete: Cascade diff --git a/src/app/bugreport/page.tsx b/src/app/bugreport/page.tsx deleted file mode 100644 index ff1898f4a..000000000 --- a/src/app/bugreport/page.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { useAuth } from "@/hooks/use-auth"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Loader2, CheckCircle2, Bug, GitPullRequest, Info } from "lucide-react"; -import { useTranslations } from "@/lib/i18n/context"; -import { queryKeys } from "@/lib/query-keys"; -import { ApiError, apiGet, apiPost } from "@/lib/api/api-fetch"; - -interface BugReportStatus { - configured: boolean; - enabled: boolean; - isAdmin: boolean; -} - -const CATEGORIES = [ - { value: "BUG", labelKey: "bugreport.categoryBug" }, - { value: "FEATURE_REQUEST", labelKey: "bugreport.categoryFeature" }, - { value: "QUESTION", labelKey: "bugreport.categoryQuestion" }, - { value: "OTHER", labelKey: "bugreport.categoryOther" }, -] as const; - -type CategoryValue = (typeof CATEGORIES)[number]["value"]; - -export default function BugReportPage() { - const { isAuthenticated } = useAuth(); - const { t, locale } = useTranslations(); - const [category, setCategory] = useState("BUG"); - const [subject, setSubject] = useState(""); - const [description, setDescription] = useState(""); - const [loading, setLoading] = useState(false); - const [result, setResult] = useState<{ - type: "success" | "error"; - message: string; - } | null>(null); - - // Status is now informational only — submission always works. - const { data: status } = useQuery({ - queryKey: queryKeys.bugreportStatus(), - queryFn: async () => { - return apiGet("/api/bugreport/status"); - }, - enabled: isAuthenticated, - }); - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setResult(null); - setLoading(true); - - try { - await apiPost("/api/feedback", { - category, - subject: subject || description.slice(0, 60), - description, - metadata: { - locale, - userAgent: - typeof navigator !== "undefined" ? navigator.userAgent : null, - url: typeof window !== "undefined" ? window.location.pathname : null, - }, - }); - setResult({ type: "success", message: t("bugreport.success") }); - setSubject(""); - setDescription(""); - } catch (err) { - setResult({ - type: "error", - message: - err instanceof ApiError - ? err.message || t("bugreport.errorCreating") - : t("common.networkError"), - }); - } finally { - setLoading(false); - } - } - - if (!isAuthenticated) { - return ( -
-

- {t("bugreport.title")} -

-

- {t("bugreport.loginRequired")} -

-
- ); - } - - if (status && status.enabled === false) { - return ( -
-
-

- {t("bugreport.title")} -

-

- {t("bugreport.subtitle")} -

-
-
- -
-

- {t("bugreport.disabledTitle")} -

-

- {t("bugreport.disabledBody")} -

-
-
-
- ); - } - - return ( -
-
-

- {t("bugreport.title")} -

-

- {t("bugreport.subtitle")} -

-
- - {status?.configured && ( -
- -

- {t("bugreport.githubEscalationNote")} -

-
- )} - {!status?.configured && ( -
- -

- {t("bugreport.internalOnlyNote")} -

-
- )} - -
-
-
-
- - -
-
- - setSubject(e.target.value)} - placeholder={t("bugreport.bugTitlePlaceholder")} - maxLength={200} - /> -
-
- -
- -