Commit Graph

49 Commits

Author SHA1 Message Date
mAi
8b0f453022 refactor(server): withOwnedInstance wrapper for admin /feedback/[id] endpoints
§3.C of docs/plans/architecture-improvements.md.

Lifts the auth + ownership + try/catch preamble that was inlined across
four admin endpoints into a single wrapper. Each endpoint now:

  export const POST = withOwnedInstance(async ({ inst, event }) => {
    // inst is guaranteed valid + owned, errors caught + tagged
  }, 'admin feedback X');

Files:

- New lib/server/admin-route.ts — runtime wiring (requireAuth, getInstanceById,
  handleApiError, Response helpers).
- New lib/server/admin-route-decision.ts — pure ownership decision branch.
  Lives in its own module so bun:test can exercise it without pulling in
  $env/dynamic/private through the feedback.ts → supabase.ts chain (same
  constraint as the existing rate-limit.test.ts comment).
- New lib/server/admin-route.test.ts — 4-row decision-table test
  (anonymous → 401, missing instance → 404, foreign owner → 401, owner → ok).

Endpoints rewired (auth+ownership boilerplate removed):

- /api/admin/feedback/[id]/+server.ts (GET / PATCH / DELETE — local `ownerOf`
  helper deleted, was only used here)
- /api/admin/feedback/[id]/posts/[post_id]/hide/+server.ts
- /api/admin/feedback/[id]/share/+server.ts
- /api/admin/feedback/[id]/export/+server.ts

The list endpoint /api/admin/feedback/+server.ts has the auth half but no
ownership half (it lists by owner_user_id = userId), so it stays unchanged.

Behaviour unchanged. 29 tests pass. svelte-check + bun run build clean.
2026-05-07 19:44:44 +02:00
mAi
bc278a87d6 Merge mai/cronus/chat-compose-grow: rows=3 + field-sizing auto-grow + shorter placeholder 2026-05-06 16:33:48 +02:00
mAi
c261b8e75b ui: chat compose grows with content + better default size
- rows='1' → rows='3' so the compose box has presence even when empty
  (it was a one-line slit, easy to miss).
- Add `field-sizing: content` on .fb-compose__textarea so modern browsers
  (Chrome 123+ / Firefox 137+ / Safari 17.4+) auto-grow it to fit content
  natively. Cap at the existing max-height: 10rem with overflow-y: auto so
  long messages don't eat the bubbles scroll area.
- For older browsers: feature-detect via CSS.supports('field-sizing',
  'content'). When unsupported, an oninput handler sets style.height =
  scrollHeight + 'px' (capped at 160px). Modern browsers no-op the JS
  resizer to avoid fighting the native CSS rule.
- After send (chatBody → ''), an $effect clears the inline height so the
  textarea snaps back to rows='3'. Native field-sizing handles this on
  its own; the effect only fires on the JS-fallback path.
- Shorten placeholder to "Nachricht…" — the keyboard hint was chrome the
  user discovers on their own.
2026-05-06 16:33:42 +02:00
mAi
18644275f1 Merge mai/cronus/participant-chat-cap: cap chat column to viewport on long forms 2026-05-06 15:46:08 +02:00
mAi
0868f8cd70 fix: cap participant chat column to viewport on long forms
m: "The size of the chat should be limited to the screen size (- frame)
so it does not go via the whole long form..."

The grid layout (1fr 1fr at ≥900px) was stretching the chat column to
match the form column's height. With a 20-question form the right side
of the page became a 3000px scroll-soup of empty chat space.

Two-line fix:

- .fb-participant gets `align-items: start` at ≥900px — grid items take
  their own natural height instead of stretching to the tallest sibling.
- .fb-participant__col--chat gets `max-height: calc(100vh - 2rem)` always
  + `position: sticky; top: 1rem` at ≥900px. Long form scrolls past it;
  chat stays pinned in the viewport. On mobile (single-column stack) the
  existing 70vh cap stays — the desktop max-height applies on top of it,
  so mobile uses min(70vh, 100vh - 2rem) which is just 70vh.

No markup changes — pure CSS.
2026-05-06 15:46:03 +02:00
mAi
538419fb1b Merge mai/hermes/single-sub-fixes: reload-shows-summary + copy + card redesign + hide answer-again on success 2026-05-06 15:44:42 +02:00
mAi
778df213da mAi: #4 - single-submission follow-up fixes
Four bugs from m's smoke pass on the just-shipped single-submission feature:

1. Reload showed the legacy "you can submit again" branch instead of the
   read-only summary, because the client never refetched its previous
   submission. Fix: page server load now does an IP+UA backstop lookup so
   first paint is correct; client onMount supplements with a session_id
   lookup against the new GET ?session_id= variant for the
   cleared-cookies-but-same-browser case. Renamed JSON field
   previous_submission to keep server/client shape symmetric. Same parametised
   .eq() pattern as the submit handler — no PostgREST .or() with a
   user-controlled session id.

2. Trailing colon in "Du hast schon abgesendet. Du kannst trotzdem nochmal
   antworten:" reads like an unfinished sentence. Rewrote as a question:
   "Du hast bereits abgesendet. Möchtest du eine weitere Antwort senden?"
   The branch is now also gated on !singleSubmission — when the toggle is
   on it never fires (the previous_submission branch wins).

3. The .fb-already card looked like a form replica (boxes around values).
   Replaced with a confirmation summary: ✓-icon header ("Antwort gesendet"
   + timestamp), then a definition list with muted labels above plain
   values, no input outlines. On ≥560px the rows become a two-column grid
   with light dividers.

4. The "Noch eine Antwort senden" ghost button on the success card was
   misleading when single_submission is on (clicking it 409s on next
   submit). Hidden when singleSubmission is true; the success banner
   alone now stands.

