feat(feedback): in-app user feedback with admin review and account roles
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 47s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
User-facing Feedback screen (Settings -> Info, registered accounts only): a message (<=1024 runes) plus one optional attachment, an anti-spam gate (one unreviewed message at a time), and the operator's inline reply with a Settings/Info badge. Server-rendered admin console section (/_gm/feedback): unread/read/archived queue with per-user search, detail with read/reply/ archive/delete/delete-all, safe attachment serving (nosniff, images inline via <img>, others download-only). Introduces account_roles, the first per-account role table; feedback_banned blocks only feedback submission, granted/revoked from /users and the delete-with-block action. - migration 00004_feedback (feedback_messages + account_roles) + jetgen - backend internal/feedback (store+service), internal/account/roles.go - wire: FlatBuffers feedback.submit/get/unread; gateway guest gate (Op.NonGuest, is_guest via session resolve) -> guest_forbidden before any backend call - reply push reuses NotificationEvent with a new admin_reply sub-kind - UI: /feedback route + screen, attachment picker, badge, channel detection, i18n - tests: feedback unit (Go+UI), gateway guest-gate, inttest lifecycle, e2e - docs: PLAN stage 19, ARCHITECTURE s15, FUNCTIONAL(+ru), TESTING, READMEs
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import Screen from '../components/Screen.svelte';
|
||||
import TabBar from '../components/TabBar.svelte';
|
||||
import { app, handleError } from '../lib/app.svelte';
|
||||
import { app, handleError, refreshFeedbackBadge } from '../lib/app.svelte';
|
||||
import { connection } from '../lib/connection.svelte';
|
||||
import { gateway } from '../lib/gateway';
|
||||
import { navigate } from '../lib/router.svelte';
|
||||
@@ -18,15 +18,19 @@
|
||||
let incoming = $state<AccountRef[]>([]);
|
||||
|
||||
const guest = $derived(app.profile?.isGuest ?? true);
|
||||
// The lobby ⚙️ badge is the shared count: incoming friend requests plus an awaiting
|
||||
// operator feedback reply (the Settings → Info badge folds in here).
|
||||
const settingsBadge = $derived(app.notifications + (app.feedbackReplyUnread ? 1 : 0));
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
games = (await gateway.gamesList()).games;
|
||||
if (!guest) {
|
||||
[invitations, incoming] = await Promise.all([gateway.invitationsList(), gateway.friendsIncoming()]);
|
||||
// The ⚙️ badge counts only what lives behind it (incoming friend requests);
|
||||
// The ⚙️ badge counts incoming friend requests plus an awaiting feedback reply;
|
||||
// invitations surface in their own lobby section above.
|
||||
app.notifications = incoming.length;
|
||||
void refreshFeedbackBadge();
|
||||
}
|
||||
setLobby({ games, invitations, incoming });
|
||||
// Warm the cache for the ongoing games so opening one from the lobby is instant. The list
|
||||
@@ -234,7 +238,7 @@
|
||||
<span class="sq">📊</span><span class="lbl">{t('lobby.stats')}</span>
|
||||
</button>
|
||||
<button class="tab" onclick={() => navigate('/settings')}>
|
||||
<span class="sq">⚙️{#if app.notifications > 0}<span class="badge">{app.notifications}</span>{/if}</span>
|
||||
<span class="sq">⚙️{#if settingsBadge > 0}<span class="badge">{settingsBadge}</span>{/if}</span>
|
||||
<span class="lbl">{t('lobby.settings')}</span>
|
||||
</button>
|
||||
</TabBar>
|
||||
|
||||
Reference in New Issue
Block a user