Commit graph

2462 commits

Author SHA1 Message Date
Till JS
18a94b9266 feat(manacore/web): clickable cross-module links with overlay stacking
Clicking a linked item in a DetailView opens the target app's DetailView
as a stacked overlay on top of the current one. Supports:
- Cross-app navigation (e.g. click linked event from todo detail)
- Back button to return to previous overlay
- Correct app color + title in overlay header
- Overlay stack with goBack() popping the top frame
- Added paramKey to EntityDescriptor for correct ID mapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:36:40 +02:00
Till JS
5828f60934 feat(manacore/web): add habits module with tally board, inline create, and detail view
New habit tracking module: define habits (emoji, color, daily target), tap to log with timestamp, view streaks and 7-day charts. Includes workbench ListView with inline creation, full-page detail view, and drag/drop entity integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:34:07 +02:00
Till JS
0af8c7cec6 fix(ui): make all drag bar icons always visible (not hover-only)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:30:20 +02:00
Till JS
339e3d344f feat(ui): move window controls (minimize, maximize, close) into drag bar
Window buttons now live in the drag handle bar alongside the move arrows,
appearing on hover. Header row is simplified to just icon + title.

Layout: ◀ ···drag··· [−] [□] [✕] ▶
All buttons appear on hover, close button has red hover state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:28:37 +02:00
Till JS
ad9bbec393 feat(ui): add left/right arrow buttons to PageShell drag bar
Arrow buttons appear on hover at the left/right edges of the drag bar
to quickly reorder pages without dragging. Hidden when the page is
already first (no left arrow) or last (no right arrow).

- PageShell: onMoveLeft/onMoveRight props, CaretLeft/CaretRight icons
- AppPage: pass through move callbacks
- +page.svelte: handleMoveLeft/handleMoveRight with array swap + persist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:25:51 +02:00
Till JS
2529c91d58 feat(manacore/web): show item title + app color in drag preview
DragPreview now accepts a resolveEntity callback that maps drag type
+ data to display info (title, app color, app name). Dragging a task
shows "Meeting mit Team · Todo" instead of just "task".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:24:58 +02:00
Till JS
a60799ddff fix(ui): reduce drag handle bar height
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:21:46 +02:00
Till JS
e8a4b984f3 fix(ui): full-width draggable header bar on PageShell
- Drag handle bar spans full width (not just the icon)
- Entire bar is draggable with grab cursor
- Subtle background color differentiates it from content (border-bottom)
- Hover/active states for visual feedback
- DotsSixVertical icon rotated 90° (horizontal grip lines)
- Dark mode support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:20:53 +02:00
Till JS
d7b146a186 fix(ui): add top padding to workbench on desktop
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:17:20 +02:00
Till JS
cf03743bae fix(ui): add left scroll offset to PageCarousel
First panel no longer sticks to the left edge. Left padding set to
calc(50vw - 240px) so the carousel starts with generous breathing room.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:16:20 +02:00
Till JS
ec7c563283 fix: remove stale references to deleted packages (shared-auth-stores, shared-profile-ui, shared-app-onboarding)
- Dockerfile.sveltekit-base: remove COPY lines for 3 deleted packages
- CI workflow: remove shared-profile-ui from SHARED_WEB_PATTERN
- manavoxel package.json: remove shared-auth-stores dependency
- uload CLAUDE.md: update auth store reference to shared-auth-ui
- APP_ONBOARDING.md: update package path to shared-ui/onboarding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:15:58 +02:00
Till JS
a15b027e96 fix(shared-ui): block click event after drag to prevent detail view opening
After a successful drag-and-drop, the browser fires a click event on
the source element. This was opening the detail view overlay instead of
completing the drop. Now a one-time click blocker is added after drag
ends to swallow the spurious click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:15:53 +02:00
Till JS
9b8814ea37 fix(ui): make homepage PageCarousel full-width (no side padding)
Break out of the layout's max-w-7xl px-4 container using negative
margins so the workbench carousel fills the entire viewport width.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:14:08 +02:00
Till JS
1bd001ec9a fix(manacore/web): restrict page drag to handle only, allow item DnD
Page reorder drag now only starts from the drag-handle icon, not from
anywhere in the page. This allows dragSource on list items (tasks,
events, contacts) to work without being intercepted by page reorder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:11:58 +02:00
Till JS
f819b24596 fix: revert guestMode $state() — caused effect_update_depth_exceeded
Making guestMode reactive with $state() triggered an infinite effect
cascade: guestMode.notifications being accessed in the template created
a reactive dependency that, combined with 8+ AppPages each running
$effect for module loading, exceeded Svelte's effect update depth limit.

