feat(auth,security,scheduling): JWT revocation, S3 fix, notes validation, docs, tests

- JWT revocation with JTI (UUID v4): in-memory tracking, POST /api/logout,
  refresh handler revokes old JTI, RequireAuth rejects revoked tokens
- Fix extractKey for S3 portfolio deletion: extracts full key path from URLs
  instead of just filename, preventing orphaned storage files
- Notes validation: max=1000000 on all 13 Notes fields across 4 booking structs
- CharCounter: grapheme-aware counter (Intl.Segmenter), threshold 750K,
  color-coded, integrated into 6 booking/admin components
- loginInProgress: timestamp-based tracking, 30s staleness, 20-entry cap (429),
  ticker cleanup for stuck entries
- Profile picture 15MB client-side limit, portfolio 20MB backend limit
- Exceptional scheduling: expand query start to Monday of week
- TodayCalendar: week-range fetching, closing time indicator, short-day lunch skip
- NavBar: link reorder, mobile burger badge, slide transition, backdrop
- ImageUpload: 20MB limit with visual feedback
- formatDateISO: shared YYYY-MM-DD utility, shouldApplyLunchProtection helper
- Update README.md and all Obsidian docs (Overview, Technical, Admin, Future Work)
- Add 28 new tests: JWT (11), auth handlers (7), portfolio extractKey (5),
  notes validation (5). go build + go vet clean with test,dev tags
This commit is contained in:
2026-06-03 11:17:41 +01:00
parent 169d7dc6e3
commit bffb984ebb
30 changed files with 1016 additions and 62 deletions
@@ -18,6 +18,7 @@
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
import CharCounter from '$lib/components/ui/CharCounter.svelte';
interface Props {
open: boolean;
@@ -893,6 +894,7 @@
placeholder="Tell us why you need to make changes"
rows={2}
/>
<CharCounter text={notes} />
</div>
{:else if editMode === 'services'}
<!-- ─── Services Only (with time restriction) ── -->
@@ -1018,6 +1020,7 @@
placeholder="Any special requests or notes for your appointment"
rows={2}
/>
<CharCounter text={notes} />
</div>
{:else if editMode === 'both-services'}
<!-- ─── Both Step 1: Service Selection (no time restriction) ── -->
@@ -1128,6 +1131,7 @@
placeholder="Any special requests or notes for your appointment"
rows={2}
/>
<CharCounter text={notes} />
</div>
{/if}
{/if}