bun check 0 errors, bun test 25 pass, bun build OK.
2026-05-06 15:44:36 +02:00
mAi
0135de10f6 Merge mai/hermes/single-submission: single-submission enforcement (m/fdbck#4) 2026-05-06 15:32:26 +02:00
mAi
120f0798cd mAi: #4 - single-submission enforcement (default on)
Block repeat submissions per participant by default. No new fingerprint
column — dedup against the existing client_session_id and (client_ip,
user_agent) we already store on feedback_submissions.

Schema migration `fdbck_feedback_instances_add_single_submission` adds
single_submission BOOLEAN NOT NULL DEFAULT true (applied via Supabase MCP).
Existing instances default to true. Author opts out per-instance via the
new toggle on /admin/feedback/new and the detail Edit tab.

Server (POST /api/public/feedback/<slug>/submit): when
inst.single_submission is true, look up the most recent existing submission
matching instance_id AND (client_session_id = body.client_session_id) OR
(client_ip = req.ip AND user_agent = req.user_agent). Two separate
parameterised queries instead of a single PostgREST `.or()` filter — the
user-controlled session id has no character restriction in the schema, so
splicing it into a filter string would risk PostgREST filter injection.
Returns 409 with { error: 'already_submitted', submitted_at, display_name,
answers } so the client can render the previous answers without an extra
round-trip.

Client (/f/<slug>): on 409, replace the form with a read-only "already
submitted on <date>" card listing the previous answers per question. Reuses
question shape via a small summariseSubmittedAnswer() helper covering all
six question types including date_ranked_choice rating maps. No submit
button on the read-only view; live results polling still kicks off.

Admin schemas (InstanceCreate + InstanceUpdate) accept the new
single_submission boolean. POST/PATCH endpoints persist it.

bun check 0 errors, bun test 25 pass, bun build OK.
2026-05-06 15:32:20 +02:00
mAi
2d0df881c9 Merge mai/hermes/date-ranked-results-viz: calendar heatmap + cleaner bars + summarizeAnswer fix 2026-05-06 14:28:48 +02:00
mAi
936713b18c mAi: date_ranked_choice results — calendar heatmap + cleaner bars
m's complaint: "current mode just ranked looks bad". Replace the per-option
"5 horizontal bar rows" mini-chart with two complementary views the user
toggles between, calendar default.

Calendar (default when ≥2 options):
- Horizontal day strip from min(start) to max(start), one cell per day.
- Spans > 30 days suppress empty days; otherwise contiguous (incl. blanks).
- Each day cell stacks one coloured slot per option starting that day —
  multiple options on the same day stack vertically inside the cell.
- Slot colour = mean rating on a red→amber→green gradient (1 → 3 → 5).
  Linear interp between #ef4444, #f59e0b, #16a34a; empty days fall back to
  --color-bg-secondary. Slot shows time range, mean, and rating count.
- Hover (`title`) gives the full date, label, mean, and count for the day.

Bars (toggle, also the only view for single-option questions):
- One row per option, sorted by aggregator (mean desc → 5-count → 4-count
  → total → id). Per row: rank chip, date+label, large coloured avg,
  single horizontal stacked bar with one segment per non-zero rating
  bucket (segment colour matches the bucket's rating on the same r→a→g
  scale), total count.
- Empty rows say "Keine Bewertung" instead of an empty bar.

Bundled fix: `summarizeAnswer()` on the admin Responses tab now renders
date_ranked_choice answers as e.g. "3.5 avg (4 rated)" instead of the
default `[object Object]` String() fall-through.

bun check 0 errors, bun test 25 pass, bun build OK.
2026-05-06 14:28:34 +02:00
mAi
4bb7f75104 Merge mai/cronus/participant-redesign: two-column form|chat + phone-style bubbles + per-user color 2026-05-06 14:22:53 +02:00
mAi
86c46baffb ui: /f/[slug] two-column form|chat + phone-style bubbles + per-user color
m: "The difference between our form and the live chat needs to be more
prominent. I think we should style it more like a chat, too — maybe
separate into two columns — Form on the Left, Chat on the Right. And
chat messages more... like a chat on the phone. And we colorize different
users differently."

Layout

- New .fb-shell--wide modifier (max-width 960px) on the participant
  shell so the two columns breathe. .fb-participant grid: 1fr at <900px,
  1fr 1fr at ≥900px. Form column on the left, chat column on the right.
  Single-column flow (form-only or chat-only) just fills its column.
- Title + description + closed-line + name field stay full-width above
  the columns. Live results + footer stay full-width below.

Chat bubbles

- New .fb-bubble primitive replaces the old .fb-chat__post box-with-border
  pattern on /f/[slug] only (admin moderator UI keeps .fb-chat__post).
- My posts: right-aligned, --color-primary background, white text, no
  border. Others: left-aligned, --color-bg-secondary background, 3px
  left-border in the speaker's deterministic color.
- Author name: tiny + weight 500. For others, name text is the speaker's
  color; for me, it's --color-primary (you stay green).
- Timestamp: HH:mm if today, dd.MM HH:mm otherwise; viewer-localised.
- Vertical density: 0.6rem gap by default, +0.5rem extra when the speaker
  changes (bumps to ~1.1rem — visually distinct turn boundaries).

Per-user color

- 7-color palette (rose / orange / amber / cyan / blue / violet / pink),
  greens deliberately excluded so they don't clash with --color-primary
  which the viewer's own bubbles use.
- Stable hash of client_session_id maps to a palette index — same user
  gets the same color across reloads.
- Color applied to: author name text, bubble left-border. No avatar
  circle in v1.

Compose

- New .fb-compose at the bottom of the chat column (in flex-flow, no
  position:sticky needed — bubbles take flex:1 and scroll, compose stays
  pinned). Textarea + send button side-by-side.
- Single-row textarea (rows=1, max-height 10rem) grows on input.
- Enter alone sends; Shift+Enter inserts a newline; IME composition is
  respected (e.zh ne IME's confirm Enter doesn't submit).
- Placeholder includes the keyboard hint in German.

Auto-scroll

- $effect on posts.length scrolls the bubbles container to the bottom
  on append + initial load with behavior:'smooth'. Replaces the old
  imperative queueMicrotask calls inside fetchPosts/postChat.

Form column

- Section H2 ("Fragebogen") dropped — column boundary already names the
  section. Submit button: full-width on mobile, auto-width on desktop.
- Submit-success banner now uses .fb-form-banner--success class
  (replaces inline style hardcoded green).

CSS hygiene

- Removed orphaned .fb-chat__list / .fb-chat__form / .fb-chat__form-actions
  / .fb-chat__empty (only the participant page used them; new bubble
  primitives replace them). Admin-side .fb-chat / .fb-chat__post / etc.
  kept intact for the moderator UI.

Anti-scope honored: admin pages untouched, German strings unchanged,
3s polling interval kept, no new dependencies.
2026-05-06 14:22:48 +02:00
mAi
e984f26369 Merge mai/hermes/date-ranked-choice: date_ranked_choice question type (m/fdbck#1) 2026-05-06 14:15:52 +02:00
mAi
5ef08e5930 mAi: #1 - UI: builder + participant renderer + results display
- FormBuilder.svelte: new `Date ranked choice` type in the type picker.
  Per-option editor uses datetime-local inputs (start required, end
  optional) plus an optional free-text label. Below the option list, two
  optional rating-1 / rating-5 label fields and an "Allow participants to
  skip individual options" toggle (default on). Local↔UTC conversion
  helpers keep storage in UTC ISO 8601 while the input element shows the
  author's local time.
- /f/[slug]: participant rows of `(date · optional label)` + 1-5 button
  group + a "—" skip button. Required check enforces "at least one rated"
  for required questions and "all rated" when allow_partial=false.
- Results.svelte: ranked list of options with mean rating, count, and a
  per-option distribution histogram. Heading shows the option's local-time
  date range. Sort order comes from the aggregator (mean desc + tiebreaks).
- feedback.css: layout for the new builder rows, participant rating rows
  (mobile-stacks), and the ranked results list.

Refs m/fdbck#1.
2026-05-06 14:13:22 +02:00
mAi
439b030471 mAi: #1 - server: date_ranked_choice aggregation + export + tests
- `aggregateResults` ingests rating maps, tallies per-option counts +
  histogram (1-5 buckets) + running sum, then `finalise` computes per-option
  means and sorts options by mean desc with tiebreaks (count of 5s, then
  4s, then total count, then id). Question-level `count` reflects
  submissions that rated at least one option.
- Out-of-range, fractional, and non-integer ratings are silently dropped —
  the aggregator never trusts user data, schema validates it on submit.
- CSV export expands a date_ranked_choice question into one column per
  option named `<qid>[<optid>]`. JSON export is unchanged (it serialises
  the rating map directly).
- New `results.test.ts` covers: per-option counts and means, histogram
  tallying, mean-with-tiebreak ordering, ignoring bad ratings, and missing
  answers. Wires the file into the `bun test` script.

Refs m/fdbck#1.
2026-05-06 14:13:11 +02:00
mAi
91098e0965 mAi: #1 - schema: date_ranked_choice question type
New discriminated-union variant for `FeedbackQuestionSchema`:

  { type: 'date_ranked_choice',
    options: [{ id, start, end?, label? }, …],
    scale?: { min_label?, max_label? },
    allow_partial?: boolean }

- Times stored as UTC ISO 8601 strings (datetime with offset). Author UI
  feeds them through datetime-local inputs that the browser already treats
  as local time; renderer converts back to viewer-local on display.
- Rating scale is locked at 1-5 (5-point Likert) per design — the `scale`
  field exposes only labels, not min/max bounds.
- Per-option ids are 1-64 chars, alphanumeric + `-`/`_`, must be unique.
- 2-50 options per question.

Submission answer union extended with a `Record<string, 1|2|3|4|5|null>`
shape for the per-option rating map (`{ opt1: 5, opt2: null }`).

Refs m/fdbck#1.
2026-05-06 14:13:01 +02:00
mAi
4259f16d45 Merge mai/cronus/builder-on-new: visual ↔ JSON editor toggle on /admin/feedback/new 2026-05-06 14:06:06 +02:00
mAi
3cc1efa970 ui: visual ↔ JSON toggle on /admin/feedback/new (t-fdbck-builder-on-new)
m's complaint: "I already want the visual editor/json editor switch — why
only after creating an empty form, that makes no sense". Three steps to
get to the obvious starting place — create empty, navigate to detail,
switch tab — is friction.

Mirrors the detail-page Edit-tab pattern verbatim:

- editMode / editForm / editFormJson state, plus syncJsonFromVisual /
  syncVisualFromJson / switchEditMode helpers ported 1:1 from
  /admin/feedback/[id]/+page.svelte. The two pages now author questions
  the same way.
- Default mode: Visual, with a null editForm + "No questions yet." +
  "+ Add questions" button. Clicking the button calls ensureBuilderForm()
  which seeds the same { id: 'q1', label: 'Question 1', type: 'short_text',
  required: false } stub the detail page seeds.
- JSON mode unchanged: textarea + "Insert sample" + helper text.
- Submit logic resolves form_definition from whichever mode is active
  (mirrors detail-page saveEdits parsedForm branch).
- Disclosure framing kept ("Add questions now (advanced)") — collapsed by
  default so the title-+-chat-only path stays uncluttered.

Reuses FormBuilder.svelte directly. No new component, no new dep.
POST /api/admin/feedback contract unchanged.
2026-05-06 14:05:55 +02:00
mAi
006ecf442a Merge mai/cronus/fdbck-minimalist-ui: minimalist redesign — cards, header collapse, ⋯ menus, spacing scale 2026-05-06 12:38:28 +02:00
mAi
4ab4dc5c2d ui: tokenise .fb-question spacing + dark-mode QA notes
Final polish + verification commit. Tokenises the last hard-coded
margin in .fb-question (1.25rem → var(--space-4)) so the spacing scale
introduced in commit 1 is the single source of truth. Visually
identical (1.25rem === --space-4); the payoff is that any future
adjustment to the field-gap token propagates here automatically.

Optimistic status toggle was implemented inline in the list (commit 4)
and detail (commit 5) pages with a revert-on-failure path — no further
sweep needed.

Dark-mode QA — verified at 375 / 1024 / 1440 widths via headless
Chromium with --enable-features=WebContentsForceDark + --force-dark-mode
(token cascade confirmed firing: the rendered gradient matches the dark
--gradient-bg in feedback.css, not Chrome's auto-dark inversion of the
light gradient):

- /  — wordmark + tagline + ghost CTA centred, dark-teal gradient ✓
- /login — vertical-centred form, white-on-dark fields, primary CTA ✓

Admin pages and /f/[slug] need post-deploy verification once head
merges + deploys this branch — they require auth + a real form slug
which the local preview can't supply. The token cascade is shared
with these pages so visual regressions are unlikely; functional QA
of the optimistic toggle, ⋯ menu click-outside, and share-strip
short-link flow should happen against fdbck.msbls.de.
2026-05-06 12:37:26 +02:00
mAi
d9e4bfc165 ui: minimalist /f/[slug] polish
- Drop the inline-label name row (.fb-name-row) — the only
  inline-label-with-input pattern in the app. Replace with the same
  stacked-label .fb-question pattern every other field uses.
- Closed-state: switch from amber .fb-banner--closed to a quiet neutral
  .fb-closed-line (italic muted text between two thin border lines).
  Closed is a state, not an alert.
- Mine-post chat bubble: drop the full primary-coloured border in favour
  of a 3px left-border-only accent. Less loud, still recognisable.
- Footer "fdbck.msbls.de" wraps as a permalink to / with hover affordance.

German strings on this page are unchanged per m's override (m/fdbck#3
will handle proper i18n separately).
2026-05-06 12:31:55 +02:00
mAi
94f6ba934d ui: minimalist /admin/feedback/[id] detail — header collapse + share strip
The header was the densest surface in the app: 8 controls in a single
flex-wrap row plus a separate Share section bolted between header and tabs.
This commit collapses both into something readable.

Header:

- 8 visible controls → 3 visible. Status pill is now clickable and toggles
  between open/closed (optimistic), replacing the Close/Reopen button. The
  ⋯ menu absorbs Copy /f/<slug>, Export CSV, Export JSON, and a separator
  before Delete (still in red). All gone from the top-row strip: the raw
  /f/slug, Copy link, Preview, Close/Reopen, CSV, JSON, Delete buttons.
- Quiet text-only .fb-back-link replaces the chip-style "← All forms"
  button.
- New .fb-detail-head primitive lays out title-block on the left + actions
  on the right with proper flex-wrap behaviour.

Share:

- Standalone <section data-fb-share> deleted. Its job moves to a new
  inline .fb-share-strip directly under the title in the header.
- Strip always shows a usable URL: short_url if it exists, else the raw
  /f/<slug>. Copy + Open ↗ buttons sit alongside.
- Below the strip, a compact <details class="fb-share-strip__replace">
  holds the slug input + Create/Replace button. Summary text adapts to
  whether a short link already exists.

Tab body:

- Drop the inner <h2> in every tab body (the active tab pill names the
  section). All four tab bodies now use .fb-tab-body for consistent top
  padding (var(--space-6)).
- "X responses already received…" warning becomes a muted .fb-question__help
  line, not a .fb-banner box.
- Visual / JSON toggle becomes a real .fb-segment control matching the
  shape of .fb-tabs (consistency).
- Save row uses .fb-save-row with the version pill ("Current version: vN")
  rendered as a quiet .fb-version-note next to the Save button instead of
  decorating the H2 like before.
- Submissions table extracted to a small <style> block (.fb-detail-table)
  instead of inline style="..." chunks.

Click-outside-to-close + Escape close any open ⋯ menu, mirroring the list
page. Polling, refresh, and all backend contracts unchanged.

Delete still uses confirm() per m's override — deletion remains a deliberate
two-step action, no undo toast.
2026-05-06 12:31:00 +02:00
mAi
80f2f82ac1 ui: minimalist /admin/feedback list — cards + ⋯ menu + status pill toggle
m chose cards over a spacious-list pattern. They're MINIMALIST cards: subtle
.fb-card bg on the gradient page bg, no border + shadow stack, generous
internal padding, plenty of negative space between cards (1.5rem mobile,
2rem desktop). 2-up at ≥640px so they breathe without widening the shell.

Per-row simplifications:

- Drop the H2 "Your forms (N)" — the cards are the count.
- Drop the descriptive paragraph in the header — single primary CTA on the
  right is the entire header.
- Card title is the link to detail. The "Edit" button becomes implicit.
- Subtitle merges mode + counts on one muted line; the "created DATE" line
  and the raw "/f/<slug>" line both go away (slug is in the menu, date is
  available on the detail page).
- Right side: a clickable .fb-status-pill that flips status optimistically,
  next to a .fb-menu (⋯ trigger + native <details> panel) holding Copy link
  / Open / Edit / ────── / Delete.
- Optimistic status toggle: pill flips instantly, PATCH fires in background,
  reverts to server state on failure. Status is reversible so this is safe.
- Delete still uses the existing confirm() modal (m's override — no undo
  toast, deletion remains a deliberate two-step action).
- All inline style="..." removed except a tiny hoisted .fb-list-head style
  block for the header layout.

Click-outside-and-Escape close any open ⋯ menu — added via document-level
listener in onMount, cleaned up in onDestroy.

Empty state gets generous whitespace + a primary "Create your first form"
CTA in the .fb-empty container.
2026-05-06 12:28:21 +02:00
mAi
5080f39079 ui: minimalist /admin/feedback/new
- Drop the page subtitle ("Set up a feedback form, a live chat session…").
  The H1 + the form below carry meaning on their own.
- Replace the chip-style back-button with a quiet text-only .fb-back-link.
- Replace the inline-checkbox-as-fb-option-row chat toggle with a proper
  .fb-toggle (label-left + hint + native checkbox-right).
- Tuck the JSON-questions textarea + sample button + helper text behind a
  <details> disclosure labelled "Add questions now (advanced)".
  The visual builder on the detail page is the canonical path; the JSON
  paste at creation time is a power-user speed-up that no longer dominates
  the page. Common path now reads as 4 inputs and a button.
- Move the "Insert sample" button inside the disclosure where it belongs.

Backend untouched. /api/admin/feedback POST contract unchanged.
2026-05-06 12:27:11 +02:00
mAi
b1ee5530fd ui: minimalist landing + login
- /: vertical-centred narrow shell, wordmark grows to 2.5rem with -0.03em
  tracking, tagline simplified to "feedback by link", single ghost CTA
  to /login. Drops the redundant "this page is only reachable through a
  private link" sentence (the user is already here).
- /login: vertical-centred narrow shell, drops "Admin access only."
  subtitle (URL says it), error moves from .fb-banner--error block above
  the button to .fb-inline-error muted text below it (no layout shift,
  less alarm).
2026-05-06 12:26:15 +02:00
mAi
2593122337 style: CSS foundation for minimalist UI redesign
Adds the spacing scale, status-pill tokens, and a set of new utility classes
that the per-screen commits will use:

- spacing scale: --space-1 through --space-9 (single source of truth for
  vertical rhythm; replaces ad-hoc rem values throughout the .svelte files)
- status pill tokens: --color-status-{open,closed}-{bg,fg} (dark-mode aware,
  closed pulls from the same warning palette as .fb-banner--closed)
- .fb-shell.fb-page-narrow + .fb-page-center for vertical-centred narrow
  shells (landing + login)
- .fb-back-link — quiet text-only back-link, replaces the chip-style button
- .fb-inline-error — quieter alternative to .fb-banner--error
- .fb-toggle / .fb-toggle__{text,label,hint} — label-left + checkbox-right
  boolean fields (no UI library)
- .fb-status-pill / .fb-status-pill__dot / --open / --closed — clickable
  pill that toggles status
- .fb-menu / .fb-menu__{btn,panel,item,divider,item--danger} — native
  <details>/<summary>-based ⋯ menu, no JS framework needed
- .fb-card / .fb-card__{head,title,actions,meta} + .fb-card-grid — minimalist
  card on the gradient page bg, no border + shadow stack, generous padding
- .fb-empty — generous empty state
- .fb-share-strip / __url / __placeholder / __replace — inline header strip
  for the detail page, replaces the standalone Share section
- .fb-closed-line — neutral muted closed-state line for /f/[slug]
- .fb-segment / __btn / __btn--active — small segmented control matching
  .fb-tabs (for inline Visual/JSON toggle on Edit tab)
- .fb-detail-head / __title / __actions, .fb-tab-body, .fb-version-note,
  .fb-save-row — detail-page header + tab-body layout primitives

Also normalises:

- .fb-section margin-bottom 1.75rem → var(--space-7) (≈ 2.5rem)
- focus-ring opacity 0.15 / 0.25 → 0.2 across .fb-input + .fb-btn for a
  single consistent focus treatment

No structural .svelte changes here — only CSS additions and three numeric
edits. Existing pages continue to render exactly as before; the per-screen
commits that follow consume these classes.
2026-05-06 12:25:29 +02:00
mAi
301cec817a docs: minimalist UI redesign proposal
Per-screen audit + 6 design principles + per-screen mockups + commit-by-commit
implementation plan + 7 open questions.

Boldest moves: collapse the 5-button-per-row admin list into a hover-revealed
⋯ menu with clickable status pill; fold the standalone Share section into the
detail-page header as an inline link strip; drop the JSON-questions textarea
from /new behind a <details> disclosure so the common path reads as four
inputs and a button.

No code touched — design only. Awaiting m's go before coder shift.
2026-05-06 12:16:18 +02:00
mAi
3d03ee0c85 Merge mai/hermes/fdbck-shlink-short-link: shlink integration + admin Share section 2026-05-05 23:15:26 +02:00
mAi
696b796383 mAi: #2 - admin Share section + env-var docs
Self-contained "Share" section on the admin detail page. When no short URL
exists yet: shows an optional custom-slug input + "Create short link"
button. When one exists: shows the URL with Copy + Open buttons and a
collapsed "Replace" form for picking a new slug.

Append-only — does not touch existing buttons, the icon system, or
feedback.css; uses inline styles + existing fb-* classes only, so it stays
out of dokploy's parallel button-system refactor.

.env.example documents SHLINK_URL + SHLINK_API_KEY (must be copied from the
flexsiebels.de Dokploy app config to fdbck.msbls.de before this works in
prod).

Refs m/fdbck#2.
2026-05-05 23:13:13 +02:00
mAi
c5d0b2ae60 mAi: #2 - shlink server: helper + share endpoint + schema
Port flexsiebels' shlink REST v3 wrapper for short-link sharing of feedback
forms. New helper `src/lib/server/shlink.ts` reads SHLINK_URL (default
https://msbls.de) + SHLINK_API_KEY from env. New auth-gated
`POST /api/admin/feedback/<id>/share` builds the long URL from
PUBLIC_SITE_URL + instance.slug, calls shlink, persists shortUrl/shortCode
on feedback_instances, and returns the updated row. Adds ShareCreateSchema
(zod) for the request body and extends FeedbackInstance with the new
columns.

DB columns short_url + short_code added via Supabase migration
fdbck_feedback_instances_add_short_url (both TEXT NULL).

Refs m/fdbck#2.
2026-05-05 23:13:05 +02:00
m
e53b468dea Merge mai/dokploy/button-system: button variants + icons + Inter font 2026-05-05 23:12:18 +02:00
m
527fd57a72 button system: variants + icons + consistent sizing + Inter font
- Icon.svelte: inline lucide SVGs (apache-licensed paths copied directly,
  no npm dep) — edit, copy, external-link, lock, unlock, trash, plus, eye,
  eye-off, check, x, arrow-left, arrow-right, download.

- Refactored .fb-btn system in feedback.css:
  * Base = primary green, fixed 2.5rem height, gap for icons, Inter font.
  * .fb-btn--secondary: tinted primary-light surface that fills on hover.
  * .fb-btn--ghost: subtle gray bg with border (NOT bare white) — addresses
    m's "no background color" complaint. Dark-mode override included.
  * .fb-btn--danger: red, lifts shadow on hover.
  * .fb-btn--sm / --lg: 2rem / 3rem heights with proportional padding.
  * .fb-btn--icon: square icon-only variant.
  * Focus-visible ring via box-shadow on the green primary tint.

- app.html: preconnect + load Inter (400/500/600/700) from Google Fonts.
  .fb-page now stacks 'Inter' first.

- Applied icons + the right variant on every button across admin chrome
  + login per the brief table:
  * list row: Edit (secondary, edit), Copy link (ghost, copy), Open
    (ghost, external-link), Close/Reopen (ghost, lock/unlock),
    Delete (danger, trash) — all --sm so the row breathes.
  * list header CTA "+ New form" (primary, plus).
  * /admin/feedback/new: back link (ghost --sm, arrow-left), Insert
    sample (secondary --sm, plus), Create form submit (primary, check).
  * detail header: back link, Copy link, Preview, Close/Reopen
    (secondary), CSV/JSON exports (ghost, download), Delete (danger,
    trash) — all --sm.
  * detail chat: hide/show buttons promoted to .fb-btn--ghost --sm
    with eye/eye-off icons; killed the inline-styled bare button.
  * detail edit tab: Visual/JSON toggle now flips between --secondary
    (active) and --ghost (inactive) with no inline style overrides;
    Save (primary, check); "+ Add questions" (secondary --sm, plus).
  * login: Sign in keeps primary, gains arrow-right hint on the right.

- FormBuilder: Add-option / type-add buttons reduced to --sm + plus
  icon. Remove-question / remove-option icon-buttons get the trash/x
  lucide SVG. Killed the redundant CSS overrides on these
  (.fb-builder__add-option / .fb-builder__add .fb-btn) since
  .fb-btn--sm now does the sizing.

Acceptance: no inline button overrides remain on .fb-btn instances; all
admin row heights are consistent (2rem); ghost buttons have the tinted
surface; dark mode handled via @media block.
2026-05-05 23:12:12 +02:00
m
ea65eb7bb7 Merge mai/dokploy/admin-list-actions: per-row actions + English rewrite + segmented tabs + /admin/feedback/new 2026-05-05 18:51:43 +02:00
m
643c356cb6 admin: per-row actions, English rewrite, segmented tabs, /admin/feedback/new
Per-row action bar on the list page:
[Edit] [Copy link] [Open] [Close|Reopen] [Delete] — Delete confirms then
DELETE + invalidateAll(); Close/Reopen PATCHes status, no confirm; per-row
error banner.

Full English rewrite of admin chrome (list + detail + builder), login,
landing. Drop dev jargon — "instance" / "slug" / "schema" / docs/plans
references gone. Sample SAMPLE_FORM content also translated to a
session-feedback example. Participant /f/<slug> stays untouched (author-
supplied content). Results.svelte stays as-is too — shared with the
participant page where the surrounding chrome is German.

Tab strip on /admin/feedback/<id> restyled as a segmented pill bar
(.fb-tabs / .fb-tab / .fb-tab--active). Active tab gets the green
primary-light background + bolder text + radius-md, hover lifts to white.
Earlier tabs were nearly invisible.

Split create form to its own route /admin/feedback/new (page + auth-only
+page.server.ts mirroring the list loader). List page now shows just the
form list with a "+ New form" CTA in the header.
2026-05-05 18:51:38 +02:00
m
f31c1d6f35 Merge mai/dokploy/modern-styling: flexsiebels palette + viewport fix + lifted cards 2026-05-05 18:29:59 +02:00
m
25e3acdfe4 modern styling: flexsiebels palette, viewport reset, lifted cards
- html/body reset (margin 0, bg + color via tokens, fill viewport) — kills
  the white user-agent frame around the dark page in dark mode.
- Replace --fb-* tokens with the flexsiebels variables.css token set
  (--color-*, --radius-*, --shadow-*) and keep --fb-* aliases pointing at
  them so existing class names work without rewriting.
- Page background is now the flexsiebels green gradient (light mode) and
  a charcoal→teal gradient (dark mode).
- Buttons: green primary with shadow + active-press transform, ghost
  variant with proper border + hover.
- Inputs/textareas: rounded-md, focus ring via box-shadow on
  --color-border-focus instead of bare outline.
- Scale buttons: hover hint + green active state with shadow.
- Chat posts + builder cards: white surface with shadow-sm.
- Banners: subtle elevation; dark-mode variants for closed/error so they
  read on the dark gradient.
- Headings: tighter letter-spacing, slightly larger h1.

System dark mode (prefers-color-scheme) still toggles automatically; the
participant page sections stay flat (no re-introduced frame).
2026-05-05 18:29:55 +02:00
m
eadfc39670 Merge mai/dokploy/results-builder-versioning: Ergebnisse tab + live results, form builder, form versioning 2026-05-05 18:24:52 +02:00
m
5e758d49af admin Ergebnisse tab + live results, form builder + JSON view, form versions w/ snapshots
Migration: + fdbck.feedback_instances.live_results_enabled bool default false
           + fdbck.feedback_submissions.form_snapshot jsonb (frozen form per submission)

Schemas (moved $lib/server/schemas.ts → $lib/schemas.ts so the form-validation
Zod runtime can be reused on the client):
- form_definition.version: "0.YYMMDD" (today = 0.260505) with .b/.c suffix
  for same-day re-edits when older snapshots already use that day
- live_results_enabled on Create + Update DTOs

Server:
- submit/+server: writes the parsed form_definition into form_snapshot so
  results stay queryable after the form is later edited
- admin POST: stamps todayVersion() on first save
- admin PATCH: stampVersion() keeps current version while no submission has
  it yet; otherwise advances to today (or .b/.c)
- new $lib/server/results.ts: pure aggregation + version helpers
  (scale → histogram + mean, choices → counts + other_count for vanished
  options, boolean → yes/no, text → list of free-text answers)
- new GET /api/public/feedback/<slug>/results: gated on live_results_enabled,
  strips free-text answers (count-only) for participant-side display
- admin GET + page loader return aggregated results alongside submissions

UI:
- Results.svelte component (shared admin/participant) — CSS bar charts,
  no external lib
- FormBuilder.svelte — add/remove/reorder/edit questions, type switch,
  options/scale config; visual ↔ JSON toggle in admin Edit tab keeps both
  views in sync
- admin detail: new "Ergebnisse" tab with version stamp, "live_results"
  checkbox in Edit tab, info banner about version bumps when submissions exist
- /f/<slug>: after submit (and only if live_results_enabled), polls
  /results every 5s and renders <Results /> below the form
2026-05-05 14:54:03 +02:00
m
cbde51b0f2 Merge mai/dokploy/fix-jsonb-form-definition: parse JSONB form_definition, footer text, flat sections 2026-05-05 12:05:27 +02:00
m
d084fc098b fix /f/[slug]: parse JSONB form_definition, footer text, flatten section frame
- supabase-js with .schema('fdbck') returns JSONB columns as JSON-encoded
  strings; getInstanceBySlug + getInstanceById + admin list now JSON.parse
  via a shared parseFormDefinition helper, so FeedbackFormDefinitionSchema
  sees an object and questions actually render.
- footer: 'flexsiebels.de · per-Link Feedback' → 'fdbck.msbls.de'.
- .fb-section: drop the white card frame (transparent bg, no border, no
  border-radius) — sections now flow flat on the page.
2026-05-05 12:04:29 +02:00
mAi
b914294769 README + design doc copy
- README.md: stack, run-locally, test/check/build, structure tree, data
  model summary, anti-abuse layers, scope notes, issue origin pointer.
- docs/plans/feedback-feature.md: copied verbatim from flexsiebels for
  self-containment (single source of truth in this repo from now on).
2026-05-05 11:38:11 +02:00
mAi
699000c63d Dockerfile: oven/bun:latest, root-run (avoids alpine UID 1000 collision)
Mirrors msbls.de pattern, simplified (no mbrian-core submodule clone).

UID note: oven/bun:1-alpine has a built-in 'bun' user at UID/GID 1000 and
`addgroup -u 1000` on top of it breaks the build silently. mExDraw#14
(commit fc62b9c) lost ~4 weeks of Dokploy deploys to that. Comment in the
Dockerfile so the next person doesn't trip over the same.

Production build verified locally: vite build ✓ (4.08s).
2026-05-05 11:37:36 +02:00
mAi
f9140a414a admin pages (list + detail) + login page (Supabase email/password)
- /admin/feedback (page.server.ts + page.svelte): list with status/mode badges, counts, JSON-editor create form. flex()→fdb() rename done.
- /admin/feedback/[id] (page.server.ts + page.svelte): tabbed detail (Chat / Submissions / Edit), 5s admin polling, hide-toggle, close/reopen, CSV/JSON export, delete. flex()→fdb() rename done.
- /login: simple email + password form posting to /api/auth/sign-in. Pre-redirect if already authed (locals.userId in load). Honours ?redirect= query.

Pages otherwise byte-identical ports of the flexsiebels versions — schema
helper rename happens in /server/fdb.ts.

bun run check: 0 errors, 13 warnings (known false-positive 'data/inst captured
at init'; same pattern flexsiebels has).
2026-05-05 11:36:42 +02:00
mAi
4c68b48417 /f/[slug] participant page (no layout reset hack — whole app is naked)
Direct port from flexsiebels worktree. Imports getInstanceBySlug from
$lib/server/feedback (which uses fdb()) — schema rename happens at the
helper level, page code is identical.

Behaviour:
- LocalStorage: feedback:display_name (global) + feedback:session:<slug>
- 3s polling /posts?since=<latest_ts>; auto-scroll on new
- Hidden posts: '(Beitrag entfernt)' for others; own session sees body + note
- Honeypot 'company' input (CSS-hidden, aria-hidden)
- 423 → closed banner; 429 → rate-limit message; required-validation client+server
- noindex meta + no-referrer
- Question types: short_text, long_text, single_choice, multi_choice, scale, boolean

Root +layout.svelte already gives the naked shell (no sidebar/footer/bottom-nav)
so the +layout@.svelte reset trick is unnecessary here.

bun run check: 0 errors, 5 warnings (known false-positive 'data captured at
init' on $state — data from server load doesn't change client-side; same warning
pattern as flexsiebels).
2026-05-05 11:35:30 +02:00
mAi
946c755f17 feedback API endpoints (port from flexsiebels, fdb() schema rename)
Public (slug-gated, auto-allowlisted):
- GET  /api/public/feedback/[slug]              — instance config
- POST /api/public/feedback/[slug]/submit       — form submission (honeypot, rate-limit, required-validation, 423 if closed)
- GET  /api/public/feedback/[slug]/posts        — chat polling (?since=, hides body of moderated posts)
- POST /api/public/feedback/[slug]/posts        — new chat post (honeypot, rate-limit, 423 if closed)

Admin (requireAuth, owner-scoped):
- GET/POST   /api/admin/feedback                — list/create
- GET/PATCH/DELETE /api/admin/feedback/[id]    — detail/update/delete (PATCH closes/reopens, sets closed_at)
- POST       /api/admin/feedback/[id]/posts/[post_id]/hide — toggle hidden flag
- GET        /api/admin/feedback/[id]/export?format=csv|json — single-file dump

Auth:
- POST /api/auth/sign-in   — Supabase email+password, sets access+refresh cookies
- POST /api/auth/sign-out  — clears cookies

bun run check: 0 errors, 0 warnings.
2026-05-05 11:34:54 +02:00
mAi
f5992ebc5b schemas + rate-limit + feedback helpers + tests
- src/lib/server/schemas.ts: feedback Zod schemas (Question discriminated union + FormDefinition + Instance create/update + Submission/Post/Hide + SignIn).
- src/lib/server/rate-limit.ts (+ test): in-memory token bucket — direct port from flexsiebels.
- src/lib/server/feedback.ts: generateSlug (32-char base62), getInstanceBySlug/ById via fdb(), RATE_LIMIT constants, clampUserAgent.
- src/lib/server/public-scope.test.ts: gate behaviour tests (allowlist coverage + 6 evaluatePolicy cases). Adapted for fdbck's allowlist (no /api/share, no /api/gotify-public).
- @types/bun added so svelte-check resolves bun:test imports — clean baseline (no 'Cannot find bun:test' tech debt that the flexsiebels project carries).

bun run check: 0 errors, 0 warnings.
bun run test: 20/20 pass.
2026-05-05 11:32:23 +02:00
mAi
fa1ad92517 auth + supabase + public-scope hook (mirrors flexsiebels gate, no API keys)
- src/lib/server/supabase.ts: getSupabaseAdmin/Anon (lazy singletons, env-driven URL)
- src/lib/server/fdb.ts: schema accessor for the fdbck Postgres schema
- src/lib/server/auth.ts: cookie-based JWT auth (access+refresh), Supabase getUser/refreshSession. NO API key path — fdbck has no api_keys table; if needed later, add a separate module.
- src/lib/server/request-context.ts + public-scope.ts: public-scope policy gate ported from flexsiebels#59. Allowlist /api/auth/* and /api/public/* by default.
- src/lib/server/response.ts + errors.ts: json/requireAuth + parseBody/handleApiError
- src/hooks.server.ts: validate cookies, set locals.userId, refresh tokens, run handler inside RequestState scope, evaluatePolicy after.
- src/routes/+layout.svelte: minimal naked shell (only loads feedback.css). NO sidebar/footer/bottom-nav per spec.
- src/routes/+page.svelte: brief landing page + admin-login link.
- src/lib/styles/feedback.css: copied verbatim from flexsiebels worktree.

bun run check: 0 errors, 0 warnings.
2026-05-05 11:30:13 +02:00
mAi
ae2984088a skeleton: SvelteKit fullstack app (msbls.de pattern, fdbck variant)
Bootstrap from /home/m/dev/web/msbls.de template:
- SvelteKit 2.15 + Svelte 5 + adapter-node + bun + vite 6
- Deps trimmed: @supabase/supabase-js, postgres, zod (+ dev: kit, vite-plugin-svelte, svelte-check, typescript)
- No mbrian-core submodule (irrelevant for fdbck)
- src/app.html minimal (no fonts, no theme toggler)
- src/app.d.ts declares App.Locals { userId: string | null }
- robots.txt Disallow: / (whole app is naked, per-link or auth-only)
- .env.example: Supabase + PUBLIC_SITE_URL + optional COOKIE_DOMAIN

Initial mai init scaffolding (.claude, .m, .mcp.json, AGENTS.md) bundled in
this first commit since the repo was empty before bootstrap.

Spawned from m/flexsiebels.de#63 pivot — see docs/plans/feedback-feature.md
for the full spec (copied in next commit).
2026-05-05 11:27:59 +02:00