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

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

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

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

View File

@@ -0,0 +1,245 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export type DockTone = 'progress' | 'success' | 'error';
export type DockItem = {
id: string;
label: string;
message: string;
tone: DockTone;
progress?: number | null;
meta?: string;
actionLabel?: string;
actionKind?: 'cancel' | 'retry' | 'remove';
};
let { heading = 'Queue', items = [] as DockItem[] } = $props();
const dispatch = createEventDispatcher<{
action: { id: string; actionKind: 'cancel' | 'retry' | 'remove' };
}>();
function toneLabel(tone: DockTone): string {
if (tone === 'error') return 'Issue';
if (tone === 'success') return 'Done';
return 'Live';
}
</script>
{#if items.length > 0}
<aside class="status-dock">
<div class="dock-head">
<div class="dock-kicker">{heading}</div>
<div class="dock-count">{items.length}</div>
</div>
<div class="dock-list">
{#each items as item (item.id)}
<section class="dock-card" data-tone={item.tone}>
<div class="dock-card-head">
<div>
<div class="dock-label">{item.label}</div>
<div class="dock-message">{item.message}</div>
</div>
<span class="dock-tone">{toneLabel(item.tone)}</span>
</div>
{#if item.meta}
<div class="dock-meta">{item.meta}</div>
{/if}
{#if item.progress != null}
<div class="dock-progress">
<div class="dock-progress-fill" style={`width:${Math.max(3, Math.min(item.progress, 100))}%`}></div>
</div>
{/if}
{#if item.actionLabel && item.actionKind}
<button
class="dock-action"
type="button"
onclick={() => dispatch('action', { id: item.id, actionKind: item.actionKind! })}
>
{item.actionLabel}
</button>
{/if}
</section>
{/each}
</div>
</aside>
{/if}
<style>
.status-dock {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 60;
width: min(360px, calc(100vw - 32px));
display: grid;
gap: 10px;
pointer-events: none;
}
.dock-head,
.dock-card {
pointer-events: auto;
backdrop-filter: blur(16px);
border: 1px solid rgba(35, 26, 17, 0.1);
box-shadow: 0 24px 40px rgba(35, 24, 15, 0.14);
}
.dock-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-radius: 20px;
background: rgba(255, 251, 246, 0.92);
}
.dock-kicker,
.dock-count,
.dock-tone {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.dock-kicker { color: #6a5f53; }
.dock-count { color: #1f1a15; }
.dock-list {
display: grid;
gap: 10px;
}
.dock-card {
display: grid;
gap: 10px;
padding: 14px;
border-radius: 22px;
background: rgba(255, 251, 246, 0.94);
animation: dockRise 240ms ease;
}
.dock-card[data-tone='progress'] {
background:
linear-gradient(145deg, rgba(255, 251, 246, 0.96), rgba(241, 247, 243, 0.88)),
radial-gradient(circle at 100% 0%, rgba(47, 106, 82, 0.12), transparent 26%);
}
.dock-card[data-tone='success'] {
background:
linear-gradient(145deg, rgba(251, 255, 251, 0.96), rgba(239, 247, 242, 0.9)),
radial-gradient(circle at 100% 0%, rgba(47, 106, 82, 0.14), transparent 26%);
}
.dock-card[data-tone='error'] {
background:
linear-gradient(145deg, rgba(255, 250, 249, 0.96), rgba(252, 239, 236, 0.9)),
radial-gradient(circle at 100% 0%, rgba(178, 61, 46, 0.16), transparent 30%);
}
.dock-card-head {
display: flex;
align-items: start;
justify-content: space-between;
gap: 10px;
}
.dock-label {
font-size: 0.95rem;
font-weight: 650;
color: #1f1a15;
line-height: 1.25;
}
.dock-message {
margin-top: 3px;
font-size: 0.84rem;
color: #655a4e;
line-height: 1.4;
}
.dock-meta {
font-size: 0.76rem;
color: #86796c;
}
.dock-tone {
padding: 5px 8px;
border-radius: 999px;
white-space: nowrap;
}
.dock-card[data-tone='progress'] .dock-tone {
background: rgba(47, 106, 82, 0.12);
color: #2f6a52;
}
.dock-card[data-tone='success'] .dock-tone {
background: rgba(47, 106, 82, 0.12);
color: #2f6a52;
}
.dock-card[data-tone='error'] .dock-tone {
background: rgba(178, 61, 46, 0.12);
color: #b23d2e;
}
.dock-progress {
height: 5px;
border-radius: 999px;
background: rgba(35, 26, 17, 0.08);
overflow: hidden;
}
.dock-progress-fill {
height: 100%;
border-radius: 999px;
background: #2f6a52;
transition: width 220ms ease;
}
.dock-card[data-tone='error'] .dock-progress-fill {
background: #b23d2e;
}
.dock-action {
justify-self: start;
padding: 8px 12px;
border-radius: 999px;
border: 1px solid rgba(35, 26, 17, 0.1);
background: rgba(255, 255, 255, 0.72);
color: #1f1a15;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
cursor: pointer;
font-family: var(--font);
}
@keyframes dockRise {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.status-dock {
right: 12px;
left: 12px;
bottom: 12px;
width: auto;
}
}
</style>