mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
Mirror of github.com/Memo-2023/mana-monorepo
Closes backlog C from the Phase 9 audit. The data layer has had
real field-level LWW since Sprint 1, but when the server's value
beat a local edit, the user had no way to know. This commit adds
the missing UI piece: a toast that appears whenever applyServerChanges
overwrites a non-empty local field with a strictly newer server
value, with a one-click "restore my version" path.
sync.ts — detection
-------------------
Two new exports:
- SyncConflictPayload: per-field overwrite event shape
(tableName, recordId, field, wasLocal, nowServer, localTime,
serverTime).
- subscribeSyncConflicts(listener): in-module pub/sub. Returns
an unsubscribe function.
Both LWW branches in applyServerChanges (insert-as-update and the
canonical update-with-fields path) now call notifyConflict() when:
1. The server time is STRICTLY greater (not equal) than the local
field time → there's actually an edit window to lose
2. The local field value is non-null/undefined → user actually
typed something to overwrite
3. The values are not equal (cheap JSON-string compare for objects,
=== for primitives) → there's a real change, not an idempotent
server replay
Why a custom registry instead of CustomEvent + window.dispatchEvent?
The existing sync-telemetry + quota-detect helpers use
window.dispatchEvent which doesn't work in node-based vitest envs
(no DOM EventTarget). The conflict bus is small enough that a plain
Set<listener> is simpler than polyfilling EventTarget — and the
node test path matters because we need automated coverage of the
detection logic.
conflict-store.svelte.ts — UI state
-----------------------------------
Svelte 5 $state-backed store with three responsibilities:
1. Coalescing: a SyncConflict is keyed by `${tableName}|${recordId}`,
so a burst of N field-overwrites on the same record collapses
into ONE toast with all affected fields underneath. The original
wasLocal value is preserved across coalescing (we don't clobber
the user's first typed value if a later field event arrives).
2. Auto-dismiss: each conflict has a 30s TTL after which it
evicts itself. Manual dismiss trumps the timer.
3. Restore: writes wasLocal back to Dexie with a fresh updatedAt
that beats the server's serverTime, plus a __fieldTimestamps
patch so the field-LWW pass on the next sync round will let
our value win. Deferred via setTimeout(0) so it lands AFTER
applyServerChanges releases its per-table apply lock — running
before the lock release would silently drop the restore (the
hook suppression is per-table-set, not per-record).
FIFO eviction at MAX_VISIBLE=8 keeps a bursty server from growing
the visible array unbounded.
SyncConflictToast.svelte — the UI
---------------------------------
Mounts globally in +layout.svelte. Stacks bottom-right above the
OfflineIndicator. Each toast shows:
- Module label ("Aufgabe", "Notiz", "Termin", …) derived from a
table-name → German label map. Unknown tables fall through to
the bare table name.
- Field count summary ("Feld »title«" / "3 Felder") — we
deliberately do NOT render the actual values because some are
encrypted blobs and decrypting them in the toast would be
significant complexity for marginal UX gain. The user knows
what they were just editing.
- Two buttons: "Wiederherstellen" (calls conflictStore.restore)
and "Behalten" (calls dismiss).
Slide-in animation, dark-mode-aware styling, role="alertdialog"
for accessibility.
Wiring
------
data-layer-listeners.ts:
- Imports installConflictListener from conflict-store
- Calls it from installDataLayerListeners() right after the
quota + telemetry handlers
- Adds the disposeConflict() call to the cleanup return
+layout.svelte:
- Imports SyncConflictToast and mounts it next to SuggestionToast
so it inherits the same global-overlay positioning context
Tests
-----
Five new integration tests in sync.test.ts cover:
- Fires when server overwrites a non-empty local field with a
strictly newer value
- Does NOT fire when local field is null/undefined (no edit to lose)
- Does NOT fire when values are equal (idempotent replay)
- Fires once per overwritten field on a multi-field update
- Does NOT fire on a timestamp tie (LWW lets server win silently
when there's no real edit window)
All 25 sync tests + 138 total data-layer tests pass. The new
captureConflicts() helper subscribes via subscribeSyncConflicts()
which works in the node-vitest env without needing a DOM polyfill.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| .changeset | ||
| .claude | ||
| .github | ||
| .husky | ||
| apps | ||
| docker | ||
| docs | ||
| games | ||
| load-tests | ||
| NewAppIdeas/Roblox Reimagined | ||
| packages | ||
| patches | ||
| scripts | ||
| services | ||
| tests/e2e | ||
| .dockerignore | ||
| .editorconfig | ||
| .env.development | ||
| .env.macmini.example | ||
| .gitignore | ||
| .npmrc | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| CLAUDE.md | ||
| cloudflared-config.yml | ||
| docker-compose.dev.yml | ||
| docker-compose.macmini.yml | ||
| eslint.config.mjs | ||
| gift-codes-2026-02-14.txt | ||
| lint-staged.config.js | ||
| mac-mini-setup.sh | ||
| package.json | ||
| playwright.config.ts | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| test-chat-auth.sh | ||
| TROUBLESHOOTING.md | ||
| turbo.json | ||
| vitest.config.ts | ||
Mana Monorepo
Monorepo containing all Mana projects — a self-hosted multi-app ecosystem with shared packages and unified tooling.
Projects
| Project | Description | Apps |
|---|---|---|
| mana | Multi-app ecosystem platform | Expo mobile, SvelteKit web |
| chat | AI chat application | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| todo | Task management | NestJS backend, SvelteKit web, Astro landing |
| calendar | Calendar & scheduling | NestJS backend, SvelteKit web, Astro landing |
| clock | Pomodoro & time tracking | NestJS backend, SvelteKit web, Astro landing |
| contacts | Contact management | NestJS backend, SvelteKit web |
| picture | AI image generation | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| cards | Card/deck management | NestJS backend, Expo mobile, SvelteKit web |
| zitare | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing |
| mukke | Music player | NestJS backend, SvelteKit web |
| planta | Plant care tracker | NestJS backend, SvelteKit web |
| storage | Cloud storage | NestJS backend, SvelteKit web |
| questions | Q&A with web search | SvelteKit web |
| skilltree | Skill tree visualization | NestJS backend, SvelteKit web |
| nutriphi | Nutrition tracking | NestJS backend, SvelteKit web |
| citycorners | City guide | NestJS backend, SvelteKit web, Astro landing |
| presi | Presentation tool | NestJS backend, SvelteKit web |
| photos | Photo management | NestJS backend, SvelteKit web |
Getting Started
Prerequisites
- Node.js 20+
- pnpm 9.15.0+
- Docker (for PostgreSQL, Redis, MinIO)
Installation
pnpm install
Development
# Start infrastructure (PostgreSQL, Redis, MinIO)
pnpm docker:up
# Start any app with auto DB setup
pnpm dev:chat:full
pnpm dev:todo:full
pnpm dev:calendar:full
pnpm dev:contacts:full
# Build & quality
pnpm run build
pnpm run type-check
pnpm run format
See CLAUDE.md for comprehensive development documentation.
Architecture
mana-monorepo/
├── apps/ # Product applications
├── services/ # Microservices (auth, search, LLM, bots)
├── packages/ # Shared packages
├── docker/ # Docker configuration
└── scripts/ # Development & deployment scripts
Tooling
- Package Manager: pnpm 9.15.0
- Build System: Turborepo
- Formatting: Prettier (tabs, single quotes, 100 char width)
- Hosting: Mac Mini (self-hosted) via Docker + Cloudflare Tunnel
- Analytics: Umami (stats.mana.how)
License
Private - All rights reserved