Root cause: GuestMode object has reactive getters that return new
references on each access. Wrapping it in $state() made every template
access trigger a re-render, which triggered more effects, creating an
unbounded cascade.

Fix: Keep guestMode as plain variable (non-reactive). The warning about
non_reactive_update is acceptable — guestMode is set once during init
and its properties are accessed imperatively, not reactively.

Also:
- Remove debug console.logs from AppPage, +page, +layout
- Keep onMount for workbench state loading (replaces $effect)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:10:21 +02:00
Till JS
6ced238571 chore: delete 25 web-archived directories, remove stale stubs, clean workspace config
- Delete all 25 apps/*/apps/web-archived/ directories (superseded by unified ManaCore app)
- Remove stale +page.server.ts stubs from teams, organizations, settings (always returned empty data)
- Simplify teams and organizations pages to static empty-state (no server load dependency)
- Delete empty apps/context/apps/mobile/components/variants/index.ts
- Remove commented-out apps-archived entries from pnpm-workspace.yaml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:03:49 +02:00
Till JS
e1077e261f fix(manacore/web): fix entity registration hang + registry type errors
- Make entity registration lazy (ensureEntitiesRegistered) to avoid
  circular imports at module load time
- Re-enable cross-module drop target in AppPage
- Fix Component type in app-registry to accept any props (AnyComponent)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:57:59 +02:00
Till JS
d8ce4eaf34 refactor: consolidate codebase — remove archived code, deduplicate packages, standardize middleware
- Delete 17 server-archived/ directories (consolidated into apps/api/)
- Delete apps-archived/ (clock, wisekeep) and services-archived/ (it-landing, ollama-metrics-proxy)
- Fix type safety: replace all `any` casts in cross-app-queries.ts with proper Local* types
- Remove duplicate shared-auth-stores package (identical copy of shared-auth-ui/stores/)
- Remove duplicate theme store from shared-stores (canonical version in shared-theme)
- Migrate memoro-server rate-limiter to shared-hono/rateLimitMiddleware
- Migrate uload-server JWT auth + error handler to shared-hono (authMiddleware, errorHandler)
- Migrate arcade-server error handling to shared-hono
- Merge shared-profile-ui and shared-app-onboarding into shared-ui
- Unify /clock route into /times/clock, remove redirect stubs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:55:58 +02:00
Till JS
7ee57b7afd feat(manacore/web): add entity descriptor system with cross-module drag-and-drop
Introduce EntityDescriptor pattern: each module declares how its items
can be displayed, dragged, dropped, and created from other modules.

- Entity types + registry with executeDrop orchestration
- Entity descriptors for todo, calendar, contacts
- List items are now draggable (dragSource) across pages
- Pages accept cross-module drops (event→todo, contact→calendar, etc.)
- Drops create new items + bidirectional manaLinks via shared-links
- LinkedItems component shows cross-module links in DetailViews
- Visual feedback: page glows purple on valid hover, green on success

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:42:38 +02:00
Till JS
d73591865c fix: move bottomChromeHeight after isTagStripVisible declaration
$derived cannot reference variables declared later in the script.
Moved the calculation below the isTagStripVisible $state declaration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:39:14 +02:00
Till JS
f0d5ba2128 fix: allow localhost in CSP connect-src during development
Dev env vars (_CLIENT suffixed) are empty, so localhost:3001 (auth),
localhost:3050 (sync), localhost:3060 (api) were blocked by CSP.

Added http://localhost:* and ws://localhost:* to connect-src when
NODE_ENV !== 'production'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:37:13 +02:00
Till JS
d368bd34b5 fix: replace bind:clientHeight with calculated bottom chrome height
bind:clientHeight + $effect setting CSS variable caused infinite reflow
loop: height change → CSS var → padding change → height change.

Now using $derived calculation from state (isCollapsed, isTagStripVisible)
instead of DOM measurement. No reflow loop possible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:34:42 +02:00
Till JS
9534d29967 fix: revert client sync from per-app SSE to HTTP polling
Per-app SSE connections exhaust the browser's 6-connection-per-origin
limit, causing the app to hang on load. Reverted to HTTP polling for
both eager and lazy apps.

SSE server endpoint remains available for future use as a single unified
stream (one connection for all apps, similar to the old unified WebSocket).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:31:53 +02:00
Till JS
81d5e83861 fix: revert @const to svelte:component (invalid placement in div)
@const can only be used inside {#if}, {#each}, etc. — not directly in
a <div>. Reverted ActionZone and AuthGateModal back to <svelte:component>
which works correctly (the deprecation warning is less important than
a broken app).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:04:18 +02:00
Till JS
c21793baaf fix: resolve all 40 Svelte dev warnings for clean startup
- Add $state() to 4 reactive variables (guestMode, emailInput, passwordInput, searchInputElement)
- Replace 3 deprecated <svelte:component> with direct component references
- Fix 8 a11y issues: add ARIA roles, tabindex, keyboard handlers to click-handler divs
- Remove 22 unused CSS selectors across 8 shared-ui components

Zero warnings on dev startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:01:17 +02:00
Till JS
2bd8f0babf fix: change unified API default port from 3050 to 3060
Port 3050 is used by mana-sync. The unified API server gets its own port.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:54:07 +02:00
Till JS
1245fdc077 feat(manacore/web): enhance Times & Zitare pages, add DetailViews, clean up homepage
- Times ListView: inline timer with start/stop, name input, live elapsed display
- Times DetailView: editable entry with duration (h/m/s), project, billable, delete
- Zitare ListView: tap-to-cycle quotes, inline fav button, tag drag-and-drop support
- Zitare DetailView: full quote view with category, author bio, share, favorite
- Homepage: remove tag bar (available via PillNav), carousel uses full width
- Register both DetailViews in app-registry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:52:27 +02:00
Till JS
c5906e4c29 fix: update all dev scripts to use unified API server
Replace 15 individual dev:*:server scripts with single dev:api command.
Update all dev:*:full, dev:*:app, dev:*:local scripts to start the
unified API server (apps/api) instead of archived individual servers.

dev:manacore:servers now starts: auth + sync + api (3 processes instead of 8)
dev:manacore:full now works without errors.

Removed: wisekeep scripts (project archived), broken server references.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:51:46 +02:00
Till JS
9966e9edeb fix(ui): remove Observatory, API Keys, Gifts from PillNav
These are secondary pages accessible via settings/profile, not primary navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:40:32 +02:00
Till JS
f7ee9ead44 fix(branding): rename ManaContacts to Kontakte
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:38:53 +02:00
Till JS
019f3eb9fd feat(manacore/web): show tags in detail views with click-to-remove
Display assigned tags as colored pills in todo, calendar, contacts
DetailViews. Clicking a tag pill removes it from the item. Hover turns
the X icon red for clear affordance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:37:41 +02:00
Till JS
794424d6ad fix(ui): open all AppDrawer apps in new tab
All app clicks in the PillNav app drawer now open in a new tab via
window.open('_blank'). Previously internal URLs used window.location.href
(same tab navigation) which was confusing in the unified app context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:36:26 +02:00
Till JS
0d142efa3c feat(manacore/web): show tags as labeled pills instead of dots
Replace tiny 6px tag dots with readable pills showing colored dot +
tag name. Uses color-mix for subtle tinted background per tag color.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:30:44 +02:00
Till JS
e9d4cbfdba fix(manacore/web): fix tag drag-and-drop — use reactive .value instead of .subscribe()
useAllTags() returns a Svelte 5 runes object with .value, not an
Observable with .subscribe(). Also fix getTagsByIds() calls to pass
the allTags array as first argument.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:26:22 +02:00
Till JS
976fb5fdf8 fix(ui): move nav toggle to right side of InputBar, make it larger
- Add rightAction snippet prop to QuickInputBar (InputBar.svelte)
- Move toggle from leftAction to rightAction (renders after submit button)
- Increase toggle size from 28px to 36px for better tap target

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:21:47 +02:00
Till JS
06ebc6271d feat(manacore/web): add tag drag-and-drop to workbench pages
- Mount DragPreview + draggable tag bar on workbench homepage
- Add dropTarget on list items in todo, calendar, contacts ListViews
- Show assigned tags as colored dots on list items
- Tags can be dragged from the tag bar onto any item to assign them
- Drop target highlights with module-colored outline on hover

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:20:54 +02:00
Till JS
fb5271acc8 fix(ui): move PillNav toggle inside QuickInputBar via leftAction snippet
Toggle button now renders inside the InputBar pill (left side) instead of
as a separate element next to it. Uses the existing leftAction snippet prop.

- Button is 28px circle, subtle background, no border/shadow (lives inside pill)
- Remove bottom-stack-row wrapper (no longer needed)
- Cleaner visual: toggle is part of the input bar, not floating beside it

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:15:29 +02:00
Till JS
81f781c133 fix(ui): move Tags to leftmost in PillNav, match toggle button to InputBar style
- Tags pill moved to first position in nav items (leftmost)
- Toggle button: 44px round (matches InputBar height), same border-radius
  (9999px), same backdrop-filter blur, same box-shadow, theme-aware colors
- Button sits directly next to QuickInputBar with no gap, vertically centered

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:08:24 +02:00
Till JS
b3dd8cded9 fix(ui): dynamic bottom-chrome-height for tabs, notifications, main content
The bottom-stack container now exports its height as --bottom-chrome-height
CSS custom property (via bind:clientHeight + $effect on :root). All elements
that need to position above the bottom chrome read this variable:

- PageCarousel minimized-tabs: bottom: var(--bottom-chrome-height)
- NotificationBar: bottom: var(--bottom-chrome-height)
- Main content padding: calc(var(--bottom-chrome-height) + 2rem)

This ensures tabs, notifications, and content never overlap with the
bottom stack regardless of PillNav collapse state or TagStrip visibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:59:16 +02:00
Till JS
b415567dfa refactor(ui): unified bottom-stack container for PillNav, QuickInput, TagStrip
Replace 4 independent position:fixed elements with one flex container that
stacks them naturally from bottom to top. Elements push each other
automatically — no more hardcoded offsets or z-index conflicts.

Stack order (bottom → top):
1. PillNavigation (collapsible)
2. TagStrip (togglable)
3. QuickInputBar + toggle button row

Shared-UI changes:
- PillNavigation: add positioning='fixed'|'static' prop
- QuickInputBar: add positioning='fixed'|'static' prop
- TagStrip: add positioning='fixed'|'static' prop
- All default to 'fixed' for backward compatibility

Layout changes:
- Wrap all bottom elements in .bottom-stack (position:fixed, flex-column)
- Remove hardcoded bottomOffset calculations
- Toggle button is now inline next to QuickInputBar (not separately positioned)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:52:40 +02:00
Till JS
bed2060ba5 fix(manacore/web): fix height resize using element's current height as start value
When heightPx is not yet set, use the element's actual offsetHeight
instead of 0 so the first drag correctly adjusts height.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:51:54 +02:00
Till JS
b66a26810f feat(manacore/web): add 2D resize (width + height) to workbench pages
Extend PageShell resize handle to support diagonal drag for both
width and height. Height is persisted per page in workbench settings.
Resize cursor changed from ew-resize to nwse-resize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:49:39 +02:00
Till JS
ef0c834a2b feat(ui): add PillNav toggle button next to QuickInputBar
Small floating button (▼/▲) at the bottom-right that toggles PillNav
visibility. QuickInputBar's bottomOffset adjusts dynamically when nav
is collapsed (70px → 12px), reclaiming vertical space.

- Button uses existing handleCollapsedChange() with localStorage persistence
- Smooth transitions on position and rotation
- Glass-morphism style (blur + semi-transparent background)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:45:43 +02:00
Till JS
9ea7e482f0 refactor(manacore/web): rename AppView → ListView across all 24 modules
Consistent naming: ListView.svelte + DetailView.svelte as a pair.
Update app-registry.ts and splitscreen/registry.ts imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:45:11 +02:00
Till JS
fed38efb8b fix(sync): fix SSE live updates — 2 bugs found during E2E testing
Bug 1: NotifyUser() early-returned when no WebSocket clients existed,
skipping SSE subscriber notifications entirely. Fixed by restructuring
to check WS clients and SSE subscribers independently.

Bug 2: SSE stream cursor defaulted to client's `since` parameter when
no initial data existed. If `since` was in the future (or very recent),
live updates had created_at < cursor and were silently filtered out.
Fixed by defaulting cursor to now() when no initial data is returned.

Bug 3: NotifyUser used original sseSubs slice instead of sseSubsCopy
after releasing the read lock (race condition).

Verified E2E: Push from client A → SSE stream on client B receives
live change event with correct data within ~1 second.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:39:46 +02:00
Till JS
4cb1bda852 feat(manacore/web): add overlay detail views for cards, storage, presi
Add inline-editable DetailViews with auto-save for:
- cards: deck details with color, description, public toggle
- storage: file details with rename, favorite, size/type info
- presi: presentation deck details with slide count

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 23:26:57 +02:00
Till JS
650dea5e1d feat(manacore/web): add overlay detail views for 8 more modules
Add inline-editable DetailViews with auto-save for:
planta, inventar, skilltree, memoro, questions, uload, mukke, citycorners

Wire AppView list items to open overlay via navigate() with sibling
navigation support. Fix citycorners table names (cityLocations→ccLocations).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:57:09 +02:00
Till JS
a08f1501f2 feat(manacore/web): add overlay detail views with inline editing, consolidate routes
- Remove /dashboard (redundant), merge /home into / (app root)
- Update all redirects and navigation references accordingly
- Add panel navigation stack with overlay detail views for workbench
- Implement inline-editable DetailViews for todo, calendar, contacts
- Auto-save on blur, prev/next navigation with sibling arrows
- Fix minimized tabs z-index behind quick input bar
- Fix showNewEvent undefined error in calendar AppView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:43:05 +02:00
Till JS
c8daa443fc feat(sync): replace WebSocket with SSE client for real-time sync
Client now connects to GET /sync/{appId}/stream via fetch + ReadableStream
instead of WebSocket + HTTP pull. Each app gets its own SSE connection that
delivers initial sync data + live updates in one persistent stream.

Changes:
- Remove WebSocket connection (connectUnifiedWs)
- Add connectSSE() per app using fetch + ReadableStream
- Parse SSE events (changes, heartbeat) from streamed response
- Auto-reconnect on disconnect with WS_RECONNECT_DELAY
- Fallback to polling if SSE endpoint not available
- ensureAppSynced() connects SSE for lazy apps on first visit
- handleOnline() reconnects all active SSE streams
- handleOffline() aborts all SSE connections

Benefits: 1 connection instead of 2 (WS + HTTP), data delivered instantly
without notification → pull round-trip, works through HTTP proxies/CDN.

Push (POST /sync/{appId}) remains unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:27:30 +02:00
Till JS
068a64b275 feat(sync): add SSE streaming endpoint for real-time sync
New endpoint GET /sync/{appId}/stream sends Server-Sent Events with
change data directly, replacing the WebSocket notification + HTTP pull
round-trip pattern.

Server (Go):
- HandleStream() in handler.go: SSE endpoint with initial sync + live streaming
- Hub.Subscribe()/Unsubscribe() in hub.go: channel-based SSE subscriber system
- Notification type for type-safe SSE events
- convertChanges() helper extracted from duplicated code
- WriteTimeout set to 0 for SSE long-lived connections

Protocol: Client connects to /sync/{appId}/stream?collections=a,b&since=...
Server sends initial changes, then streams live changes as other clients sync.
Heartbeat every 30s keeps connection alive. Push still uses POST /sync/{appId}.

WebSocket remains available as fallback (not removed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:24:10 +02:00