All notable changes to Kaffeelisten will be documented here.
Format follows Keep a Changelog. Versioning follows Semantic Versioning.
- LinkedIn and other social previews now show a branded cover image. The public pitch deck exposes a 1200×630 Open Graph image, complete image metadata, and a canonical URL so link crawlers can build a reliable preview.
- Paid tracking is now scan-and-tick, not modal-per-person. The Bezahlt column (last three months of checkboxes per person) in Mitarbeitende is on by default (requires migration
029), and a summary strip above the list shows „X von Y bezahlt" per month with the euro amount still outstanding — so the admin sees coverage across everyone at a glance and ticks people off in place. The per-person Zahlungen dialog stays as an optional detail view for full history; it can still be hidden entirely under Einstellungen → Zahlungsübersicht. „Firma zahlt" people are excluded from the counts and show a muted marker.
- Per-company documents, split from the admin/CEO report. The monthly email is now three independent streams: the admin/CEO aggregate report (all companies, to the report recipients), a per-company document to each company's billing contact, and the per-member document. Each company with a billing contact gets its own monthly Aufstellung, or an invoice when it's set to Firma zahlt (
company_paid) and invoicing is configured. Toggle Firmendokumente turns the per-company stream off.company_paidmembers are covered by the company document and not billed individually. Requires migration026. - Previews for every email variant. Einstellungen now previews the Admin-/CEO-Bericht, the Firmendokument (Aufstellung and Rechnung) and the Mitglieder-Dokument (Aufstellung and Rechnung). Invoice previews render with ITC1 placeholder values when the issuer block hasn't been filled in yet, and reflect unsaved edits.
- Optional invoice mode. In Einstellungen → Rechnungsstellung the admin can switch the monthly member emails from an informational statement to a proper invoice issued by ITC1. When enabled, the admin fills ITC1's issuer block (legal name, address, USt-IdNr, receiving IBAN/BIC, number prefix, payment terms, VAT rate); the email then shows a document number, a net/VAT/gross breakdown and a prominent "wohin überweisen" box. The developers never appear as issuer. Requires applying migrations
023–025. - Per-company billing mode. A company can be set to Firma zahlt (
company_paid): one invoice goes to a company billing contact covering all its members, instead of billing each member.company_paidrequires a billing contact email; Einzeln (individual) is unchanged and remains the default. - Invoice numbering + ledger. Every issued document gets a unique, never-reused number (
<Präfix><laufende Nummer>), recorded in a billing ledger. Re-running a month re-sends the same numbers instead of allocating new ones. - Payment tracking. A Rechnungen — Zahlungsstatus card lists each month's documents with a per-recipient bezahlt/offen toggle.
- Per-member payment tracking in Mitarbeitende. Each person now has a Zahlungen action opening a month-by-month bezahlt/offen view, working with or without invoice mode. Amounts are derived live from the current month plus the archive. People whose company covers the coffee (Firma zahlt) are shown as von Firma übernommen rather than billed personally. Requires migration
027. - Inline paid grid in Mitarbeitende. A Bezahlt column shows the last three months as compact checkboxes per person (current month emphasized, the two prior greyed but still toggleable), so the admin can see and update who has paid at a glance without opening the modal. The window rolls forward automatically each month; Firma zahlt members show a muted Firma marker instead. The column is off by default and toggled on under Einstellungen → Zahlungsübersicht (requires migration
028); the per-person Zahlungen modal stays available regardless. - Vercel Web Analytics. Privacy-friendly, cookieless page/visit metrics via
@vercel/analytics; active only on Vercel-hosted deployments (a no-op locally). Enable Web Analytics in the Vercel project dashboard to collect data. - Invoice/statement PDFs attached to every email + archive zips. Each member and company email now carries its document as a PDF attachment. In invoice mode: when a company pays, its contact gets one invoice PDF; when each person pays, every member gets their own invoice PDF and the company contact receives a ZIP archive of all its members' invoices plus a ledger sheet. The Geschäftsführung (CEO) additionally receives a single global ZIP — the full invoice ledger (Excel) plus every invoice PDF and the summary report — in place of the plain report attachments. In report mode the CEO still gets the report as one PDF + one Excel. PDF generation is bounded per run (wall-clock deadline + count cap) and fail-soft: a slow or failed render never blocks the email or the month-end archive.
- Company editor redesign. The company dialog now always shows the contact person (name + e-mail) and a compact Wer zahlt? switch (Firma zahlt / Jede Person), with inline copy explaining what each choice means. New companies default to Firma zahlt.
- Supabase CLI config committed. Added
supabase/config.tomlaligned to the linked project (Postgres 17), sosupabase linkno longer reports a config drift.
- Invoice mode is off by default; with it off, behaviour is unchanged (statements as before). Delivery is the email body — no per-user PDF is generated. See
docs/prd-billing-commercial-addendum.md; invoice wording/VAT/retention are subject to confirmation by ITC1's tax adviser.
- Admin can create and edit members, companies and items again. After the RLS lockdown moved all admin writes to the service-role API, saving any member, company or item in the admin panel failed with a server error — the
service_roledatabase role had never been grantedINSERT/UPDATEon those tables (onlySELECT). Reads were unaffected, so the breakage only showed on save. Requires applying migration022to the database.
- Member email confirmation (proof of ownership). When a member is added — or their work email is changed — Kaffeelisten now emails them a one-time confirmation link. Clicking it verifies the address on the new public page
/email-bestaetigen. The admin Mitarbeitende table shows a Bestätigt / Ausstehend badge per person and a per-row action to (re-)send the confirmation email. Unverified members still receive their monthly statement — they are only flagged, never skipped. Seedocs/email-deliverability.md.
- International member names are no longer corrupted on save. The admin member form force-title-cased every name, mangling names like McDonald, van der Berg, de la Cruz and O'Brien. Names are now stored with the casing the admin typed (only surrounding/duplicate whitespace is trimmed).
- Member work-email addresses are now checked for deliverability when saved. Beyond the existing shape check, the admin API verifies the email's domain actually accepts mail (MX record, with an A/AAAA fallback), rejecting typos like
[email protected]or[email protected]. Transient DNS failures never block a save. - All outgoing email now sets a
Reply-To. Reports, member statements, PIN-reset codes and the new confirmation mail point replies at the first configuredADMIN_EMAILaddress instead of the no-reply from-address, improving deliverability and trust. New DNS/Resend audit checklist for SPF/DKIM/DMARC indocs/email-deliverability.md.
First production release for the ITC1 Deggendorf campus. Consolidates all development since the initial scaffold; entries below are grouped by the change that introduced them.
- Upgraded vulnerable production dependencies.
npm auditfixes bringtmp(path traversal) andws(memory disclosure / DoS) to patched versions. One moderate advisory remains inexceljs's bundleduuid; it requires abufargument we never pass, so it isn't reachable — left rather than downgradingexceljsto a breaking major.
- PDF report Chromium version aligned. The report downloaded a v131 Chromium pack while
@sparticuz/chromium-minis pinned to v147; the mismatch could ship a binary the bundledpuppeteer-corecan't drive. The pack URL now matches the installed version.
- Added an automated test suite (Vitest) covering report scheduling, the admin session token, report summarisation, HTML/CSV escaping, and subject-template rendering. CI now runs the tests and typechecks the serverless
api/code (previously onlysrc/was typechecked). The CSV formula-injection guard moved into a shared, testedlib/csv.ts.
- Admin sessions now use a signed, HttpOnly cookie instead of the PIN held in
sessionStorage. After a successful PIN entry (or reset) the server sets aSecure,SameSite=Strict,HttpOnlycookie carrying an HMAC-signed, expiring token (newADMIN_SESSION_SECRET); the clear PIN is no longer stored in the browser or sent on every admin request. Admin endpoints authenticate the cookie and fail closed (500) if the signing secret is unconfigured. Changing the PIN now requires both an active session and the current PIN. Added a/api/admin/logoutendpoint and an Abmelden action in the admin sidebar.
- The automatic monthly report now only sends closed periods, in Europe/Berlin time. Previously the cron reported the current, still-open month and evaluated the send day in the server's timezone — a configured send day fired mid-month against an incomplete period. The report now always covers the previous, fully-closed month, the send day is the day of the following month it goes out (evaluated in Europe/Berlin), and a missed day is retried automatically on the next nightly run (idempotent per month, no double-send).
- The automatic-send day picker now reflects the new model: the chosen day is the day of the following month on which the closed month is sent, and the default ("So früh wie möglich") is the 1st.
- Set
ADMIN_SESSION_SECRET(a long random value) in Vercel before/with this deploy — admin endpoints fail closed without it. No database migration is required. Any admin logged in before the deploy is signed out once and re-enters their PIN.
- Admin PIN is no longer practically brute-forceable. Rate limiting is now durable and cross-instance (a shared
auth_throttletable +pin_rate_consume/pin_rate_resetRPCs, migration 020) instead of per-serverless-instance memory, and it is applied on every PIN-verifying endpoint via a singlerequireAdminhelper — previously only/verify-pinwas throttled while/api/admin/data,settings,theme,preview-reportandsend-reporthad none. A correct PIN resets the counter so honest admins aren't progressively slowed. The reset/recovery endpoints (request-pin-reset,reset-pin,change-pin) are throttled too. - Removed the fail-open authentication path. A transient database error while checking whether a PIN is set now throws (fails closed) instead of silently re-enabling the
ADMIN_PINenv bootstrap. - PIN-reset codes now use a cryptographically-secure RNG (
crypto.randomInt) instead ofMath.random(), which is predictable and unsuitable for a security token.
- The admin session still uses the PIN sent per request (held in
sessionStorage) rather than an HttpOnly, Secure, SameSite session cookie. Resolved infeat/v1-gate-scheduling-session(see the top of this changelog).
- CSV export is no longer vulnerable to spreadsheet formula injection. Cells beginning with
=,+,-,@, or a leading control character (which Excel/Sheets can execute as formulas when the CSV is opened) are now prefixed with an apostrophe so they render as text. - Added HTTP security headers (Vercel): a
Content-Security-Policy(default-src 'self',frame-ancestors 'none',object-src 'none', restrictedconnect-src/script-src/style-src, with Google Fonts allow-listed), plusX-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy,Permissions-Policy, and an explicitStrict-Transport-Security. All/api/*responses now sendCache-Control: no-store.
- Fixed a stored-HTML/script injection in the PDF report (Critical). User-controlled member, company and item names were inlined unescaped into the report HTML, which Puppeteer then rendered — a crafted name could inject markup/script and, because Chromium ran with web security relaxed, exfiltrate data or make server-side requests. All names are now HTML-escaped, the PDF is rendered with JavaScript disabled and all network requests blocked,
--disable-web-securityis dropped from the Chromium launch, and the report document carries a strictContent-Security-Policy(default-src 'none'). Defense in depth: even markup that slipped through could neither execute nor phone home.
- Report emails no longer report failure as success. The company report and each member statement now check Resend's
{ error }result (the SDK does not throw on API errors). A failed company send aborts the run before archiving/pruning, so a report that wasn't delivered can't quietly wipe the month's data. Member-statement failures are counted and surfaced instead of silently swallowed. - The monthly prune can no longer delete unreported data. Old live transactions are removed only if they were actually archived (new
prune_reported_transactionsRPC filters on existence intransactions_archive), so a month that was never reported is retained rather than permanently lost. - Duplicate reports on retry are prevented. A new
report_runsledger (migration 019) records each month's run; a completed month is skipped on re-invocation and a concurrent second invocation is locked out. Resend idempotency keys dedupe individual emails within a run. A manual admin send explicitly forces a re-send (with a fresh key) while still respecting the concurrency lock. - A PDF-generation failure no longer sinks the whole report. If the PDF can't be built, the report still goes out with the Excel attachment / email only, instead of aborting.
- Apply migration 019 (report_runs table + prune RPC) before/with this deploy — it's additive and safe to apply anytime, but the report run needs it.
- The public (anon) Supabase key can no longer read personal data or write catalogue data. Previously the key shipped in the browser bundle had table-wide
SELECTon members (includingwork_email) and transactions, plusINSERT/UPDATEon companies, members and items — so anyone with the key could read every member's work email and every transaction and corrupt the catalogue. Migration 015 revokes all of that. The anonymous member flow keeps only what it needs: reading active companies/items, reading the non-PII columns (id, name, company_id, active) of active members, and inserting its own member row and transactions. - All admin data access now runs server-side through a PIN-protected, service-role API (
/api/admin/data). The admin dashboard and the Companies/Items/Members pages no longer query Supabase with the anon key; reads (including work emails) and all catalogue writes are authenticated server-side. Admin writes are now validated on the server (name/price/category/email bounds, company existence). This makes the privacy notice's "only campus admins have access" statement actually enforced by the database. - Anonymous writes are now validated server-side. Logging and self-registration go through
SECURITY DEFINERRPCs (log_order,register_member; migration 017) instead of raw table inserts. The server derives the company and timestamp (no longer trusted from the client), rejects unknown/inactive members and items, bounds each quantity, and enforces the per-order cap — so quantities/timestamps/relationships can no longer be forged and inactive items can't be charged. Migration 018 then removes the direct anonINSERTgrants, leaving the RPCs as the only write path.
- Duplicate orders via „Bestätigen“ → „Rückgängig“ → „Bestätigen“. The success screen's undo returned to the confirm step but the order had already been written, so re-confirming logged it a second time. „Bestätigen“ now writes the order once through the validated RPC and only then shows „Gespeichert“ (so success is never shown before the data is saved), and „Rückgängig“ genuinely deletes the just-logged rows via
undo_order. Re-confirming after an undo therefore can't duplicate.
- Configurable per-order item limit. Admins can cap how many items a single person may log in one order (Settings → Bestellung → „Max. Artikel pro Bestellung“; leave empty for unlimited). Enforced both in the member flow (with a notice when reached) and server-side in
log_order.
- Anonymous request rate limiting for the write RPCs is not yet in place (validation is, abuse-volume throttling isn't).
- Admin auth hardening (durable rate limiting, HttpOnly session instead of the PIN in
sessionStorage, removing the fail-open bootstrap andMath.random()recovery codes), report reliability (Resend error handling + idempotency + atomic archive/prune), PDF HTML escaping/sandboxing, and dependency upgrades remain open v1.0 gate items.
- New „Imperium (Death Star)" brand palette (Settings → Erscheinungsbild → Marken-Palette): a cold imperial-steel accent for the Death Star cafeteria. Selecting it swaps the landing-page illustration and the browser tab favicon to a Death Star mark and recolors the whole app (member flow + admin) with the imperial accent (issue #16).
- Send-day picker (
DayGridPicker) now shows a plain-language summary line ("Versand am 1. jedes Monats" / "…letzten Tag des Monats"), tactile rounded day cells with a solid-amber selected state, and a distinct "Letzter Tag des Monats" row with a calendar glyph and a confirmation check. - PIN entry (
PinInput) now has a blinking caret in the active slot, dots/digits that pop in as you type, a shake on a wrong PIN, and revealed reset-codes rendered in JetBrains Mono. - Template fields (
TemplateField) now highlight{platzhalter}tokens live inside the field as amber chips, show the raw token on each insert-chip, and render the "Beispiel" preview in an elevated card with the substituted sample values highlighted. - JetBrains Mono is now loaded for monospaced/numeric text (revealed PINs, code columns) — matching the design system's
--font-monostack.
- The
/datenschutzpage is now more comprehensive and precise for the production deployment: the Arbeits-E-Mail is correctly described as required (was "freiwillig"), matching the mandatory self-registration field. - Added sections for the verantwortliche Stelle (ITC1 GmbH), Empfänger & Auftragsverarbeiter (Supabase, Vercel, Resend, incl. non-EU transfer / SCC note), and full Betroffenenrechte (Art. 15–21 + Beschwerde beim BayLDA).
- Corrected the localStorage disclosure (last-used company and light/dark preference) and clarified stammdaten vs. 90-day archive retention. Stand updated to Juli 2026.
- PIN reset now recognizes server admins from
ADMIN_EMAILeven after custom report recipients are configured, so those admins still receive their one-time recovery code. - "Namen hinzufügen" (member self-registration) inputs were white in dark mode — the Vorname/Nachname/Arbeits-E-Mail fields had no background and fell back to the browser default. They now use the themed inset (
bg-surface-2, focus lifts tobg-surface) like the other form fields.
- Member self-registration ("Namen hinzufügen") now requires Vorname, Nachname and Arbeits-E-Mail (with email-format validation) — matching the admin-side member form, so every self-added member is reachable for the monthly statement.
- The whole app now supports Light / Dark / System. Colors are driven by semantic CSS-variable tokens (
--bg,--surface,--fg,--accent, …) mapped into Tailwind; aThemeProviderresolves the mode, persists a per-device choice, and follows the OS in "System". - Appearance mode is a single global admin setting (Settings → Erscheinungsbild → Standard-Modus): „Hell"/„Dunkel" force that appearance for everyone (members and admins); „System" follows each device's OS. There is no per-device switcher.
- The app logo and the landing/empty illustrations now recolor with the theme (inlined as
currentColorSVGs instead of static<img>s). - Admin "Erscheinungsbild" settings: set the app's default mode (Hell/Dunkel/System) and pick a brand palette — three presets (Standard-Amber, ITC1, Wald) plus three custom palettes, each with its own light and dark accent colour. Selecting/editing previews instantly; "Speichern" applies it app-wide (member flow + admin) for everyone. Stored in a new public
app_themetable. - Report and statement emails now use the brand-palette accent (light variant — email dark-mode is unreliable). The separate accent picker in "Berichts-Format" is removed; the email accent follows the palette under "Erscheinungsbild".
- The browser tab favicon and theme-colour now follow the active brand palette (generated at runtime from the accent). The installed PWA home-screen icon stays static.
- Light mode is visually unchanged — the light token values equal the previous stone/amber palette.
- Send day is now a calendar-style grid (1–28 + „Letzter Tag des Monats") instead of a dropdown.
- Subject/intro fields gained placeholder chips and a live example: click
{monat}/{jahr}/{name}/{gesamt}to insert them at the cursor, with a "Beispiel:" line showing the resolved text as you type. New{jahr}and{gesamt}placeholders (member total) are supported. - Fixed: report previews (and a real send with a blank field) no longer show the literal word "null" — an empty subject/intro now correctly falls back to the built-in default.
- Self-service PIN reset is now on the
/adminlogin screen, not inside Settings. After 5 failed PIN attempts the keypad locks and the recovery flow opens (also reachable anytime via a "PIN vergessen?" link). - Recovery now asks for your admin email and sends the one-time code only to that address (if it's on the admin list), instead of broadcasting to all recipients — so a locked-out admin can regain access even when they didn't receive a shared PIN. The email instructs setting a fresh PIN immediately; entering the code + a new PIN logs you straight in.
- The
ADMIN_RECOVERY_PINserver backstop still works in the same code field when email is unavailable. - Removed the now-redundant "PIN zurücksetzen" flow from the Settings page (it keeps "PIN ändern" for logged-in changes).
- Automatic report schedule control — a Settings card to turn the month-end automatic send on/off and choose the send day (a specific 1.–28. or the last day of the month). The cron now runs daily and the function enforces the chosen day.
- Light report-format customization — accent colour, email subject and intro text (with
{monat}/{name}placeholders), and per-report attachment toggles (attach PDF and/or Excel to the company report), applied to both the company report and the member statement - Report preview — "Vorschau" opens the company report (admin + CEO) and the member statement rendered exactly as they'll be sent, reflecting your current unsaved edits; uses this month's real data or a small sample when the month is empty
- Report recipients now also show the
ADMIN_EMAILenv fallback as read-only "Server" chips so you can see who currently receives reports before configuring your own list
- Migration 013 —
app_settingsgainsauto_report_enabled,auto_report_day,report_accent,report_subject,report_intro,report_include_pdf,report_include_excel,member_subject,member_intro(all with safe defaults)
- Admin Settings page (replaces the placeholder): manage report recipients (add/remove with inline validation and an empty-state warning), the CEO/Geschäftsführung CC address and its toggle, and the per-member monthly-statement toggle — all saved together via a "Speichern" bar
- 6-digit admin PIN with self-service change & reset:
- "PIN ändern" — verify the current PIN and set a new 6-digit PIN (segmented PIN entry, confirm field)
- "PIN zurücksetzen" — two-step flow: email a one-time code to the report recipients + CEO, or use the server-side emergency recovery PIN, then set a new PIN
- The login keypad now renders the PIN length reported by the server (6 by default) instead of a fixed 4 digits
- CEO copied on every monthly report — the configured CEO address is CC'd on both the manual send and the month-end cron when the toggle is on
- Per-member monthly statement email — each person who consumed that month and has a work e-mail receives their own warm, itemized statement (date, item, quantity, unit price, amount, total) in addition to the company report; can be turned off in Settings
- Report recipients now come from the Settings page (
app_settings.report_recipients);ADMIN_EMAILis used only as a bootstrap fallback when the list is empty /api/send-reportand/api/admin/verify-pinnow authenticate against the hashed PIN in the database, falling back toADMIN_PINonly until a PIN is set from the dashboard
- Migration 012 —
pgcrypto-backed, service-role-only functions for verifying/setting the PIN and for issuing/consuming one-time PIN-reset codes (hashes only; clear PIN/codes are never stored)
- Mandatory fields when adding a member: Vorname, Nachname, Arbeits-E-Mail and Unternehmen are now all required (with email-format validation), so every member is reachable for the upcoming per-member monthly statement
docs/phase-2-production.md— the plan for moving off the hackathon demo onto an ITC1 production deployment (data reset, 6-digit PIN management, CEO report CC, per-member statements, expanded admin settings), with a migration runbook
app_settingstable (migration 010) — single-row admin config: hashed 6-digit PIN, email-based PIN reset token, report recipients, CEO CC, and feature toggles; service-role onlymembers.work_emailbecomes mandatory (migration 011, guarded — apply after the demo data is cleared)supabase/maintenance/clear_demo_data.sqlto wipe demo data andsupabase/seed_production.template.sqlto seed the real campus data (run manually)
- Admin form primitives so the design system covers inputs (previously every form used ad-hoc markup):
AdminField— labelled text/number/email input withformand compactfiltervariants, optional leading icon, hint/error text, and a required-field asteriskAdminSelect— labelled select with the sameform/filtervariants andoptionspropToggle— accessible on/off switch (replaces the bare active/inactive checkboxes)Toast— extracted the bottom-right notification into a reusable component (success/error)EmptyState— extracted the empty-table illustration block into a reusable component
- Admin pages (Übersicht/Einträge log, Unternehmen, Mitarbeitende, Items) now build their filter bars and modal forms from the new primitives — visually unchanged, with active/inactive now shown as a toggle and required fields marked with an asterisk
apps/web/public/logo.svg: amber rounded-square app icon built from the same cappuccino-with-steam paths (white stroke) — used only where a solid-background icon is required (favicon, PWA install)tools/generate-icons.mjs: renderslogo.svgvia Puppeteer + local Chrome to produce correctly sized PWA PNGs
favicon.svg: replaced ☕ emoji with the proper amber-square cappuccino logo markpwa-192x192.png/pwa-512x512.png: regenerated fromlogo.svgindex.html: added<link rel="apple-touch-icon">pointing atpwa-192x192.pngfor iOS home-screen installsvite.config.ts: fixedincludeAssetsto reference files that actually exist on disk- PDF report header: cappuccino-with-steam SVG paths inlined directly in the amber band with
stroke="white"— the illustration appears as-is beside the "Kaffeelisten" wordmark, no square background - Email HTML: cappuccino-with-steam SVG embedded as a
data:image/svg+xml;base64<img>in the email body (stone-700 stroke on white background, above the summary table); SVG inline tags are stripped by major email clients so base64<img>is used instead
/datenschutz— German GDPR notice page explaining what data is collected, why, retention period (90 days), who has access (admin only), how to request deletion, and confirmation that no tracking cookies or analytics are used- Start screen footer: "Datenschutz" link opens
/datenschutz; built on the sameFlowShellwrapper with amber accent styling and a back-to-home chevron
supabase/seeds/002_demo_data.sql: full ITC1 Deggendorf demo dataset- 28 companies: 20 Gewerbepark established tenants (4process AG, ADLINK Technology, B-plus GmbH, fivefingergames, Level51, Medtronic, TÜV NORD Diagnostics, etc.) + 8 Gründerzentrum startups (Career Captain, Quimedo, MOVEMASTER, The Blockchain Academy, etc.)
- 239 members: 10–12 per established company, 2–5 per startup; realistic German names with
vorname.nachname@company-domainwork emails - 12 items: Filterkaffee, Espresso, Cappuccino, Latte Macchiato, Wasser, Cola, Radler, Apfelsaft, Donut, Croissant, Brezel, Müsliriegel — with correct categories and prices
- 391 transactions: May 2026 (7 working days); 78 % member attendance; first transaction per member always coffee; morning-biased timestamps
- Company selector: colored letter-band chevrons for faster visual scanning when scrolling through a long company list
- "Meine Firma" shortcut: appears on the start/company screen when 90 %+ of recent on-device selections (localStorage) are the same company — one tap skips the company step entirely
- TÜV NORD Diagnostics GmbH: corrected garbled
Üencoding in the database (was stored as mojibake from the initial seed run)
deactivateInactiveMembers()in_lib/report.ts: after each monthly report, soft-deactivates any member whose last transaction is older than 90 days; brand-new members with zero transactions are never auto-deactivated- Both cleanup jobs (
pruneOldTransactions+deactivateInactiveMembers) are called automatically byrunMonthlyReport()after each report send
pruneOldTransactions()now also prunestransactions_archiveto the same 90-day rolling window, keeping Supabase free-tier storage in check
work_emailfield on thememberstable (nullable text, Supabase migration 007)- Self-registration modal: optional "Arbeits-E-Mail" input stored on member creation; never shown in the member flow after registration
- Admin Members add/edit modal: optional "Arbeits-E-Mail" input for admin-managed members
- Excel report "Pro Unternehmen" sheet: new "E-Mail" column between Person and Einträge
- Excel report "Alle Einträge" sheet: new "E-Mail" column between Person and Unternehmen
- Admin CSV export: new "E-Mail" column between Person and Unternehmen
feat/send-report-pdf-mobile (PR #7)
/api/send-report— PIN-protected POST endpoint; acceptsmonthbody param; generates PDF + Excel, sends both via Resend, archives totransactions_archive/api/cron/monthly-report— Vercel Cron wrapper (schedule0 22 28-31 * *); verifiesAuthorization: Bearer {CRON_SECRET}; last-day-of-month guard prevents false fires- PDF report: flat amber-600 header, white KPI strip (entries, total, consumers, companies), company overview table, per-company member breakdown; generated via puppeteer +
@sparticuz/chromium-min - Excel report (
.xlsx, 3 sheets):Zusammenfassung(company totals),Pro Unternehmen(member breakdown with subtotals),Alle Einträge(full log); brand-styled headers viaexceljs - Email body includes summary metrics and per-company breakdown; attachments named
kaffeelisten-YYYY-MM.pdf/.xlsx - Admin month selector: filters all dashboard views (log, summary cards, CSV export) to the selected month; manual report trigger sends for the selected month; cron always uses current month
- Data retention: transactions are no longer deleted after a report send;
transactionstable keeps a rolling 3-month window;pruneOldTransactions()removes rows older than 3 months after each report
/api/send-reportwas publicly accessible; now requiresx-admin-pinheader matchingADMIN_PINenv var- Vercel Cron was checking
x-cron-secretbut Vercel sendsAuthorization: Bearer; corrected - Excel headers were unstyled because
eachCellskips not-yet-created cells; switched to explicitaddRow()+ index-based styling - Excel file triggered "repair" dialog in desktop Excel due to missing
bgColoron fill definitions; added - Replaced
xlsx(SheetJS community — no cell styling) withexceljsfor brand-styled workbooks - Member item grid collapses to 1 col mobile / 2 col tablet; member name grid starts at 1 col on mobile
- FlowShell padding reduced on narrow screens; start screen hero and SuccessScreen scale down on mobile
feat/admin-crud (PR #6)
- Admin Items page: full table (name, category, unit, price, status badge) with add/edit modal and toggle-active; name search, category filter, status filter, sort by name/price/category
- Admin Companies page: full table with add/edit modal and toggle-active; status filter, name A→Z/Z→A sort
- Admin Members page: full table with company join, add/edit modal, toggle-active; name search, company filter, status filter, sort by name or company
- Log page: client-side filter bar — company dropdown, name search, item dropdown, date sort direction toggle; results count shown live
- Log page: CSV export — UTF-8 BOM CSV, semicolon-delimited, German decimal format
- Sidebar: distinct icons for Unternehmen (building) and Mitarbeitende (users) — previously both used the report icon
- Supabase migration 005: anon
INSERT/UPDATEgrants and RLS policies for companies, members, items
- Admin layout: sidebar fixed/non-scrolling; content pane scrolls independently; topbar stays sticky at top of content area
- Member self-registration stores full name (e.g. "Anna Müller") in DB; abbreviated form ("Anna M.") computed on-the-fly for display only
feat/implement-design (PR #4)
- Full member-facing UI from design bundle: warm stone/amber palette, Bavarian motifs, SVG illustrations
- PWA manifest with correct
lang: "de", theme colour, and icon set (192×192, 512×512, maskable) - Favicon and apple-touch-icon in
apps/web/public/ apps/web/vercel.jsonSPA rewrite rule — fixes 404 on hard refresh to any non-root route- Admin dashboard: transaction log table, company-level summary cards, PIN keypad
- Supabase migration 002: explicit
GRANT SELECT/INSERTfor anon and authenticated roles (PostgREST requires this in addition to RLS policies) - Supabase migration 003:
anon_read_transactionsSELECT policy — admin dashboard was always showing zero entries without it - Supabase migration 004:
anon INSERTon members +anon_insert_membersRLS policy — required for member self-registration - Member self-registration in the name-selection step: inline modal, name standardised as "Vorname N." with automatic disambiguation to more letters when the same initial already exists in the company roster
- Multi-item cart in the item step: each
ItemCardhas inline−/+quantity controls; all items are batch-inserted as separate transaction rows on confirm - PRD open question 2 resolved: members log multiple items per session
- Back button on the item step now clears cart and resets member selection — previously left stale state that caused the confirm screen to render incorrectly
- ESLint config (
apps/web/.eslintrc.cjs) was deleted by the design-implementation commit; restored to fixnpm run lintin CI - PWA manifest
langdefaulted to"en"; set to"de"to suppress browser auto-translation
- Initial repo scaffold with Vite + React + TypeScript + Tailwind
- npm workspaces monorepo — root
package.jsonproxiesdev,build,lint,typechecktoapps/web - Supabase schema: companies, members, items, transactions, transactions_archive
- Row-Level Security policies and GRANT statements for anon/authenticated roles
- Dev seed data for ITC1 campus (companies, members, items)
- Member-facing logging flow: start → company → member → item → confirm → success (auto-reset 3s)
- Admin panel at
/adminwith server-side PIN check (Vercel serverless function) - Monthly report email via Resend, triggered manually or by cron
- Vercel Cron Job firing on last day of month at 23:00 CET
- PWA manifest and Workbox service worker (offline shell caching)
- GitHub Actions CI: lint + typecheck on every PR and push
- Monthly report cron workflow in
.github/workflows/ - GitHub issue templates
CLAUDE.mdagent context filedocs/prd.md,docs/design-foundation.md,docs/design-system.md,docs/roadmap.md