refactor(shared-tailwind): rewrite themes.css to single-layer shadcn convention

Pre-launch theme system audit found multiple parallel layers in themes.css
(--theme-X full hsl strings, --X partial shadcn aliases, --color-X populated
by runtime store with raw channels) plus dead-code companion files. The
inconsistency caused light-mode regressions when scoped-CSS consumers
wrote `var(--color-X)` standalone — the variable holds raw HSL channels
which is invalid as a color value, browser fell back to inherited (white).

Rewrite to one consistent layer:

  - Source of truth: --color-X defined as raw HSL channels (e.g.
    `0 0% 17%`) in :root, .dark, and all variant [data-theme="..."]
    blocks. Matches the format the runtime store
    (@mana/shared-theme/src/utils.ts) writes, eliminating the
    static-fallback-vs-runtime mismatch and the corresponding flash
    of unstyled content on hydration.

  - @theme inline uses self-reference + Tailwind v4 <alpha-value>
    placeholder so utility classes generate correctly AND opacity
    modifiers work: `text-foreground/50` → `hsl(var(--color-foreground) / 0.5)`.

  - @layer components (.btn-primary, .card, .badge, etc.) wraps
    var(--color-X) refs with hsl() — they were broken in light mode
    too for the same reason.

Convention going forward (also documented in the file header):

  1. Markup: use Tailwind utility classes (text-foreground, bg-card, …)
  2. Scoped CSS: hsl(var(--color-X)) — always wrap with hsl()
  3. NEVER raw var(--color-X) in CSS — that's the bug pattern

Net file: 692 → 580 LOC. Single source layer, no indirection.

Also delete dead companion files (zero imports anywhere):
  - tailwind-v4.css (had broken self-reference, never imported)
  - theme-variables.css (legacy hex-based palette)
  - components.css (legacy component utilities)
  - index.js / preset.js / colors.js (Tailwind v3 preset format,
    irrelevant under Tailwind v4)

package.json exports map shrinks accordingly to just `./themes.css`.

Consumers using `hsl(var(--color-X))` (~379 files across mana-web,
manavoxel-web, arcade-web) keep working unchanged — the public API
name `--color-X` is preserved. Only the broken pattern `var(--color-X)`
(~61 files) needs a follow-up sweep, handled in a separate commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-09 01:13:06 +02:00
parent 3a3cd126cf
commit 919fcca4b7
63 changed files with 1072 additions and 5398 deletions

View file

@ -1,5 +1,21 @@
# =============================================================================
# Mac Mini Production Environment
# Copy to .env.macmini and fill in the values
# =============================================================================
#
# Copy to .env.macmini (gitignored) and fill in the values. This file is
# loaded by `docker compose -f docker-compose.macmini.yml ...` on the
# Mac Mini host. The compose file references vars via ${VAR} (REQUIRED —
# missing means container fails to start) and ${VAR:-default} (OPTIONAL
# — falls back to the inline default if unset).
#
# Sections below mirror that split:
# 1. REQUIRED — production deployment cannot boot without these
# 2. OPTIONAL — defaults exist in compose; only set to override
#
# Verify the example covers every var the compose file uses:
# grep -ohE '\$\{[A-Z_][A-Z0-9_]*' docker-compose.macmini.yml | sort -u
# (audit baseline established 2026-04-08, see
# docs/REFACTORING_AUDIT_2026_04.md item #9)
# ============================================
# Compose project name (pinned, do not change)
@ -69,3 +85,120 @@ GRAFANA_PASSWORD=your-grafana-admin-password
# Web Analytics (Umami)
# ============================================
UMAMI_APP_SECRET=your-umami-secret-here
# =============================================================================
# REQUIRED — production cannot boot without these
# =============================================================================
# ─── Azure OpenAI ───────────────────────────────────────────
# Some compose entries reference ${AZURE_OPENAI_KEY} (no default), distinct
# from the ${AZURE_OPENAI_API_KEY:-} above. Provide both — they may be the
# same value or different keys depending on which deployment they hit.
AZURE_OPENAI_KEY=
AZURE_OPENAI_DEPLOYMENT=
# ─── Azure Speech (mana-stt / mana-tts fallback) ────────────
# Four rotation keys + endpoint. Get from Azure Portal → Speech resource.
AZURE_SPEECH_ENDPOINT=
AZURE_SPEECH_KEY_1=
AZURE_SPEECH_KEY_2=
AZURE_SPEECH_KEY_3=
AZURE_SPEECH_KEY_4=
# ─── Azure Blob Storage (Memoro batch audio) ────────────────
AZURE_STORAGE_ACCOUNT_NAME=
AZURE_STORAGE_ACCOUNT_KEY=
# ─── Google Gemini ──────────────────────────────────────────
# Used by mana-llm + several Gemini-Vision modules (planta, nutriphi).
GEMINI_API_KEY=
# ─── Service-to-service auth keys ───────────────────────────
# Shared secrets backends use to call each other without going through
# user JWTs. Generate with: openssl rand -base64 32
# MANA_SERVICE_KEY appears in compose with BOTH a default and a no-default
# reference, so it MUST be set to a real value in production.
MANA_SERVICE_KEY=
MANA_CREDITS_SERVICE_KEY=
MEMORO_SERVICE_KEY=
# ─── Memoro Supabase (legacy) ───────────────────────────────
# Memoro still keeps recording metadata in Supabase. Move to mana_platform
# is tracked in the Memoro CLAUDE.md.
MEMORO_SUPABASE_URL=
MEMORO_SUPABASE_SERVICE_KEY=
# =============================================================================
# OPTIONAL — defaults baked into docker-compose.macmini.yml
# =============================================================================
# Only uncomment + set if you want to override the in-compose default.
# Each line shows the default that ships in the compose file so you know
# what you're overriding.
# ─── Database / Cache (defaults are insecure!) ──────────────
# POSTGRES_PASSWORD=devpassword # CHANGE for prod
# REDIS_PASSWORD=redis123 # CHANGE for prod
# ─── MinIO (defaults are insecure!) ─────────────────────────
# MINIO_ACCESS_KEY=minioadmin # CHANGE for prod
# MINIO_SECRET_KEY=minioadmin # CHANGE for prod
# ─── Better Auth ────────────────────────────────────────────
# Default falls back to ${JWT_SECRET}. Override only if you want a
# distinct session-signing key.
# BETTER_AUTH_SECRET=
# ─── LLM models ─────────────────────────────────────────────
# MANA_LLM_API_KEY= # default empty (open llm.mana.how)
# MANA_LLM_MODEL=ollama/gemma3:12b
# OLLAMA_URL=http://host.docker.internal:13434
# OLLAMA_MODEL=gemma3:12b
# ─── Third-party AI APIs (optional) ─────────────────────────
# OPENROUTER_API_KEY=
# GROQ_API_KEY=
# GOOGLE_API_KEY=
# TOGETHER_API_KEY=
# ─── STT / TTS (defaults point to GPU box on LAN) ───────────
# STT_SERVICE_URL=http://192.168.178.11:3020
# TTS_SERVICE_URL=http://192.168.178.11:3022
# MANA_STT_API_KEY=
# ─── Stripe (defaults empty — billing disabled if unset) ────
# STRIPE_SECRET_KEY=
# STRIPE_CREDITS_WEBHOOK_SECRET=
# STRIPE_SUBSCRIPTIONS_WEBHOOK_SECRET=
# ─── Mail (Stalwart) — defaults work for the bundled stack ──
# SMTP_HOST=stalwart
# SMTP_PORT=587
# SMTP_USER=noreply
# SMTP_PASSWORD=ManaNoReply2026! # CHANGE for prod
# STALWART_ADMIN_PASSWORD=ChangeMe123! # CHANGE for prod
# ─── Search (SearXNG) ───────────────────────────────────────
# SEARXNG_SECRET=change-me-searxng-secret
# ─── Error tracking (GlitchTip / Sentry) ────────────────────
# GLITCHTIP_DSN_MANA_WEB=
# GLITCHTIP_SECRET_KEY=change-me-in-production
# ─── Notifications ──────────────────────────────────────────
# NTFY_TOPIC=
# TELEGRAM_BOT_TOKEN=
# TELEGRAM_CHAT_ID=
# ─── Cloudflare (only if deploying landings via wrangler) ───
# CLOUDFLARE_ACCOUNT_ID=
# CLOUDFLARE_API_TOKEN=
# EXPO_ACCESS_TOKEN=
# ─── Admin / abuse limits ───────────────────────────────────
# ADMIN_USER_IDS= # comma-separated user IDs
# MAX_DAILY_SIGNUPS=0 # 0 = unlimited
# ─── Misc ───────────────────────────────────────────────────
# AZURE_OPENAI_API_VERSION=
# AZURE_STORAGE_CONTAINER=memoro-batch-audio
# AZURE_SPEECH_REGION=germanywestcentral

View file

@ -9,10 +9,11 @@
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
import { consumeCredits, validateCredits } from '@mana/shared-hono/credits';
import type { AuthVariables } from '@mana/shared-hono';
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Chat Completion (sync) ──────────────────────────────────

View file

@ -4,6 +4,7 @@
*/
import { Hono } from 'hono';
import type { AuthVariables } from '@mana/shared-hono';
const ALLOWED_AVATAR_TYPES = new Set([
'image/jpeg',
@ -13,7 +14,7 @@ const ALLOWED_AVATAR_TYPES = new Set([
'image/svg+xml',
]);
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Avatar Upload (S3) ─────────────────────────────────────
@ -33,9 +34,7 @@ routes.post('/:id/avatar', async (c) => {
if (file.type === 'image/svg+xml') {
// SVGs stay on shared-storage (Sharp can't process SVG)
const { createContactsStorage, generateUserFileKey } = await import(
'@mana/shared-storage'
);
const { createContactsStorage, generateUserFileKey } = await import('@mana/shared-storage');
const storage = createContactsStorage();
const key = generateUserFileKey(userId, `avatar-${c.req.param('id')}.svg`);
const result = await storage.upload(key, Buffer.from(buffer), {

View file

@ -7,10 +7,11 @@
import { Hono } from 'hono';
import { consumeCredits, validateCredits } from '@mana/shared-hono/credits';
import type { AuthVariables } from '@mana/shared-hono';
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── AI Generation (server-only: mana-llm) ──────────────────

View file

@ -6,7 +6,8 @@
* This module handles web import via mana-search + mana-llm, and share links.
*/
import { Hono } from 'hono';
import { Hono, type Context } from 'hono';
import { logger } from '@mana/shared-hono';
const MANA_SEARCH_URL = process.env.MANA_SEARCH_URL ?? 'http://localhost:3021';
const MANA_LLM_URL = process.env.MANA_LLM_URL ?? 'http://localhost:3030';
@ -35,7 +36,7 @@ routes.post('/import/url', async (c) => {
extracted = await res.json();
}
} catch (e) {
console.error('mana-search extract failed:', e);
logger.error('guides.extract_failed', { error: e instanceof Error ? e.message : String(e) });
}
const content = extracted.markdown ?? extracted.content ?? '';
@ -128,7 +129,7 @@ routes.get('/share/:token', (c) => {
// ─── Shared: LLM guide generation ──────────────────────────
async function generateGuideFromText(
c: Parameters<Parameters<typeof Hono.prototype.post>[1]>[0],
c: Context,
opts: { text: string; title?: string; sourceUrl?: string; isAiPrompt?: boolean }
) {
const systemPrompt = `Du bist ein Experte für das Erstellen strukturierter Schritt-für-Schritt-Anleitungen.
@ -189,7 +190,7 @@ Regeln:
throw new Error(`LLM error: ${llmRes.status}`);
}
const llmData = await llmRes.json<{ content: string }>();
const llmData = (await llmRes.json()) as { content: string };
const rawJson = llmData.content.trim();
// Extract JSON from potential markdown code fences
@ -209,7 +210,7 @@ Regeln:
sections: parsed.sections ?? [],
});
} catch (e) {
console.error('Guide generation failed:', e);
logger.error('guides.generate_failed', { error: e instanceof Error ? e.message : String(e) });
return c.json({ error: 'Guide-Generierung fehlgeschlagen', details: String(e) }, 500);
}
}

View file

@ -4,8 +4,9 @@
*/
import { Hono } from 'hono';
import type { AuthVariables } from '@mana/shared-hono';
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Song Upload (presigned URL) ────────────────────────────

View file

@ -7,6 +7,7 @@
*/
import { Hono } from 'hono';
import { logger, type AuthVariables } from '@mana/shared-hono';
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
@ -20,7 +21,7 @@ const ANALYSIS_PROMPT = `Du bist ein Ernährungsexperte. Analysiere die Mahlzeit
"suggestions": []
}`;
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Photo Analysis (server-only: Gemini Vision) ────────────
@ -81,7 +82,9 @@ routes.post('/analysis/photo', async (c) => {
return c.json(analysis);
} catch (err) {
console.error('Photo analysis failed:', err);
logger.error('nutriphi.photo_analysis_failed', {
error: err instanceof Error ? err.message : String(err),
});
return c.json({ error: 'Analysis failed' }, 500);
}
});
@ -115,7 +118,9 @@ routes.post('/analysis/text', async (c) => {
return c.json(analysis);
} catch (err) {
console.error('Text analysis failed:', err);
logger.error('nutriphi.text_analysis_failed', {
error: err instanceof Error ? err.message : String(err),
});
return c.json({ error: 'Analysis failed' }, 500);
}
});

View file

@ -8,11 +8,12 @@
import { Hono } from 'hono';
import { consumeCredits, validateCredits } from '@mana/shared-hono/credits';
import type { AuthVariables } from '@mana/shared-hono';
const REPLICATE_TOKEN = process.env.REPLICATE_API_TOKEN || '';
const IMAGE_GEN_URL = process.env.MANA_IMAGE_GEN_URL || '';
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── AI Image Generation (server-only: Replicate/local) ─────

View file

@ -7,10 +7,11 @@
*/
import { Hono } from 'hono';
import { logger, type AuthVariables } from '@mana/shared-hono';
const LLM_URL = process.env.MANA_LLM_URL || 'http://localhost:3025';
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Photo Upload (server-only: S3 storage) ─────────────────
@ -38,7 +39,9 @@ routes.post('/photos/upload', async (c) => {
201
);
} catch (err) {
console.error('Upload failed:', err);
logger.error('planta.upload_failed', {
error: err instanceof Error ? err.message : String(err),
});
return c.json({ error: 'Upload failed' }, 500);
}
});
@ -81,7 +84,9 @@ routes.post('/analysis/identify', async (c) => {
return c.json(analysis);
} catch (err) {
console.error('Analysis failed:', err);
logger.error('planta.analysis_failed', {
error: err instanceof Error ? err.message : String(err),
});
return c.json({ error: 'Analysis failed' }, 500);
}
});

View file

@ -10,6 +10,7 @@ import { Hono } from 'hono';
import { eq, and, gt, or, isNull, asc } from 'drizzle-orm';
import { HTTPException } from 'hono/http-exception';
import { authMiddleware } from '@mana/shared-hono/auth';
import type { AuthVariables } from '@mana/shared-hono';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import {
@ -112,7 +113,7 @@ function generateShareCode(): string {
// ─── Routes ─────────────────────────────────────────────────
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Public endpoint (no auth) ──────────────────────────────
@ -187,7 +188,9 @@ routes.post('/share/deck/:deckId', async (c) => {
}
// Parse optional expiry
const body = await c.req.json<{ expiresAt?: string }>().catch(() => ({}));
const body = (await c.req.json<{ expiresAt?: string }>().catch(() => ({}))) as {
expiresAt?: string;
};
const [share] = await db
.insert(sharedDecks)
@ -223,6 +226,7 @@ routes.get('/share/deck/:deckId/links', async (c) => {
routes.delete('/share/:shareId', authMiddleware(), async (c) => {
const userId = c.get('userId');
const shareId = c.req.param('shareId');
if (!shareId) throw new HTTPException(400, { message: 'shareId required' });
const share = await db.query.sharedDecks.findFirst({
where: eq(sharedDecks.id, shareId),

View file

@ -7,8 +7,9 @@
*/
import { Hono } from 'hono';
import type { AuthVariables } from '@mana/shared-hono';
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── File Upload (server-only: S3) ──────────────────────────
@ -46,9 +47,8 @@ routes.post('/files/upload', async (c) => {
}
// Non-images -> shared-storage as before
const { createStorageStorage, generateUserFileKey, getContentType } = await import(
'@mana/shared-storage'
);
const { createStorageStorage, generateUserFileKey, getContentType } =
await import('@mana/shared-storage');
const storage = createStorageStorage();
const key = generateUserFileKey(userId, file.name);
@ -91,10 +91,13 @@ routes.get('/files/:id/download', async (c) => {
return c.json({ url });
}
const data = await storage.download(storagePath);
return new Response(data.body, {
const [buffer, metadata] = await Promise.all([
storage.download(storagePath),
storage.getMetadata(storagePath).catch(() => null),
]);
return new Response(new Uint8Array(buffer), {
headers: {
'Content-Type': data.contentType || 'application/octet-stream',
'Content-Type': metadata?.contentType || 'application/octet-stream',
'Content-Disposition': `attachment; filename="${storagePath.split('/').pop()}"`,
},
});

View file

@ -18,7 +18,7 @@ import { z } from 'zod';
import { eq, and, asc, sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { serviceAuthMiddleware } from '@mana/shared-hono';
import { serviceAuthMiddleware, type AuthVariables } from '@mana/shared-hono';
import {
pgSchema,
uuid,
@ -93,7 +93,7 @@ const db = drizzle(connection, { schema: { tasks, projects, reminders } });
// ─── Routes ────────────────────────────────────────────────
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── RRULE Compute ─────────────────────────────────────────

View file

@ -7,6 +7,7 @@
*/
import { Hono } from 'hono';
import { logger, type AuthVariables } from '@mana/shared-hono';
import { eq, and } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
@ -120,7 +121,7 @@ const db = drizzle(connection, { schema: { locations, cities, pois, guides, guid
// ─── Routes ─────────────────────────────────────────────────
const routes = new Hono();
const routes = new Hono<{ Variables: AuthVariables }>();
// ─── Guide Generation (server-only: AI + search) ────────────
@ -152,7 +153,11 @@ routes.post('/guides/generate', async (c) => {
// Fire-and-forget async pipeline
runGuidePipeline(guide.id, userId, city, params.language || 'de', params.maxPois || 10).catch(
(err) => {
console.error('Guide generation failed:', err);
logger.error('traces.guide_generation_failed', {
guideId: guide.id,
cityId: city.id,
error: err instanceof Error ? err.message : String(err),
});
db.update(guides)
.set({ status: 'error' })
.where(eq(guides.id, guide.id))

View file

@ -1,660 +1,18 @@
# Calendar Project Guide
# Calendar — consolidated into the unified Mana app
## Übersicht
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/calendar/apps/server/` and `apps/calendar/apps/web/`
directories have been removed. Active code now lives in:
**Kalender** ist eine vollständige Kalender-Anwendung für persönliches und geteiltes Zeitmanagement. Die App unterstützt mehrere Kalender, wiederkehrende Termine, CalDAV/iCal-Synchronisation und Erinnerungen.
- **Backend compute routes**: [`apps/api/src/modules/calendar/routes.ts`](../api/src/modules/calendar/routes.ts)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/calendar/`](../mana/apps/web/src/lib/modules/calendar/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/calendar/`](../mana/apps/web/src/routes/(app)/calendar/)
- **Landing page** (still standalone): [`apps/calendar/apps/landing/`](apps/landing/)
| App | Port | URL |
|-----|------|-----|
| Server | 3014 | http://localhost:3014 |
| Web App | 5179 | http://localhost:5179 |
| Landing Page | 4322 | http://localhost:4322 |
| Mobile | 8081 | Expo Go |
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
## Project Structure
```
apps/calendar/
├── apps/
│ ├── server/ # Hono/Bun compute server (@calendar/server)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── db/ # Drizzle schemas + migrations
│ │ │ ├── schema/
│ │ │ │ ├── calendars.schema.ts
│ │ │ │ ├── events.schema.ts
│ │ │ │ ├── reminders.schema.ts
│ │ │ │ ├── calendar-shares.schema.ts
│ │ │ │ └── external-calendars.schema.ts
│ │ │ └── db.ts
│ │ ├── calendar/ # Calendar CRUD
│ │ ├── event/ # Event CRUD + queries
│ │ ├── reminder/ # Reminders + notifications
│ │ ├── sync/ # CalDAV/iCal sync
│ │ ├── share/ # Calendar sharing
│ │ └── health/
│ │
│ ├── web/ # SvelteKit web application (@calendar/web)
│ │ └── src/
│ │ ├── lib/
│ │ │ ├── api/ # API clients
│ │ │ │ ├── client.ts
│ │ │ │ ├── calendars.ts
│ │ │ │ ├── events.ts
│ │ │ │ ├── reminders.ts
│ │ │ │ └── shares.ts
│ │ │ ├── stores/ # Svelte 5 runes stores
│ │ │ │ ├── auth.svelte.ts
│ │ │ │ ├── view.svelte.ts
│ │ │ │ ├── calendars.svelte.ts
│ │ │ │ ├── events.svelte.ts
│ │ │ │ ├── theme.ts
│ │ │ │ ├── navigation.ts
│ │ │ │ └── toast.ts
│ │ │ ├── components/
│ │ │ │ ├── calendar/
│ │ │ │ │ ├── CalendarHeader.svelte
│ │ │ │ │ ├── WeekView.svelte
│ │ │ │ │ ├── DayView.svelte
│ │ │ │ │ ├── MonthView.svelte
│ │ │ │ │ ├── MiniCalendar.svelte
│ │ │ │ │ └── CalendarSidebar.svelte
│ │ │ │ └── event/
│ │ │ │ └── EventForm.svelte
│ │ │ └── i18n/ # Internationalization (5 Sprachen)
│ │ └── routes/
│ │ ├── +layout.svelte
│ │ ├── +page.svelte # Hauptkalender (Wochenansicht)
│ │ ├── agenda/+page.svelte # Agenda-Ansicht
│ │ ├── event/
│ │ │ ├── new/+page.svelte # Neuer Termin
│ │ │ └── [id]/+page.svelte # Termin bearbeiten
│ │ ├── calendars/+page.svelte
│ │ ├── settings/+page.svelte
│ │ ├── feedback/+page.svelte
│ │ └── (auth)/
│ │ ├── login/+page.svelte
│ │ ├── register/+page.svelte
│ │ └── forgot-password/+page.svelte
│ │
│ ├── landing/ # Astro marketing landing page (@calendar/landing)
│ │ └── src/
│ │ ├── pages/index.astro
│ │ ├── layouts/Layout.astro
│ │ └── components/
│ │ ├── Hero.astro
│ │ ├── Features.astro
│ │ ├── CTA.astro
│ │ └── Footer.astro
│ │
│ └── mobile/ # Expo/React Native mobile app (@calendar/mobile) [TODO]
├── packages/
│ ├── shared/ # Shared types, utils, constants (@calendar/shared)
│ │ └── src/
│ │ ├── types/
│ │ │ ├── calendar.ts
│ │ │ ├── event.ts
│ │ │ ├── reminder.ts
│ │ │ └── share.ts
│ │ └── index.ts
│ └── web-ui/ # Shared Svelte components (@calendar/web-ui) [TODO]
├── package.json
└── CLAUDE.md
```
## Commands
### Root Level (from monorepo root)
```bash
# Alle Apps starten
pnpm calendar:dev # Run all calendar apps
# Einzelne Apps starten
pnpm dev:calendar:server # Start server (port 3014)
pnpm dev:calendar:web # Start web app (port 5179)
pnpm dev:calendar:landing # Start landing page (port 4322)
pnpm dev:calendar:mobile # Start mobile app [TODO]
pnpm dev:calendar:app # Start web + server together
pnpm dev:calendar:local # Start web + sync (no auth needed)
# Datenbank
pnpm calendar:db:push # Push schema to database
pnpm calendar:db:studio # Open Drizzle Studio
pnpm calendar:db:seed # Seed initial data
```
### Server (apps/calendar/apps/server)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
pnpm db:seed # Seed initial data
```
### Web App (apps/calendar/apps/web)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
### Landing Page (apps/calendar/apps/landing)
```bash
pnpm dev # Start dev server (port 4322)
pnpm build # Build for production
pnpm preview # Preview build
```
## Technology Stack
| Layer | Technology |
|-------|------------|
| **Server** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Landing** | Astro 5.x, Tailwind CSS |
| **Mobile** | React Native 0.81 + Expo SDK 54, NativeWind [TODO] |
| **Auth** | Mana Auth (JWT) |
| **i18n** | svelte-i18n (DE, EN, FR, ES, IT) |
| **Dates** | date-fns |
| **Sync** | ical.js, tsdav (CalDAV) |
## Architecture
### Core Features
1. **Persönliche Kalender** - Erstelle und verwalte mehrere farbcodierte Kalender
2. **Termine** - Vollständiges CRUD mit Wiederholungsunterstützung (RFC 5545 RRULE)
3. **Geteilte Kalender** - Teile Kalender mit Lese-/Schreib-/Admin-Berechtigungen
4. **CalDAV/iCal Sync** - Bi-direktionale Synchronisation mit Google, Apple, etc.
5. **Erinnerungen** - Push-Benachrichtigungen und E-Mail-Erinnerungen
### Kalender-Ansichten
| Ansicht | Route | Beschreibung |
|---------|-------|--------------|
| **Woche** | `/` (default) | 7-Tage-Raster mit Stunden |
| **Tag** | Click auf Tag | 24-Stunden-Timeline |
| **Monat** | Header-Switch | Traditionelles Kalenderraster |
| **Agenda** | `/agenda` | Chronologische Terminliste |
| **Jahr** | [TODO] | Kompakte 12-Monats-Übersicht |
### Web App Stores (Svelte 5 Runes)
```typescript
// auth.svelte.ts - Authentifizierung
authStore.isAuthenticated // boolean
authStore.user // User | null
authStore.signIn(email, password)
authStore.signOut()
authStore.getAccessToken()
// view.svelte.ts - Kalender-Ansicht
viewStore.currentDate // Date
viewStore.viewType // 'day' | 'week' | 'month' | 'year' | 'agenda'
viewStore.setDate(date)
viewStore.setViewType(type)
viewStore.goToToday()
viewStore.navigate(direction) // 'prev' | 'next'
// calendars.svelte.ts - Kalender-Verwaltung
calendarsStore.calendars // Calendar[]
calendarsStore.loading // boolean
calendarsStore.fetchCalendars()
calendarsStore.createCalendar(data)
calendarsStore.updateCalendar(id, data)
calendarsStore.deleteCalendar(id)
calendarsStore.getColor(calendarId)
// events.svelte.ts - Termine
eventsStore.events // Event[]
eventsStore.loading // boolean
eventsStore.fetchEvents(start, end)
eventsStore.getEventsForDay(date)
eventsStore.getEventsForWeek(date)
eventsStore.createEvent(data)
eventsStore.updateEvent(id, data)
eventsStore.deleteEvent(id)
```
### Server API Endpoints
#### Health
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/health` | GET | Health check |
#### Calendars
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/calendars` | GET | List user's calendars |
| `/api/v1/calendars` | POST | Create calendar |
| `/api/v1/calendars/:id` | GET | Get calendar details |
| `/api/v1/calendars/:id` | PUT | Update calendar |
| `/api/v1/calendars/:id` | DELETE | Delete calendar |
#### Events
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/events` | GET | Query events (date range) |
| `/api/v1/events` | POST | Create event |
| `/api/v1/events/:id` | GET | Get event details |
| `/api/v1/events/:id` | PUT | Update event |
| `/api/v1/events/:id` | DELETE | Delete event |
| `/api/v1/events/calendar/:calendarId` | GET | Get events by calendar |
#### Reminders
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/events/:eventId/reminders` | GET | List event reminders |
| `/api/v1/events/:eventId/reminders` | POST | Add reminder |
| `/api/v1/reminders/:id` | DELETE | Remove reminder |
#### Sharing
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/calendars/:id/shares` | GET | List calendar shares |
| `/api/v1/calendars/:id/shares` | POST | Share calendar |
| `/api/v1/shares/:shareId/accept` | POST | Accept invitation |
| `/api/v1/shares/:shareId/decline` | POST | Decline invitation |
#### Sync
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/sync/external` | GET | List external calendars |
| `/api/v1/sync/external` | POST | Connect external calendar |
| `/api/v1/sync/external/:id` | DELETE | Disconnect external |
| `/api/v1/sync/external/:id/sync` | POST | Trigger manual sync |
| `/api/v1/sync/caldav/discover` | POST | Discover CalDAV calendars |
| `/api/v1/calendars/:id/export.ics` | GET | Export calendar as iCal |
### Database Schema
#### calendars
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `user_id` | UUID | Owner |
| `name` | VARCHAR(255) | Calendar name |
| `description` | TEXT | Optional description |
| `color` | VARCHAR(7) | Hex color code (#3B82F6) |
| `is_default` | BOOLEAN | Default calendar flag |
| `is_visible` | BOOLEAN | Visibility in UI |
| `timezone` | VARCHAR(100) | Default timezone |
| `settings` | JSONB | CalendarSettings object |
| `created_at` | TIMESTAMP | Creation time |
| `updated_at` | TIMESTAMP | Last update |
#### events
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `calendar_id` | UUID | FK to calendars |
| `user_id` | UUID | Owner |
| `title` | VARCHAR(500) | Event title |
| `description` | TEXT | Event description |
| `location` | VARCHAR(500) | Location |
| `start_time` | TIMESTAMP | Start datetime |
| `end_time` | TIMESTAMP | End datetime |
| `is_all_day` | BOOLEAN | All-day flag |
| `timezone` | VARCHAR(100) | Event timezone |
| `recurrence_rule` | VARCHAR(500) | RFC 5545 RRULE |
| `recurrence_end_date` | TIMESTAMP | End of recurrence |
| `recurrence_exceptions` | JSONB | Exception dates |
| `parent_event_id` | UUID | Parent for instances |
| `color` | VARCHAR(7) | Override color |
| `status` | VARCHAR(20) | confirmed/tentative/cancelled |
| `external_id` | VARCHAR(255) | External calendar ID |
| `metadata` | JSONB | Attendees, URL, etc. |
| `created_at` | TIMESTAMP | Creation time |
| `updated_at` | TIMESTAMP | Last update |
#### calendar_shares
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `calendar_id` | UUID | FK to calendars |
| `shared_with_user_id` | UUID | Target user (optional) |
| `shared_with_email` | VARCHAR(255) | Email for invite |
| `permission` | VARCHAR(20) | read/write/admin |
| `share_token` | VARCHAR(64) | For link sharing |
| `share_url` | VARCHAR(500) | Public share URL |
| `status` | VARCHAR(20) | pending/accepted/declined |
| `invited_by` | UUID | Inviter user ID |
| `accepted_at` | TIMESTAMP | Accept timestamp |
| `expires_at` | TIMESTAMP | Expiration date |
| `created_at` | TIMESTAMP | Creation time |
| `updated_at` | TIMESTAMP | Last update |
#### reminders
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `event_id` | UUID | FK to events |
| `user_id` | UUID | Owner |
| `minutes_before` | INTEGER | Reminder offset |
| `reminder_time` | TIMESTAMP | Calculated time |
| `notify_push` | BOOLEAN | Push notification |
| `notify_email` | BOOLEAN | Email notification |
| `status` | VARCHAR(20) | pending/sent/failed |
| `sent_at` | TIMESTAMP | Send timestamp |
| `event_instance_date` | TIMESTAMP | For recurring events |
| `created_at` | TIMESTAMP | Creation time |
#### external_calendars
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `user_id` | UUID | Owner |
| `name` | VARCHAR(255) | Display name |
| `provider` | VARCHAR(50) | google/apple/caldav/ical_url |
| `calendar_url` | TEXT | CalDAV or iCal URL |
| `username` | VARCHAR(255) | CalDAV username |
| `encrypted_password` | TEXT | Encrypted password |
| `access_token` | TEXT | OAuth token |
| `refresh_token` | TEXT | OAuth refresh token |
| `token_expires_at` | TIMESTAMP | Token expiration |
| `sync_enabled` | BOOLEAN | Sync toggle |
| `sync_direction` | VARCHAR(20) | both/import/export |
| `sync_interval` | INTEGER | Minutes between syncs |
| `last_sync_at` | TIMESTAMP | Last sync time |
| `last_sync_error` | TEXT | Error message |
| `color` | VARCHAR(7) | Display color |
| `is_visible` | BOOLEAN | Visibility in UI |
| `provider_data` | JSONB | Provider-specific data |
| `created_at` | TIMESTAMP | Creation time |
| `updated_at` | TIMESTAMP | Last update |
### Recurrence (RFC 5545 RRULE)
Beispiele für wiederkehrende Termine:
```
FREQ=DAILY # Täglich
FREQ=WEEKLY;BYDAY=MO,WE,FR # Mo, Mi, Fr
FREQ=WEEKLY;INTERVAL=2;BYDAY=TU # Jeden 2. Dienstag
FREQ=MONTHLY;BYMONTHDAY=15 # Am 15. jeden Monats
FREQ=MONTHLY;BYDAY=2MO # Am 2. Montag jeden Monats
FREQ=YEARLY;BYMONTH=12;BYMONTHDAY=25 # Jährlich am 25.12.
FREQ=DAILY;COUNT=10 # Täglich, 10 mal
FREQ=WEEKLY;UNTIL=20241231T235959Z # Wöchentlich bis Ende 2024
```
## Environment Variables
### Server (.env)
```env
NODE_ENV=development
PORT=3014
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/calendar
MANA_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5173,http://localhost:5179,http://localhost:8081
# Notifications (optional)
EXPO_ACCESS_TOKEN=your-expo-access-token
RESEND_API_KEY=your-resend-api-key
EMAIL_FROM=calendar@mana.how
```
### Web (.env)
```env
PUBLIC_BACKEND_URL=http://localhost:3014
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
### Mobile (.env)
```env
EXPO_PUBLIC_BACKEND_URL=http://localhost:3014
EXPO_PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Shared Packages
### @calendar/shared
**Types:**
- `Calendar` - Kalender-Entity
- `CalendarSettings` - Kalender-Einstellungen (JSONB)
- `CalendarViewType` - 'day' | 'week' | 'month' | 'year' | 'agenda'
- `Event` - Termin-Entity
- `EventStatus` - 'confirmed' | 'tentative' | 'cancelled'
- `Reminder` - Erinnerung-Entity
- `ReminderStatus` - 'pending' | 'sent' | 'failed'
- `CalendarShare` - Freigabe-Entity
- `SharePermission` - 'read' | 'write' | 'admin'
- `ExternalCalendar` - Externe Kalender-Entity
**Constants:**
- `DEFAULT_CALENDAR_COLORS` - 8 vordefinierte Farben
- `DEFAULT_TIMEZONES` - Häufige Zeitzonen
## Code Style Guidelines
- **TypeScript**: Strict typing mit Interfaces
- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`)
- **Styling**: Tailwind CSS mit CSS-Variablen
- **Formatting**: Prettier mit Projekt-Config
- **i18n**: Alle UI-Texte in Locale-Dateien
### Svelte 5 Runes Beispiel
```svelte
<script lang="ts">
import { viewStore } from '$lib/stores/view.svelte';
// Reaktiver State
let loading = $state(false);
// Abgeleiteter Wert
let formattedDate = $derived(
format(viewStore.currentDate, 'MMMM yyyy', { locale: de })
);
// Side Effect
$effect(() => {
console.log('Date changed:', viewStore.currentDate);
});
</script>
```
## Quick Add Syntax
Natural language event creation via `event-parser.ts`:
```
"Meeting morgen 14 Uhr 1h @Arbeit #wichtig"
```
Recognized patterns:
- **Date**: heute, morgen, nächsten Montag, 15.12.
- **Time**: um 14 Uhr, 14:00
- **Time Range**: 14-16 Uhr, 10:00-11:30
- **Duration**: 30min, 2h, 1.5 Stunden, 2h30m
- **All-Day**: ganztägig, ganzer Tag
- **Calendar**: @Kalender (first @ref matches calendar)
- **Attendees**: @Name (subsequent @refs become attendees)
- **Tags**: #tag1 #tag2
- **Location**: in Berlin, im Büro, bei Dr. Müller
- **Recurrence**: jeden Tag, wöchentlich, monatlich
### Multi-Event Input
Split multiple events with keywords (`danach`, `dann`, `und dann`, `anschließend`) or semicolons:
```
"Meeting 14 Uhr 1h danach Review 30min"
→ Event 1: Meeting (14:00-15:00)
→ Event 2: Review (15:00-15:30, auto-offset)
"Standup 9 Uhr 30min @Arbeit; Sprint Planning 1h; Code Review 30min"
→ 3 events chained: 9:00-9:30, 9:30-10:30, 10:30-11:00
```
Context inheritance: subsequent events inherit date, time, and calendar from the first event. If the first event has a duration, the next event starts where it ends.
### Smart Duration (Auto-Estimation)
Duration is **automatically applied** to new events when no explicit duration is typed. Uses `estimateEventDuration()` from `event-estimator.ts` with weighted similarity (calendar, title overlap, tags). Falls back to `defaultEventDuration` from settings. Controllable via Settings > Termin-Einstellungen:
- **Smarte Dauer** toggle (`smartDurationEnabled`, default: on)
- **Standard-Dauer** fallback (`defaultEventDuration`, default: 60min)
Priority: explicit duration in text > history estimate > default fallback > 1h (if disabled). Runs fully offline against IndexedDB.
### Conflict Detection
`detectConflicts()` in `event-estimator.ts` checks for overlapping events. Ignores all-day events, supports exclude-by-ID for edit mode. Returns list of conflicting events with title and time.
## Quick Start
### 1. Datenbank erstellen
```bash
# PostgreSQL Container muss laufen
docker compose -f docker-compose.dev.yml up -d postgres
# Datenbank erstellen
PGPASSWORD=devpassword psql -h localhost -U mana -d postgres -c "CREATE DATABASE calendar;"
# Schema pushen
pnpm calendar:db:push
```
### 2. Apps starten
```bash
# Server + Web zusammen
pnpm dev:calendar:app
# Oder einzeln:
pnpm dev:calendar:server # Terminal 1
pnpm dev:calendar:web # Terminal 2
pnpm dev:calendar:landing # Terminal 3 (optional)
```
### 3. URLs öffnen
- Web App: http://localhost:5179
- Landing: http://localhost:4322
- API Health: http://localhost:3014/api/v1/health
## Testing API (mit curl)
```bash
# Health Check
curl http://localhost:3014/api/v1/health
# Login (get token)
TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password"}' | jq -r '.accessToken')
# Kalender abrufen
curl http://localhost:3014/api/v1/calendars \
-H "Authorization: Bearer $TOKEN"
# Neuen Kalender erstellen
curl -X POST http://localhost:3014/api/v1/calendars \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Arbeit", "color": "#3B82F6"}'
# Termine abrufen (Datumsbereich)
curl "http://localhost:3014/api/v1/events?start=2024-12-01&end=2024-12-31" \
-H "Authorization: Bearer $TOKEN"
# Neuen Termin erstellen
curl -X POST http://localhost:3014/api/v1/events \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"calendarId": "calendar-uuid",
"title": "Meeting",
"startTime": "2024-12-15T10:00:00Z",
"endTime": "2024-12-15T11:00:00Z"
}'
```
## Production Readiness
**Status: Production-Ready (2026-03-24)**
### Checklist
| Category | Status | Details |
|----------|--------|---------|
| **Error Handling** | ✅ | Global `+error.svelte` with i18n (5 languages), error tracking via GlitchTip |
| **Offline Support** | ✅ | Offline page with shared `OfflinePage` component |
| **PWA** | ✅ | Service worker, manifest, icons, apple-touch-icon, shortcuts |
| **Security Headers** | ✅ | CSP, X-Frame-Options, HSTS via `setSecurityHeaders()` |
| **Loading States** | ✅ | Skeleton loaders: CalendarView, EventDetail, Agenda, AppLoading |
| **i18n** | ✅ | 5 languages (DE/EN/FR/ES/IT), all pages including settings fully localized |
| **Meta/SEO** | ✅ | OG tags, meta description in root layout |
| **Accessibility** | ✅ | Focus trapping in all modals, ARIA roles, keyboard navigation |
| **Rate Limiting** | ✅ | ThrottlerGuard global (100 req/min) |
| **API Validation** | ✅ | DTOs with class-validator, whitelist + forbidNonWhitelisted |
| **Auth** | ✅ | JWT via mana-auth, guards on all controllers |
| **Toast System** | ✅ | All toast messages localized via svelte-i18n |
| **Docker** | ✅ | Multi-stage build, health checks, entrypoint script |
| **Tests** | ✅ | 13 unit tests, 7 E2E test suites (Playwright) |
| **Error Tracking** | ✅ | GlitchTip integration (client + server) |
| **Metrics** | ✅ | Prometheus via MetricsModule |
| **Context Menu** | ✅ | Shared ContextMenu on WeekView + AgendaView events |
### E2E Test Suites
```bash
pnpm --filter @calendar/web test:e2e
```
| Suite | Coverage |
|-------|----------|
| `auth.spec.ts` | Login, redirect, invalid credentials |
| `calendar-views.spec.ts` | Week/month/agenda views, navigation |
| `events.spec.ts` | Event CRUD |
| `calendars.spec.ts` | Calendar management |
| `settings.spec.ts` | Settings page |
| `week-view-interactions.spec.ts` | Drag-to-create, time indicator |
| `error-page.spec.ts` | 404 error page |
## Roadmap / TODO
- [ ] Mobile App (Expo)
- [ ] Year View
- [ ] CalDAV Sync Implementation
- [ ] Push Notifications
- [ ] E-Mail Reminders
- [ ] Event Attendees
- [ ] Calendar Import/Export
- [ ] Dark/Light Theme in Landing
## Important Notes
1. **Authentication**: Nutzt Mana Auth (JWT im Authorization Header)
2. **Database**: PostgreSQL mit Drizzle ORM (Port 5432)
3. **Port**: Server läuft auf Port 3014
4. **Recurrence**: Verwendet RFC 5545 RRULE Format
5. **i18n**: 5 Sprachen unterstützt (DE, EN, FR, ES, IT)
6. **Theme**: Ocean-Theme (Blautöne) als Standard
The previous 660-line "Calendar Project Guide" that described a per-product
Hono backend with its own Drizzle schemas, RRULE service, CalDAV sync,
etc. has been deleted — it had been inaccurate since the consolidation
of early 2026. Pre-consolidation reference is in git history.

View file

@ -1,195 +1,19 @@
# Chat Project Guide
# Chat — consolidated into the unified Mana app
## Project Structure
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/chat/apps/server/` and `apps/chat/apps/web/` directories
have been removed. Active code now lives in:
```
apps/chat/
├── apps/
│ ├── server/ # Hono/Bun compute server (@chat/server)
│ ├── landing/ # Astro marketing landing page (@chat/landing)
│ ├── web/ # SvelteKit web application (@chat/web)
│ └── mobile/ # Expo/React Native mobile app (@chat/mobile)
├── packages/
│ └── chat-types/ # Shared TypeScript types (@chat/types)
└── package.json
```
- **Backend compute routes**: [`apps/api/src/modules/chat/routes.ts`](../api/src/modules/chat/routes.ts)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/chat/`](../mana/apps/web/src/lib/modules/chat/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/chat/`](../mana/apps/web/src/routes/(app)/chat/)
- **Landing page** (still standalone): [`apps/chat/apps/landing/`](apps/landing/)
- **Mobile app**: [`apps/chat/apps/mobile/`](apps/mobile/)
## Commands
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
### Root Level
```bash
pnpm chat:dev # Run all chat apps
pnpm dev:chat:mobile # Start mobile app
pnpm dev:chat:web # Start web app
pnpm dev:chat:landing # Start landing page
pnpm dev:chat:server # Start server
pnpm dev:chat:local # Start web + sync (no auth needed)
pnpm dev:chat:full # Start server + web + auth together
```
### Mobile App (chat/apps/mobile)
```bash
pnpm dev # Start Expo dev server
pnpm ios # Run on iOS simulator
pnpm android # Run on Android emulator
pnpm build:dev # Build development version
pnpm build:preview # Build preview version
pnpm build:prod # Build production version
```
### Server (apps/chat/apps/server)
```bash
pnpm start:dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:seed # Seed AI models
pnpm db:studio # Open Drizzle Studio
```
### Web App (chat/apps/web)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
### Landing Page (chat/apps/landing)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
## Technology Stack
- **Mobile**: React Native 0.76.7 + Expo SDK 52, NativeWind, Expo Router
- **Web**: SvelteKit 2.x, Svelte 5, Tailwind CSS 4
- **Landing**: Astro 5.16, Tailwind CSS
- **Server**: Hono + Bun, OpenRouter AI + mana-llm (local), Drizzle ORM, PostgreSQL
- **Auth**: Mana Auth (JWT)
- **Types**: TypeScript 5.x
## Architecture
### Server API Endpoints
| Endpoint | Method | Description |
| --------------------------------- | ------ | --------------------------- |
| `/api/v1/health` | GET | Health check |
| `/api/v1/chat/models` | GET | List available AI models |
| `/api/v1/chat/completions` | POST | Create chat completion |
| `/api/v1/conversations` | GET | List user conversations |
| `/api/v1/conversations/:id` | GET | Get conversation details |
| `/api/v1/conversations/:id/messages` | GET | Get conversation messages |
| `/api/v1/conversations` | POST | Create new conversation |
| `/api/v1/conversations/:id/messages` | POST | Add message to conversation |
### Environment Variables
#### Server (.env)
```env
# Cloud AI models via OpenRouter (optional if using only local models)
OPENROUTER_API_KEY=sk-or-v1-xxx # Get at https://openrouter.ai/keys
# Local AI via mana-llm service
MANA_LLM_URL=http://localhost:3025 # mana-llm service URL
LLM_TIMEOUT=120000 # Timeout in ms (default: 120s)
# Database (uses shared Docker PostgreSQL)
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/chat
# Auth
MANA_AUTH_URL=http://localhost:3001
# Server
PORT=3002
```
#### Mobile (.env)
```env
EXPO_PUBLIC_MANA_AUTH_URL=http://localhost:3001
EXPO_PUBLIC_BACKEND_URL=http://localhost:3002
```
#### Web (.env)
```env
PUBLIC_MANA_AUTH_URL=http://localhost:3001
PUBLIC_BACKEND_URL=http://localhost:3002
```
## Code Style Guidelines
- **TypeScript**: Strict typing with interfaces
- **Mobile**: Functional components with hooks
- **Web**: Svelte 5 runes mode
- **Styling**: Tailwind CSS everywhere
- **Formatting**: 100 char line limit, 2 space tabs, single quotes
## AI Models Available
### Local Models (Ollama - Free, runs on Mac Mini)
| Model ID | Name | Best For |
| -------- | ---- | -------- |
| ...440101 | Gemma 3 4B (Lokal) | Everyday tasks (default) |
| ...440102 | Qwen2.5 Coder 7B (Lokal) | Code generation (92.7% HumanEval) |
| ...440103 | LLaVA 7B Vision (Lokal) | Image/screenshot analysis |
| ...440104 | Qwen3 VL 4B (Lokal) | Fast image analysis |
| ...440105 | DeepSeek OCR (Lokal) | Text recognition in images |
| ...440106 | Phi 3.5 (Lokal) | Compact, efficient |
| ...440107 | Ministral 3B (Lokal) | Very fast, simple tasks |
### Cloud Models (OpenRouter - Paid)
| Model ID | Name | Price | Best For |
| -------- | ---- | ----- | -------- |
| ...440201 | Llama 3.1 8B | $0.05/M | Fast cloud alternative |
| ...440202 | Llama 3.1 70B | $0.35/M | Complex reasoning |
| ...440203 | DeepSeek V3 | $0.14/M | Reasoning at low cost |
| ...440204 | Mistral Small | $0.10/M | General tasks |
| ...440205 | Claude 3.5 Sonnet | $3/M | Best quality |
| ...440206 | GPT-4o Mini | $0.15/M | Balanced performance |
### Adding New Local Models
```bash
# Add new models to existing database
pnpm --filter @chat/server db:add-local-models
```
## Quick Start
1. **Get OpenRouter API key** at https://openrouter.ai/keys
2. **Create `.env`** in `apps/chat/apps/server/`:
```env
OPENROUTER_API_KEY=sk-or-v1-xxx
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/chat
MANA_AUTH_URL=http://localhost:3001
PORT=3002
```
3. **Start services**:
```bash
pnpm docker:up # Start PostgreSQL
pnpm dev:chat:full # Start auth + backend + web
```
4. **Seed database** (first time only):
```bash
pnpm --filter @chat/server db:push
pnpm --filter @chat/server db:seed
```
## Important Notes
1. **Security**: API keys are stored in the server only - never in client apps
2. **Authentication**: Uses Mana Auth (JWT tokens)
3. **Database**: PostgreSQL with Drizzle ORM (uses shared Docker container)
4. **Deployment**: Server runs on port 3002
The previous standalone "Chat Project Guide" describing a per-product
backend was deleted in the audit cleanup of 2026-04-09 — it had been
inaccurate since the consolidation. Pre-consolidation reference is in
git history.

View file

@ -1,260 +1,17 @@
# Contacts Project Guide
# Contacts — consolidated into the unified Mana app
## Project Structure
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/contacts/apps/server/` and `apps/contacts/apps/web/`
directories have been removed. Active code now lives in:
```
apps/contacts/
├── apps/
│ ├── server/ # Hono/Bun compute server (@contacts/server) - Port 3015
│ ├── landing/ # Astro marketing landing page (@contacts/landing)
│ ├── web/ # SvelteKit web application (@contacts/web) - Port 5184
│ └── mobile/ # Expo/React Native mobile app (@contacts/mobile)
├── packages/
│ └── shared/ # Shared types, utils, configs (@contacts/shared)
└── package.json
```
- **Backend compute routes**: [`apps/api/src/modules/contacts/routes.ts`](../api/src/modules/contacts/routes.ts)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/contacts/`](../mana/apps/web/src/lib/modules/contacts/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/contacts/`](../mana/apps/web/src/routes/(app)/contacts/)
- **Landing page** (still standalone): [`apps/contacts/apps/landing/`](apps/landing/)
## Commands
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
### Root Level (from monorepo root)
```bash
pnpm contacts:dev # Run all contacts apps
pnpm dev:contacts:mobile # Start mobile app
pnpm dev:contacts:web # Start web app
pnpm dev:contacts:landing # Start landing page
pnpm dev:contacts:server # Start server
pnpm dev:contacts:app # Start web + server together
pnpm dev:contacts:local # Start web + sync (no auth needed)
```
### Mobile App (apps/contacts/apps/mobile)
```bash
pnpm dev # Start Expo dev server
pnpm ios # Run on iOS simulator
pnpm android # Run on Android emulator
```
### Backend (apps/contacts/apps/backend)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
```
### Web App (apps/contacts/apps/web)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
### Landing Page (apps/contacts/apps/landing)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
```
## Technology Stack
- **Mobile**: React Native 0.81 + Expo SDK 54, NativeWind, Expo Router, Zustand
- **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS
- **Landing**: Astro 5.x, Tailwind CSS
- **Backend**: NestJS 10, Drizzle ORM, PostgreSQL
- **Types**: TypeScript 5.x
## Architecture
### Backend API Endpoints
| Endpoint | Method | Description |
| ------------------------------------- | ------ | -------------------------- |
| `/api/v1/health` | GET | Health check |
| `/api/v1/contacts` | GET | Get user's contacts |
| `/api/v1/contacts` | POST | Create new contact |
| `/api/v1/contacts/:id` | GET | Get contact details |
| `/api/v1/contacts/:id` | PATCH | Update contact |
| `/api/v1/contacts/:id` | DELETE | Delete contact |
| `/api/v1/contacts/:id/favorite` | POST | Toggle favorite |
| `/api/v1/contacts/:id/archive` | POST | Toggle archive |
| `/api/v1/contacts/:id/photo` | POST | Upload contact photo |
| `/api/v1/tags` | GET | Get user's tags |
| `/api/v1/tags` | POST | Create new tag |
| `/api/v1/tags/:id` | DELETE | Delete tag |
| `/api/v1/contacts/:id/notes` | GET | Get contact notes |
| `/api/v1/contacts/:id/notes` | POST | Add note to contact |
| `/api/v1/notes/:id` | PATCH | Update note |
| `/api/v1/notes/:id` | DELETE | Delete note |
| `/api/v1/contacts/:id/activities` | GET | Get contact activities |
| `/api/v1/contacts/:id/activities` | POST | Log activity |
| `/api/v1/import/preview` | POST | Preview file import (vCard/CSV) |
| `/api/v1/import/execute` | POST | Execute contact import |
| `/api/v1/import/template/csv` | GET | Download CSV template |
| `/api/v1/google/auth-url` | GET | Get Google OAuth URL |
| `/api/v1/google/callback` | POST | Exchange OAuth code |
| `/api/v1/google/status` | GET | Get Google connection status |
| `/api/v1/google/disconnect` | DELETE | Disconnect Google account |
| `/api/v1/google/contacts` | GET | Fetch Google contacts |
| `/api/v1/google/import` | POST | Import from Google |
| `/api/v1/export` | GET | Quick export all contacts |
| `/api/v1/export` | POST | Export with options |
| `/api/v1/organizations/:orgId/contacts` | GET | Get organization contacts |
| `/api/v1/teams/:teamId/contacts` | GET | Get team contacts |
| `/api/v1/contacts/:id/share` | POST | Share contact |
### Database Schema
**contacts** - Contact information
- `id` (UUID) - Primary key
- `user_id` (VARCHAR) - User reference
- `first_name`, `last_name`, `display_name`, `nickname` (VARCHAR)
- `email`, `phone`, `mobile` (VARCHAR)
- `street`, `city`, `postal_code`, `country` (VARCHAR)
- `company`, `job_title`, `department` (VARCHAR)
- `website`, `birthday`, `notes`, `photo_url` (VARCHAR/TEXT/DATE)
- `is_favorite`, `is_archived` (BOOLEAN)
- `organization_id`, `team_id` (UUID) - Mana integration
- `visibility` (VARCHAR) - private/team/organization/public
- `shared_with` (JSONB) - Array of user IDs
- `created_at`, `updated_at` (TIMESTAMP)
**contact_tags** - Tags for contacts
- `id` (UUID) - Primary key
- `user_id` (VARCHAR) - User reference
- `name` (VARCHAR) - Tag name
- `color` (VARCHAR) - Tag color
**contact_to_tags** - Many-to-many relation
- `contact_id` (UUID) - Contact reference
- `tag_id` (UUID) - Tag reference
**contact_notes** - Notes for contacts
- `id` (UUID) - Primary key
- `contact_id` (UUID) - Contact reference
- `user_id` (VARCHAR) - User reference
- `content` (TEXT) - Note content
- `is_pinned` (BOOLEAN)
- `created_at`, `updated_at` (TIMESTAMP)
**contact_activities** - Activity log
- `id` (UUID) - Primary key
- `contact_id` (UUID) - Contact reference
- `user_id` (VARCHAR) - User reference
- `activity_type` (VARCHAR) - created/updated/called/emailed/met/note_added
- `description` (TEXT)
- `metadata` (JSONB)
- `created_at` (TIMESTAMP)
**connected_accounts** - OAuth provider connections (Google, etc.)
- `id` (UUID) - Primary key
- `user_id` (VARCHAR) - User reference
- `provider` (VARCHAR) - Provider name (e.g., 'google')
- `provider_account_id` (VARCHAR) - Provider's user ID
- `provider_email` (VARCHAR) - Provider account email
- `access_token` (TEXT) - OAuth access token (encrypted)
- `refresh_token` (TEXT) - OAuth refresh token (encrypted)
- `token_expires_at` (TIMESTAMP) - Token expiration time
- `scope` (TEXT) - Granted OAuth scopes
- `provider_data` (JSONB) - Additional provider-specific data
- `created_at`, `updated_at` (TIMESTAMP)
### Environment Variables
#### Backend (.env)
```
NODE_ENV=development
PORT=3015
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/contacts
MANA_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5173,http://localhost:5184,http://localhost:8081
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET=contacts-photos
# Google OAuth (for contacts import)
# Get credentials from https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:5184/data?tab=import&source=google
```
#### Mobile (.env)
```
EXPO_PUBLIC_BACKEND_URL=http://localhost:3015
EXPO_PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
#### Web (.env)
```
PUBLIC_BACKEND_URL=http://localhost:3015
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Shared Packages
### @contacts/shared
- Types: `Contact`, `ContactGroup`, `ContactTag`, `ContactNote`, `ContactActivity`
- Utils: Search, filter, import/export functions
- Configs: App configuration
## Quick Input Syntax
The NewContactModal includes a NL quick-input bar that parses contact info and pre-fills form fields:
```
"Max Mustermann @ACME Corp max@example.com +49 170 1234567 #kunde"
→ firstName: Max, lastName: Mustermann, company: ACME Corp,
email: max@example.com, phone: +49 170..., tags: [kunde]
```
Recognized patterns:
- **Name**: First and last name (remaining text after extraction)
- **Company**: `@CompanyName` or `bei CompanyName` or `von CompanyName`
- **Email**: Standard email format
- **Phone**: International (+49...), German (0123...), or 6+ digit numbers
- **Tags**: `#tag1 #tag2`
Type the full text and press **Enter** to apply. Fields are pre-filled and can be edited before saving.
### Live Duplicate Detection
While typing in the name or email fields, the system checks for duplicates against IndexedDB in real-time:
- **Email**: Exact match (case-insensitive)
- **Name**: Fuzzy match (Levenshtein distance, tolerates typos)
- Shows warning with matched contact name, company, and match type
Implementation: `duplicate-detector.ts` — runs fully offline, no server calls.
## Code Style Guidelines
- **TypeScript**: Strict typing with interfaces
- **Mobile**: Functional components with hooks, Zustand for state
- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`)
- **Styling**: Tailwind CSS / NativeWind
- **Formatting**: Prettier with project config
## Important Notes
1. **Authentication**: Uses Mana Auth (JWT in Authorization header)
2. **Database**: PostgreSQL with Drizzle ORM
3. **Port**: Backend runs on port 3015, Web on port 5184 by default
4. **Storage**: Uses MinIO/S3 for contact photos via @mana/shared-storage
5. **Mana Integration**: Contacts can be linked to Organizations and Teams
The previous standalone "Contacts Project Guide" was deleted in the
audit cleanup of 2026-04-09 — it had been inaccurate since the
consolidation. Pre-consolidation reference is in git history.

View file

@ -1,210 +1,24 @@
# Context App
# Context — consolidated into the unified Mana app
AI-powered document management and context system for knowledge organization.
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/context/apps/backend/` (NestJS) and
`apps/context/apps/web/` directories have been removed. Active code now
lives in:
| App | Port | URL |
|-----|------|-----|
| Backend | 3020 | http://localhost:3020 |
| Web App | 5192 | http://localhost:5192 |
| Mobile | 8081 | Expo Go |
- **Backend compute routes**: [`apps/api/src/modules/context/routes.ts`](../api/src/modules/context/routes.ts) (AI text generation via mana-llm, server-side credit deduction)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/context/`](../mana/apps/web/src/lib/modules/context/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/context/`](../mana/apps/web/src/routes/(app)/context/)
- **Mobile app**: [`apps/context/apps/mobile/`](apps/mobile/)
## Structure
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
```
apps/context/
├── apps/
│ ├── backend/ # Hono/Bun compute server (@context/server)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── db/ # Drizzle schemas + migrations
│ │ │ ├── schema/
│ │ │ │ ├── spaces.schema.ts
│ │ │ │ ├── documents.schema.ts
│ │ │ │ ├── token-transactions.schema.ts
│ │ │ │ ├── model-prices.schema.ts
│ │ │ │ └── user-tokens.schema.ts
│ │ │ ├── connection.ts
│ │ │ ├── database.module.ts
│ │ │ ├── migrate.ts
│ │ │ └── seed.ts
│ │ ├── space/ # Space CRUD
│ │ ├── document/ # Document CRUD + versions + tags
│ │ ├── ai/ # AI generation (Azure + Google)
│ │ ├── token/ # Token balance + stats
│ │ └── common/
│ ├── web/ # SvelteKit web application (@context/web)
│ ├── mobile/ # Expo React Native app (@context/mobile)
│ └── landing/ # (Planned) Astro Landing Page
├── packages/ # Project-specific shared code
└── package.json # Workspace root
```
The previous "Context App" guide describing a NestJS backend with its
own spaces/documents/token tables was deleted in the audit cleanup of
2026-04-09 — it had been inaccurate since the consolidation. The
token-economy logic now lives in `mana-credits`. Pre-consolidation
reference is in git history.
## Development Commands
```bash
# From monorepo root
pnpm dev:context:full # Start auth + backend + web (with DB setup)
pnpm dev:context:server # Start backend only (port 3020)
pnpm dev:context:web # Start web only (port 5192)
pnpm dev:context:app # Start web + backend together
pnpm dev:context:mobile # Start mobile app
# Database
pnpm context:db:push # Push schema to database
pnpm context:db:studio # Open Drizzle Studio
pnpm context:db:seed # Seed model prices
pnpm setup:db:context # Create DB + push schema
```
## Tech Stack
| Layer | Technology |
|-------|------------|
| **Backend** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Mobile** | React Native 0.76 + Expo SDK 52, NativeWind |
| **Auth** | Mana Auth (JWT) |
| **AI** | Azure OpenAI (GPT-4.1), Google Gemini (Pro, Flash) |
| **i18n** | svelte-i18n (DE, EN) |
## Core Features
- **Spaces**: Organize documents into collections with prefix-based short IDs
- **Documents**: Text, context references, and AI prompts with versioning
- **AI Generation**: Multi-model support (Azure OpenAI, Google Gemini)
- **Token Economy**: Track and manage AI usage credits per user
- **Document Versioning**: AI-generated summaries, continuations, rewrites
## Backend API Endpoints
### Health
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Health check |
### Spaces
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/spaces` | GET | List user's spaces |
| `/api/v1/spaces` | POST | Create space |
| `/api/v1/spaces/:id` | GET | Get space details |
| `/api/v1/spaces/:id` | PUT | Update space |
| `/api/v1/spaces/:id` | DELETE | Delete space (cascades documents) |
### Documents
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/documents` | GET | List documents (?spaceId=&preview=true&limit=) |
| `/api/v1/documents/recent` | GET | Recent documents (?limit=) |
| `/api/v1/documents` | POST | Create document |
| `/api/v1/documents/:id` | GET | Get document |
| `/api/v1/documents/:id` | PUT | Update document |
| `/api/v1/documents/:id` | DELETE | Delete document |
| `/api/v1/documents/:id/tags` | PUT | Update document tags |
| `/api/v1/documents/:id/pinned` | PUT | Toggle pinned |
| `/api/v1/documents/:id/versions` | GET | Get document versions |
| `/api/v1/documents/:id/versions` | POST | Create AI version |
### AI
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/ai/generate` | POST | Generate text (server-side AI) |
| `/api/v1/ai/estimate` | POST | Estimate token cost |
### Tokens
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/tokens/balance` | GET | Get user token balance |
| `/api/v1/tokens/stats` | GET | Usage stats (?timeframe=day\|week\|month\|year) |
| `/api/v1/tokens/transactions` | GET | Transaction history (?limit=&offset=) |
| `/api/v1/tokens/models` | GET | Available model prices |
## Database Schema
### spaces
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `user_id` | TEXT | Owner |
| `name` | VARCHAR(255) | Space name |
| `description` | TEXT | Optional description |
| `settings` | JSONB | Space settings |
| `pinned` | BOOLEAN | Pinned in sidebar |
| `prefix` | VARCHAR(10) | Short ID prefix (e.g. "A") |
| `text_doc_counter` | INTEGER | Counter for text docs |
| `context_doc_counter` | INTEGER | Counter for context docs |
| `prompt_doc_counter` | INTEGER | Counter for prompt docs |
### documents
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `user_id` | TEXT | Owner |
| `space_id` | UUID | FK to spaces (cascade delete) |
| `title` | VARCHAR(500) | Document title |
| `content` | TEXT | Document content |
| `type` | VARCHAR(20) | text / context / prompt |
| `short_id` | VARCHAR(20) | Short ID (e.g. "AD1") |
| `pinned` | BOOLEAN | Pinned flag |
| `metadata` | JSONB | Tags, word count, version info |
### token_transactions
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Primary key |
| `user_id` | TEXT | User |
| `amount` | INTEGER | Tokens used (negative for usage) |
| `transaction_type` | VARCHAR(50) | usage / bonus / purchase |
| `model_used` | VARCHAR(100) | AI model name |
| `prompt_tokens` | INTEGER | Input tokens |
| `completion_tokens` | INTEGER | Output tokens |
| `cost_usd` | NUMERIC(10,6) | Actual USD cost |
### model_prices
| Column | Type | Description |
|--------|------|-------------|
| `model_name` | VARCHAR(100) | Unique model name |
| `input_price_per_1k_tokens` | NUMERIC(10,6) | Input price |
| `output_price_per_1k_tokens` | NUMERIC(10,6) | Output price |
| `tokens_per_dollar` | INTEGER | App tokens per USD |
### user_tokens
| Column | Type | Description |
|--------|------|-------------|
| `user_id` | TEXT | Primary key |
| `token_balance` | INTEGER | Current balance |
| `monthly_free_tokens` | INTEGER | Free monthly allocation |
## Environment Variables
### Backend (.env)
```env
NODE_ENV=development
PORT=3020
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/context
MANA_AUTH_URL=http://localhost:3001
AZURE_OPENAI_API_KEY=your-key
AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com/
GOOGLE_API_KEY=your-key
```
### Web (.env)
```env
PUBLIC_BACKEND_URL=http://localhost:3020
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Important Patterns
1. **API Client pattern** - All web services use `@mana/shared-api-client` (Go-style `{ data, error }`)
2. **Svelte 5 runes** - `$state`, `$derived`, `$effect` throughout
3. **Server-side AI keys** - API keys only on backend, never in frontend
4. **Auto word/token count** - Backend calculates on create/update
5. **Optimistic updates** - Immediate UI feedback in stores
6. **Document versioning** - AI generations linked via `parent_document` in metadata
> **Note:** The legacy `apps/context/pnpm-lock.yaml` (242 KB, separate
> workspace setup) and the broken `dev:web` / `dev:server` filter scripts
> in `apps/context/package.json` are tracked in audit items #2 and #26.

View file

@ -3,9 +3,6 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "turbo run dev",
"dev:web": "pnpm --filter @context/web dev",
"dev:server": "pnpm --filter @context/server dev",
"dev:mobile": "pnpm --filter @context/mobile dev"
}
}

View file

@ -1,108 +1,21 @@
# Guides — CLAUDE.md
# Guides — consolidated into the unified Mana app
Mana Guides is a local-first step-by-step guide app (SOPs, recipes, tutorials, learning paths).
Port: **5200** (web), **3027** (server)
Theme: Teal `#0d9488`
Tier: `beta`
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/guides/apps/server/` and `apps/guides/apps/web/`
directories have been removed. Active code now lives in:
## Apps
- **Backend compute routes**: [`apps/api/src/modules/guides/routes.ts`](../api/src/modules/guides/routes.ts) (URL/text/AI import, structured guide generation via mana-search + mana-llm)
- **Frontend module** (mostly static content): [`apps/mana/apps/web/src/lib/modules/guides/`](../mana/apps/web/src/lib/modules/guides/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/guides/`](../mana/apps/web/src/routes/(app)/guides/)
| App | Package | Port | Description |
|-----|---------|------|-------------|
| `apps/web` | `@guides/web` | 5200 | SvelteKit 5 local-first UI |
| `apps/server` | `@guides/server` | 3025 | Hono/Bun compute server (import, share) |
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
## Dev Commands
The previous standalone "Guides" guide was deleted in the audit cleanup
of 2026-04-09 — it had been inaccurate since the consolidation.
Pre-consolidation reference is in git history.
```bash
pnpm dev:guides:web # Web only
pnpm dev:guides:server # Server only
pnpm dev:guides:app # Server + web
pnpm dev:guides:local # Sync + server + web (no auth)
pnpm dev:guides:full # Auth + sync + server + web
```
## Architecture
**Local-first**: All CRUD goes through `guidesStore` (Dexie.js IndexedDB), synced via mana-sync.
**Server (port 3025)**: Compute-only — web import via mana-search + mana-llm, shareable links.
### Data Model
```
LocalGuide → has many LocalSection, LocalStep
LocalSection → has many LocalStep (order field)
LocalStep → belongs to LocalGuide, optional LocalSection
LocalCollection → has many LocalGuide (ordered list)
LocalRun → belongs to LocalGuide, stepStates: Record<stepId, StepState>
```
### Collections
| Collection | Index | Description |
|------------|-------|-------------|
| `guides` | category, difficulty, collectionId | Guide library |
| `sections` | guideId, order | Optional sections within a guide |
| `steps` | guideId, sectionId, order | Steps (instruction/warning/tip/checkpoint/code) |
| `collections` | type | Path or Library groupings |
| `runs` | guideId, startedAt | Execution history |
## Routes
```
/ Library (guide grid, search, filters)
/guide/[id] Guide detail + edit mode + run history
/guide/[id]/run Run mode (?mode=scroll|focus)
/collections Collections grid
/collections/[id] Collection detail with progress
/history All run history
/shared/[token] Public shared guide (no auth needed) — Phase 4
/(auth)/login Login page
```
## Server Routes (port 3025)
```
POST /api/v1/import/url → fetch URL → mana-search extract → mana-llm → { guide, sections }
POST /api/v1/import/text → raw text/markdown → mana-llm → { guide, sections }
POST /api/v1/import/ai → AI prompt → mana-llm → { guide, sections }
POST /api/v1/share → create shareable link (7-day TTL) → { token, url, expiresAt }
GET /api/v1/share/:token → retrieve shared guide snapshot
GET /health → service health check
```
## Environment Variables
```env
# Web
PUBLIC_SYNC_SERVER_URL=ws://localhost:3050
PUBLIC_GUIDES_SERVER_URL=http://localhost:3027
# Server
PORT=3027
CORS_ORIGINS=http://localhost:5200
MANA_SEARCH_URL=http://localhost:3021
MANA_LLM_URL=http://localhost:3030
PUBLIC_BASE_URL=http://localhost:5200
```
## Key Files
| File | Purpose |
|------|---------|
| `apps/web/src/lib/data/local-store.ts` | 5 Dexie collections with TypeScript types |
| `apps/web/src/lib/data/guest-seed.ts` | 3 demo guides, 1 collection for onboarding |
| `apps/web/src/lib/stores/guides.svelte.ts` | Guide/section/step/collection mutations |
| `apps/web/src/lib/stores/runs.svelte.ts` | Run start/step state/complete mutations |
| `apps/web/src/routes/(app)/guide/[id]/run/+page.svelte` | Scroll + focus run modes |
| `apps/server/src/routes/import.ts` | URL/text/AI import via mana-llm |
| `apps/server/src/routes/share.ts` | Shareable guide links (in-memory MVP) |
## Phase Status
| Phase | Status | Description |
|-------|--------|-------------|
| 1 | Done | Core CRUD, local-first, guest seed, library/detail/run views |
| 2 | Done | Collections, StepEditorModal, CollectionEditModal, inline step add |
| 3 | In progress | Hono server (import + share done), ImportModal frontend, share button |
| 4 | Planned | DB persistence for shares, /shared/[token] public route, XP/gamification |
> **Note:** Guides is one of the few modules that doesn't own a Dexie
> collection (the catalogue is hardcoded in `index.ts`); only `tags` are
> stored. See `apps/mana/CLAUDE.md` for the standard module pattern this
> intentionally diverges from.

View file

@ -1,459 +1,63 @@
# CLAUDE.md
# Memoro
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
AI-powered voice recording + memo management. Memoro is a **hybrid**: its
frontend was consolidated into the unified Mana web app, but its backend
was kept as standalone services (Hono + Supabase) because of the
audio-processing pipeline and the legacy Supabase Storage bucket layout.
## Repository Overview
## Where things live
Memoro is a monorepo containing an AI-powered voice recording and memo management application with two apps:
| Surface | Path | Notes |
|---------|------|-------|
| **Web frontend** (local-first, in unified Mana app) | [`apps/mana/apps/web/src/lib/modules/memoro/`](../mana/apps/web/src/lib/modules/memoro/) | Same module pattern as every other module — Dexie collections, Svelte 5 stores, runes UI. Reachable via `/memoro` route in mana.how. |
| **Native mobile app** | [`apps/memoro/apps/mobile/`](apps/mobile/) | React Native + Expo SDK 55. Talks directly to `memoro-server` (NOT to mana.how). Build via EAS, see `apps/mobile/eas.json`. |
| **Backend compute** | [`apps/memoro/apps/server/`](apps/server/) (`@memoro/server`) | Hono + Bun. Handles memo CRUD, transcription callbacks, spaces, invites, credits, settings, cleanup, meetings. **Still uses Supabase** for some legacy state. Deployed as `memoro-server` container. |
| **Audio processing** | [`apps/memoro/apps/audio-server/`](apps/audio-server/) | Separate Hono+Bun service for audio uploads + transcoding. Deployed as `memoro-audio-server` container. |
| **Landing page** | [`apps/memoro/apps/landing/`](apps/landing/) | Astro static landing → Cloudflare Pages |
- **Mobile App** (`apps/mobile/`): React Native + Expo cross-platform app (iOS, Android, Web)
- **Web App** (`apps/web/`): SvelteKit companion web application
## Why memoro is not (yet) in `apps/api`
Both apps share the same Supabase backend.
Most consolidated products migrated their compute routes into
`apps/api/src/modules/{name}/`. Memoro stayed standalone because:
## Development Commands
1. **Audio pipeline.** The audio-server runs background transcoding/
upload jobs that don't fit the request-response shape of `apps/api`.
2. **Legacy Supabase coupling.** Memo and storage records still live
in Supabase tables (`storage.objects`, RLS policies on `memos`).
Migrating to mana_platform was descoped in the consolidation sprint.
3. **Three deploy targets.** `memoro-server`, `memoro-audio-server`,
and the mobile app all need to coordinate. Easier to evolve as one
unit while migration is in flight.
### Mobile App (`apps/mobile/`)
A future cleanup item is to either fold the routes into `apps/api`
(once Supabase is gone) or document this exception explicitly in the
root architecture overview.
```bash
# Development
npm start # Start Expo dev server
npm run start:dev # Start with dev environment
npm run start:prod # Start with prod environment
npm run ios # Run on iOS simulator
npm run android # Run on Android emulator
npm run web # Run web version
npm run web:dev # Run web with dev environment
## Production deployment
# Code Quality
npm run lint # Run ESLint and Prettier check
npm run lint:fix # Auto-fix linting issues
npm run lint:unused # Find unused imports/vars
npm run format # Format code with ESLint + Prettier
# Build & Deploy
npm run prebuild # Generate native projects
npm run rebuild # Clean rebuild (removes node_modules, ios/, android/)
npm run web:build # Build for web deployment
eas build --profile development # Development build
eas build --profile preview # Preview build
eas build --profile production # Production build
```
### Web App (`apps/web/`)
```bash
npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build
npm run check # Run svelte-check
npm run check:watch # Watch mode for svelte-check
```
## Architecture
### Mobile App Architecture
**Framework Stack:**
- React Native 0.83.2 + Expo SDK 55
- Expo Router (file-based routing)
- TypeScript
- NativeWind (Tailwind CSS for React Native)
- Zustand (state management)
**Key Design Patterns:**
1. **Feature-Based Architecture** (`features/`):
- Each feature is self-contained with its own services, hooks, components, and stores
- Features: auth, audioRecordingV2, memos, spaces, credits, subscription, i18n, theme, etc.
- 33 feature modules in total
2. **Atomic Design System** (`components/`):
- `atoms/`: Basic UI components (Button, Input, Text, Icon, etc.)
- `molecules/`: Composite components (MemoPreview, RecordingBar, TagSelector, etc.)
- `organisms/`: Complex components (AudioRecorder, Memory, TranscriptDisplay, etc.)
- `statistics/`: Specialized analytics components
3. **Route Structure** (`app/`):
- `(public)/`: Unauthenticated routes (login, register)
- `(protected)/`: Authenticated routes with auth guard
- `(tabs)/`: Main tab navigation (home, memos, spaces)
- `(memo)/[id]`: Dynamic memo detail pages
- `(space)/[id]`: Dynamic space detail pages
- Uses Expo Router's file-based routing with typed routes enabled
### Authentication System
Uses a **middleware-based authentication bridge** between the app and Supabase:
Both backends are part of `docker-compose.macmini.yml`:
```
Mobile App → Middleware Auth Service → Supabase
memoro-server (apps/memoro/apps/server) — main backend
memoro-audio-server (apps/memoro/apps/audio-server) — audio worker
```
**Key Points:**
- Middleware issues three tokens: `manaToken`, `appToken` (Supabase-compatible JWT), `refreshToken`
- Tokens stored securely via platform-specific `safeStorage` utility
- Auth state managed via `AuthContext` provider
- Supabase client configured to use JWT from middleware
- Row Level Security (RLS) policies use JWT claims (`sub`, `role`, `app_id`)
- Supports email/password, Google Sign-In, and Apple Sign-In
- Automatic token refresh mechanism
The mobile app builds via EAS — not part of the monorepo CI.
See `apps/mobile/features/auth/README.md` for detailed authentication flow.
## Known issues / cleanup items
### Audio Recording System
- **`@mana/notify-client` is imported by `apps/memoro/apps/server/src/lib/notify.ts:6` but NOT declared as a dependency** in `apps/memoro/apps/server/package.json`. Currently works via hoisted node_modules but should either be added as a workspace dep or replaced with a direct call to `mana-notify`. Tracked in `docs/REFACTORING_AUDIT_2026_04.md` items #29.
- **`apps/memoro/apps/server` still pulls `@supabase/supabase-js`** — not a bug, but flagged as a dependency to remove once Supabase migration completes.
- **No `apps/memoro/apps/web`** — was removed during the consolidation. The old SvelteKit "companion web app" lives now under `apps/mana/apps/web/src/lib/modules/memoro/`.
**AudioRecordingV2** is the current audio recording implementation:
## For monorepo-wide patterns
- Uses `expo-audio` (migrated from deprecated `expo-av`)
- Platform-specific services: `IOSRecordingService`, `AndroidRecordingService`
- Zustand store for state management (`recordingStore`)
- Comprehensive error handling with retry strategies
- Android: Foreground service with wake locks
- iOS: Background audio capability with `mixWithOthers` mode
- Real-time status updates via polling
- Prevents zero-byte recordings with validation
- **Background recording works correctly** - continues when app is backgrounded or locked
See [root `CLAUDE.md`](../../CLAUDE.md) for the overall architecture and
[`apps/mana/CLAUDE.md`](../mana/CLAUDE.md) for the unified web app's
module pattern (which the memoro frontend follows).
**iOS Background Recording:**
- Uses `interruptionMode: 'mixWithOthers'` for background recording support
- Recording continues when pressing home button, switching apps, or locking device
- Audio session automatically restored when returning to foreground
- JavaScript timers suspended in background, but native recording continues
- Handles real interruptions (phone calls, Siri) automatically
**Recording Options:**
- High quality: M4A format with AAC encoding (MONO for compatibility)
- Presets: HIGH_QUALITY, MEDIUM_QUALITY, LOW_QUALITY, VOICE_MEMO
- Max duration and size limits
- Pause/resume support
- Audio level metering for waveform visualization
- Optimized for voice (MONO, 96 quality) to prevent FFmpeg 'chnl' box errors
**Key Technical Details:**
- MONO recording prevents iOS spatial audio metadata issues
- Audio session verification on cold start prevents first-recording failures
- Status polling restarts when app returns from background
- Full duration captured (foreground + background time)
See `apps/mobile/features/audioRecordingV2/README.md` for full details.
See `apps/mobile/features/audioRecordingV2/TROUBLESHOOTING.md` for bug fixes and solutions.
### AI Processing System
**Blueprints:**
- Reusable AI analysis patterns for different use cases
- Examples: Text Analysis, Creative Writing, Meeting Notes
- Each blueprint has localized advice tips (32 languages)
- Stored in Supabase with public/private visibility
**Prompts:**
- Specific AI tasks for content transformation
- Examples: Summary, To-Do extraction, Translation, Q&A
- Associated with blueprints via `blueprint_prompts` join table
- Multi-language support (German/English minimum)
**Content Organization:**
- 8 categories: Coaching, Crafts, Healthcare, Journal, Journalism, Office, Sales, University
- Categories provide contextual grouping for blueprints/prompts
See `apps/mobile/docs/blueprints_and_prompts.md` for full documentation.
### Theme System
**Multi-Theme Support:**
- 4 theme variants: Lume (gold), Nature (green), Stone (slate), Ocean (blue)
- Each theme has light and dark mode variants
- 13 semantic color tokens per theme (primary, secondary, borders, backgrounds, text)
- Theme state managed via `ThemeProvider` context
- Dark mode detection + manual override
- All colors defined in `tailwind.config.js`
**Markdown Rendering:**
- Full Markdown support in memo display
- Theme-aware styles adapt to light/dark mode
- Centralized styles in `features/theme/markdownStyles.ts`
- Hybrid rendering with auto-detection
### Spaces (Collaboration)
**Team Workspaces:**
- Create unlimited collaborative spaces
- Role-based permissions (owner, member)
- Memo sharing within spaces
- Email-based invitation system
- Credit pools shared among team members
- Real-time sync via Supabase Realtime
**Backend Integration:**
- RESTful API for space management
- RLS policies for access control
- Space-specific memo filtering
See `apps/mobile/docs/SPACES.md` for implementation details.
### Subscription & Credits
**Mana Credit System:**
- Backend-driven transparent pricing
- Real-time credit validation before operations
- Usage tracking and analytics
- Credit sharing in team spaces
- Free tier: 150 Mana + 5 daily Mana
**RevenueCat Integration:**
- Cross-platform (iOS, Android, Web)
- Subscription lifecycle management
- User identification tied to auth
- Purchase restoration across devices
- 4 individual plans: Stream (€5.99), River (€14.99), Lake (€29.99), Ocean (€49.99)
- Team and Enterprise plans available
### Internationalization
**32 Languages Supported:**
- Arabic, Bengali, Bulgarian, Chinese, Czech, Danish, Dutch, English, Estonian, Finnish, French, Gaelic, German, Greek, Hindi, Croatian, Hungarian, Indonesian, Italian, Japanese, Korean, Lithuanian, Latvian, Maltese, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovenian, Spanish, Swedish, Turkish, Ukrainian, Urdu, Vietnamese
**Implementation:**
- `react-i18next` for translations
- Automatic device language detection
- Persistent user preference storage
- RTL support for Arabic/Hebrew
- Translation files in `features/i18n/translations/`
### Real-Time Features
**Supabase Realtime:**
- Live memo updates (INSERT, UPDATE, DELETE)
- Real-time collaboration in spaces
- `MemoRealtimeProvider` context for subscriptions
- Automatic reconnection handling
- RLS-aware subscriptions
### Platform-Specific Notes
**Web Platform:**
- Uses `.web.ts` file extensions for web-specific implementations
- `safeStorage.web.ts` uses localStorage (vs AsyncStorage on native)
- Web Audio API for recording (vs expo-audio)
- Some features unavailable: push notifications, haptics, native gestures
**iOS:**
- Background audio capability required
- Audio session management
- Apple Sign-In integration
- RevenueCat StoreKit 2
**Android:**
- Foreground service for recording
- Wake lock to prevent sleep
- Android 16+ requires foreground to start recording
- Google Sign-In integration
## Environment Configuration
The mobile app uses environment-specific `.env` files:
- `.env.dev`: Development environment (copy from `.env.dev.example`)
- `.env.prod`: Production environment (copy from `.env.prod.example`)
- `.env.local`: Active environment (auto-generated by npm scripts)
**Key Environment Variables:**
- `EXPO_PUBLIC_SUPABASE_URL`: Supabase project URL
- `EXPO_PUBLIC_SUPABASE_ANON_KEY`: Supabase anon key
- `EXPO_PUBLIC_MIDDLEWARE_API_URL`: Middleware auth service URL
- `EXPO_PUBLIC_APPID`: Application ID for middleware
- RevenueCat keys for iOS/Android
## Code Quality
**Linting:**
- ESLint with TypeScript plugin
- React/React Native rules
- Unused imports auto-removal
- Configuration in `eslint.config.js`
**Formatting:**
- Prettier with Tailwind plugin
- Auto-format on save recommended
**TypeScript:**
- Strict mode enabled
- Typed routes from Expo Router
- Type definitions in `types/` and feature-specific types
## Migration Notes
**Expo SDK 55 (Current):**
- React Native 0.83.2, React 19.2
- Native `allowsBackgroundRecording` support in expo-audio (no more workarounds needed)
- All Expo packages use `^55.x.x` version scheme
- New Architecture is the default (Legacy Architecture dropped)
- Android compileSdkVersion/targetSdkVersion 36
**Expo SDK 54 Migration (Historical):**
- Migrated from `expo-av` to `expo-audio`
- New audio recording API (`AudioModule.AudioRecorder`)
- Status polling instead of callbacks
- See `EXPO_54_AUDIO_RECORDING_MIGRATION.md`
**SvelteKit Web App:**
- Separate web app being built as companion
- Shares Supabase backend with mobile app
- See `SVELTEKIT_MIGRATION_ANALYSIS.md` for migration plan
## Testing Strategy
**Manual Testing:**
- Test on both iOS and Android before commits
- Verify web platform compatibility
- Check dark mode and all theme variants
- Test with different languages
**Platform Matrix:**
- iOS (simulator + device)
- Android (emulator + device)
- Web (Chrome, Safari, Firefox)
## Common Patterns
### Creating a New Feature
1. Create feature directory in `features/`
2. Add subdirectories: `components/`, `hooks/`, `services/`, `store/`, `types/`
3. Export public API via `index.ts`
4. Add feature-specific README if complex
5. Update this CLAUDE.md if architectural
### Adding a New Route
1. Add file in `app/` directory following Expo Router conventions
2. Use `(protected)/` group if authentication required
3. Use `[id]` for dynamic routes
4. Enable typed routes in `app.json` (already enabled)
5. Import route types from `expo-router`
### Working with Zustand Stores
```typescript
// Create store
export const useMyStore = create<MyState>((set, get) => ({
// state
data: null,
// actions
setData: (data) => set({ data }),
// computed/derived
getData: () => get().data,
}));
```
Stores are located in:
- Global: `store/store.ts`
- Feature-specific: `features/[feature]/store/`
### Platform-Specific Code
Use file extensions for platform-specific implementations:
- `file.ts`: Default (mobile)
- `file.web.ts`: Web platform
- `file.ios.ts`: iOS only
- `file.android.ts`: Android only
Metro bundler automatically resolves based on platform.
### Error Handling
1. Use feature-specific error types
2. Provide user-friendly messages
3. Include retry mechanisms where appropriate
4. Log errors to console for debugging
5. Consider Sentry integration for production
## Build and Deployment
**EAS Build Profiles:**
- `development`: Dev client with debugging
- `preview`: Internal distribution (TestFlight/Google Play Internal)
- `simulator`: iOS simulator build
- `production`: Auto-increment version, store-ready
**Environment Selection:**
EAS profiles automatically load correct environment via `EXPO_PUBLIC_USE_ENV_FILE` in `eas.json`.
**Version Management:**
- iOS: `buildNumber` in `app.json`
- Android: `versionCode` in `app.json`
- Production profile auto-increments both
## Important Files
- `app.json`: Expo configuration, plugins, permissions
- `eas.json`: EAS Build configuration
- `package.json`: Dependencies and scripts
- `tailwind.config.js`: Theme colors and styling
- `eslint.config.js`: Linting rules
- `babel.config.js`: Babel configuration
- `metro.config.js`: Metro bundler configuration (if present)
- `types/supabase.ts`: Auto-generated Supabase types
## Database Schema
The app uses Supabase with the following key tables:
- `memos`: Audio recordings and transcriptions
- `memories`: AI-generated insights from memos
- `blueprints`: AI analysis templates
- `prompts`: AI task templates
- `blueprint_prompts`: Many-to-many join table
- `categories`: Organization categories
- `tags`: User-defined tags
- `memo_tags`: Many-to-many join table
- `spaces`: Collaborative workspaces
- `space_members`: User-space relationships
- `profiles`: User profiles and settings
All tables use RLS policies based on JWT claims.
## Auto-Delete Audio Files (30-Day Retention)
When users enable `autoDeleteAudiosAfter30Days` in their settings, audio files older than 30 days are automatically deleted while preserving memo records (transcripts, metadata).
**Setting Location:** `app_settings.memoro.autoDeleteAudiosAfter30Days` (default: `false`)
**Two Cleanup Mechanisms:**
1. **Cloud Storage Cleanup** (memoro-service):
- Daily cron job at 3 AM UTC via Google Cloud Scheduler
- Queries `storage.objects` table for files older than 30 days
- Deletes from Supabase Storage bucket `user-uploads`
- Updates memo `source` field: `{ audio_path: null, audio_deleted: true, audio_deleted_at: timestamp }`
2. **Local Device Cleanup** (mobile app):
- Runs on app launch after successful authentication
- Throttled to once per 24 hours
- Uses `fileStorageService.cleanupOldFiles()` with 30-day retention
- Implementation: `features/storage/services/localAudioCleanup.ts`
**Key Files:**
- `memoro-service/src/cleanup/` - Cloud cleanup service
- `mana-middleware/src/modules/users/services/user-settings.service.ts` - User settings query
- `apps/mobile/features/storage/services/localAudioCleanup.ts` - Local device cleanup
- `apps/mobile/features/auth/contexts/AuthContext.tsx` - Cleanup trigger after auth
## Known Issues
1. **Android 16+ Recording**: Must be in foreground to start recording
2. **Zero-byte Recordings**: Occasional issue on some Android devices (retry mechanism in place)
3. **Token Refresh**: Email may not be in refreshed token (stored separately as workaround)
4. **Web Platform**: Limited functionality vs native (no push notifications, haptics, etc.)
## Additional Documentation
- `apps/mobile/README.md`: Full mobile app documentation
- `apps/web/README.md`: Web app documentation
- `features/auth/README.md`: Authentication system details
- `features/audioRecordingV2/README.md`: Audio recording implementation
- `docs/blueprints_and_prompts.md`: AI processing system
- `docs/SPACES.md`: Collaboration features
- `SVELTEKIT_MIGRATION_ANALYSIS.md`: Web app migration plan
The previous 459-line "Memoro repository overview" describing memoro as
a standalone monorepo with `mana-middleware` and a bespoke auth bridge
was deleted in the audit cleanup of 2026-04-09. It pre-dated the
integration into the Mana monorepo and described an architecture that
no longer exists. Pre-consolidation reference is in git history.

View file

@ -1,37 +1,17 @@
# Moodlit — Ambient Lighting & Mood App
# Moodlit — consolidated into the unified Mana app
## Architecture
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/moodlit/apps/server/` and `apps/moodlit/apps/web/`
directories have been removed. Active code now lives in:
Local-first for moods/sequences, Hono/Bun server for preset library.
- **Backend compute routes**: [`apps/api/src/modules/moodlit/routes.ts`](../api/src/modules/moodlit/routes.ts)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/moodlit/`](../mana/apps/web/src/lib/modules/moodlit/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/moodlit/`](../mana/apps/web/src/routes/(app)/moodlit/)
- **Landing page** (still standalone): [`apps/moodlit/apps/landing/`](apps/landing/)
```
Browser → IndexedDB (Moods, Sequences)
↕ sync
mana-sync → PostgreSQL
```
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
## Project Structure
```
apps/moodlit/
├── apps/
│ ├── web/ # SvelteKit web app (local-first)
│ ├── server/ # Hono/Bun (preset moods API)
│ └── landing/ # Astro landing page
└── package.json
```
## Commands
```bash
pnpm dev:moodlit:web # SvelteKit dev server
pnpm dev:moodlit:server # Hono/Bun server (port 3073)
pnpm dev:moodlit:landing # Landing page
```
## Local-First Collections
| Collection | Fields |
|-----------|--------|
| `moods` | name, colors (hex array), animation, isDefault |
| `sequences` | name, moodIds, duration (seconds) |
The previous standalone "Moodlit" guide was deleted in the audit cleanup
of 2026-04-09 — it had been inaccurate since the consolidation.
Pre-consolidation reference is in git history.

View file

@ -1,8 +1,5 @@
{
"name": "@mana/moodlit",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "turbo run dev"
}
"private": true
}

View file

@ -1,67 +1,17 @@
# News Hub — AI News Reader & Personal Library
# News Hub — consolidated into the unified Mana app
## Architecture
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/news/apps/server/` and `apps/news/apps/web/` directories
have been removed. Active code now lives in:
Local-first for saved articles, Hono/Bun server for content extraction and AI feed.
- **Backend compute routes**: [`apps/api/src/modules/news/routes.ts`](../api/src/modules/news/routes.ts) (Mozilla Readability extraction, AI feed)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/news/`](../mana/apps/web/src/lib/modules/news/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/news/`](../mana/apps/web/src/routes/(app)/news/)
- **Landing page** (still standalone): [`apps/news/apps/landing/`](apps/landing/)
```
Browser → IndexedDB (Saved Articles, Categories)
↕ sync
mana-sync → PostgreSQL
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
Browser → Hono Server → Content Extraction (Mozilla Readability)
→ AI Feed (from sync_changes)
```
## Project Structure
```
apps/news/
├── apps/
│ ├── web/ # SvelteKit web app (local-first)
│ ├── server/ # Hono/Bun (extraction, feed API)
│ └── landing/ # Astro marketing page
└── package.json
```
## Tech Stack
| Layer | Technology |
|-------|-----------|
| **Web** | SvelteKit 2, Svelte 5 (runes), Tailwind CSS 4 |
| **Server** | Hono + Bun, Mozilla Readability, JSDOM |
| **Data** | Local-first (Dexie.js + mana-sync) |
| **Auth** | mana-auth (Better Auth + EdDSA JWT) |
## Commands
```bash
pnpm dev:news:web # SvelteKit dev server
pnpm dev:news:server # Hono/Bun server (port 3071)
pnpm dev:news:local # Web + Sync + Server (no auth)
pnpm dev:news:full # Everything incl. auth
```
## Hono Server Routes
| Route | Auth | Description |
|-------|------|-------------|
| `GET /health` | No | Health check |
| `GET /api/v1/feed` | No | AI article feed (type, categoryId, limit, offset) |
| `GET /api/v1/feed/:id` | No | Single article |
| `POST /api/v1/extract/preview` | No | Preview URL content extraction |
| `POST /api/v1/extract/save` | JWT | Extract + return article data |
## Local-First Collections
| Collection | Purpose |
|-----------|---------|
| `articles` | Saved articles (user_saved) + AI feed cache |
| `categories` | Article categories |
## Key Patterns
- **Content Extraction**: Mozilla Readability + JSDOM for robust HTML parsing
- **Saved Articles**: Local-first via IndexedDB, sync to server
- **AI Feed**: Loaded from Hono server, not local-first (server-generated)
- **Auth**: Guest mode allowed, sync starts on login
The previous standalone "News Hub" guide was deleted in the audit cleanup
of 2026-04-09 — it had been inaccurate since the consolidation.
Pre-consolidation reference is in git history.

View file

@ -1,8 +1,5 @@
{
"name": "@mana/news",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "turbo run dev"
}
"private": true
}

View file

@ -1,368 +1,17 @@
# NutriPhi Project Guide
# NutriPhi — consolidated into the unified Mana app
## Overview
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/nutriphi/apps/backend/` and `apps/nutriphi/apps/web/`
directories have been removed. Active code now lives in:
**NutriPhi** is an AI-powered nutrition tracking app that allows users to photograph their meals and receive instant nutritional analysis. It uses Google Gemini for image analysis and provides personalized recommendations.
- **Backend compute routes**: [`apps/api/src/modules/nutriphi/routes.ts`](../api/src/modules/nutriphi/routes.ts) (Gemini meal-photo analysis + text analysis + recommendations)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/nutriphi/`](../mana/apps/web/src/lib/modules/nutriphi/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/nutriphi/`](../mana/apps/web/src/routes/(app)/nutriphi/)
- **Landing page** (still standalone): [`apps/nutriphi/apps/landing/`](apps/landing/)
| App | Port | URL |
|-----|------|-----|
| Backend | 3023 | http://localhost:3023 |
| Web App | 5180 | http://localhost:5180 |
| Landing Page | 4323 | http://localhost:4323 |
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
## Project Structure
```
apps/nutriphi/
├── apps/
│ ├── backend/ # Hono/Bun compute server (@nutriphi/server)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── db/ # Drizzle schemas
│ │ │ ├── schema/index.ts
│ │ │ └── db.ts
│ │ ├── meal/ # Meal CRUD
│ │ ├── goals/ # User goals
│ │ ├── favorites/ # Favorite meals
│ │ ├── analysis/ # Gemini AI integration
│ │ ├── stats/ # Daily/weekly statistics
│ │ ├── recommendations/ # AI hints & coaching
│ │ └── health/
│ │
│ ├── web/ # SvelteKit web application (@nutriphi/web)
│ │ └── src/
│ │ ├── lib/
│ │ │ ├── api/client.ts
│ │ │ ├── stores/
│ │ │ │ ├── auth.svelte.ts
│ │ │ │ └── meals.svelte.ts
│ │ │ └── components/
│ │ │ ├── Header.svelte
│ │ │ ├── DailySummary.svelte
│ │ │ ├── MealList.svelte
│ │ │ ├── AddMealButton.svelte
│ │ │ └── ProgressRing.svelte
│ │ └── routes/
│ │ ├── +layout.svelte
│ │ ├── +page.svelte # Dashboard
│ │ ├── login/+page.svelte
│ │ └── add/+page.svelte # Photo/text input
│ │
│ └── landing/ # Astro marketing page (@nutriphi/landing)
├── packages/
│ └── shared/ # Shared types, utils, constants (@nutriphi/shared)
│ └── src/
│ ├── types/index.ts
│ ├── constants/index.ts
│ └── utils/index.ts
├── package.json
└── CLAUDE.md
```
## Commands
### Root Level (from monorepo root)
```bash
# Start all apps
pnpm nutriphi:dev
# Individual apps
pnpm dev:nutriphi:server # Backend (port 3015)
pnpm dev:nutriphi:web # Web app (port 5180)
pnpm dev:nutriphi:landing # Landing page (port 4323)
pnpm dev:nutriphi:app # Web + backend together
# Database
pnpm nutriphi:db:push # Push schema to database
pnpm nutriphi:db:studio # Open Drizzle Studio
```
### Backend (apps/nutriphi/apps/backend)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
```
### Web App (apps/nutriphi/apps/web)
```bash
pnpm dev # Start dev server (port 5180)
pnpm build # Build for production
```
### Landing Page (apps/nutriphi/apps/landing)
```bash
pnpm dev # Start dev server (port 4323)
pnpm build # Build for production
```
## Technology Stack
| Layer | Technology |
|-------|------------|
| **Backend** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **AI** | Google Gemini 2.5 Flash |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Landing** | Astro 5.x, Tailwind CSS |
| **Auth** | Mana Auth (JWT) |
## Architecture
### Core Features
1. **Photo Analysis** - Take a photo, Gemini identifies foods and calculates nutrition
2. **Text Input** - Alternative: describe your meal in text
3. **Full Nutrition** - Calories, macros, vitamins, minerals
4. **Daily Goals** - Set and track calorie/macro targets
5. **AI Coaching** - Personalized tips based on eating patterns
6. **Favorites** - Save frequently eaten meals
7. **Privacy-First** - Photos are never stored, only analysis results
### API Endpoints
#### Health
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/health` | GET | Health check |
#### Analysis
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/analysis/photo` | POST | Analyze photo (Base64) |
| `/api/v1/analysis/text` | POST | Analyze text description |
#### Meals
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/meals` | GET | List meals (query by date) |
| `/api/v1/meals` | POST | Create meal |
| `/api/v1/meals/:id` | GET | Get meal details |
| `/api/v1/meals/:id` | PATCH | Update meal |
| `/api/v1/meals/:id` | DELETE | Delete meal |
#### Goals
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/goals` | GET | Get user goals |
| `/api/v1/goals` | POST | Set/update goals |
| `/api/v1/goals` | DELETE | Delete goals |
#### Favorites
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/favorites` | GET | List favorites |
| `/api/v1/favorites` | POST | Create favorite |
| `/api/v1/favorites/:id/use` | POST | Increment usage count |
| `/api/v1/favorites/:id` | DELETE | Delete favorite |
#### Stats
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/stats/daily` | GET | Daily summary |
| `/api/v1/stats/weekly` | GET | Weekly stats |
#### Recommendations
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/recommendations` | GET | List active recommendations |
| `/api/v1/recommendations/:id/dismiss` | POST | Dismiss recommendation |
### Database Schema
#### user_goals
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| user_id | UUID | User ID |
| daily_calories | INTEGER | Daily calorie target |
| daily_protein | INTEGER | Protein target (g) |
| daily_carbs | INTEGER | Carbs target (g) |
| daily_fat | INTEGER | Fat target (g) |
#### meals
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| user_id | UUID | User ID |
| date | TIMESTAMP | Meal date/time |
| meal_type | VARCHAR | breakfast/lunch/dinner/snack |
| input_type | VARCHAR | photo/text |
| description | TEXT | AI-generated description |
| confidence | REAL | AI confidence (0-1) |
#### meal_nutrition
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| meal_id | UUID | FK to meals |
| calories | REAL | Calories (kcal) |
| protein | REAL | Protein (g) |
| carbohydrates | REAL | Carbs (g) |
| fat | REAL | Fat (g) |
| fiber | REAL | Fiber (g) |
| sugar | REAL | Sugar (g) |
| vitamin_* | REAL | Various vitamins |
| calcium, iron, etc. | REAL | Minerals |
#### favorite_meals
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| user_id | UUID | User ID |
| name | VARCHAR | Favorite name |
| nutrition | JSONB | Cached nutrition data |
| usage_count | INTEGER | Times used |
#### recommendations
| Column | Type | Description |
|--------|------|-------------|
| id | UUID | Primary key |
| user_id | UUID | User ID |
| type | VARCHAR | hint/coaching |
| message | TEXT | Recommendation text |
| dismissed | BOOLEAN | User dismissed |
## Environment Variables
### Backend (.env)
```env
NODE_ENV=development
PORT=3023
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/nutriphi
MANA_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5180,http://localhost:4323
# Gemini AI (uses gemini-2.5-flash model)
GEMINI_API_KEY=your-gemini-api-key
```
> **Note:** Get your API key from https://aistudio.google.com/apikey
### Web (.env)
```env
PUBLIC_BACKEND_URL=http://localhost:3023
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Shared Package (@nutriphi/shared)
**Types:**
- `UserGoals` - Daily nutrition targets
- `Meal`, `MealNutrition` - Meal data
- `FavoriteMeal` - Saved favorites
- `DailySummary`, `WeeklyStats` - Statistics
- `AIAnalysisResult` - Gemini response format
- `Recommendation` - AI hints/coaching
**Constants:**
- `DEFAULT_DAILY_VALUES` - Reference daily values
- `MEAL_TYPE_LABELS` - Localized meal names
- `NUTRIENT_INFO` - Labels, units, colors
- `CREDIT_COSTS` - Credit pricing
**Utils:**
- `calculateProgress()` - Progress towards goals
- `sumNutrition()` - Sum multiple meals
- `formatNutrient()` - Display formatting
- `detectDeficiencies()` - Find nutrient gaps
- `suggestMealType()` - Based on time of day
## Quick Start
### 1. Create Database
```bash
# PostgreSQL must be running
docker compose -f docker-compose.dev.yml up -d postgres
# Create database
PGPASSWORD=devpassword psql -h localhost -U mana -d postgres -c "CREATE DATABASE nutriphi;"
# Push schema
pnpm nutriphi:db:push
```
### 2. Set Gemini API Key
Add to `.env.development`:
```env
GEMINI_API_KEY=your-gemini-api-key
```
### 3. Start Apps
```bash
# Backend + Web together
pnpm dev:nutriphi:app
# Or individually:
pnpm dev:nutriphi:server # Terminal 1
pnpm dev:nutriphi:web # Terminal 2
pnpm dev:nutriphi:landing # Terminal 3
```
### 4. Open URLs
- Web App: http://localhost:5180
- Landing: http://localhost:4323
- API Health: http://localhost:3023/api/v1/health
## Testing API
```bash
# Health Check
curl http://localhost:3023/api/v1/health
# Login (get token)
TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password"}' | jq -r '.accessToken')
# Analyze text
curl -X POST http://localhost:3023/api/v1/analysis/text \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "Spaghetti Bolognese mit Parmesan"}'
# Get daily summary
curl http://localhost:3023/api/v1/stats/daily \
-H "Authorization: Bearer $TOKEN"
```
## Credit System
| Action | Credits |
|--------|---------|
| Photo Analysis | 5 |
| Text Analysis | 2 |
| AI Coaching | 10 |
## Privacy Features
- Photos are NEVER stored on servers
- Photos are sent directly to Gemini, analyzed, then discarded
- Only nutrition results are saved
- Full data export available (GDPR)
- One-click account deletion
## Color Theme
| Color | Value | Usage |
|-------|-------|-------|
| Primary | #22C55E | Main actions, progress |
| Secondary | #F97316 | Accent, warnings |
| Accent | #14B8A6 | Highlights |
| Calories | #F59E0B | Calorie displays |
| Protein | #EF4444 | Protein displays |
| Carbs | #3B82F6 | Carb displays |
| Fat | #8B5CF6 | Fat displays |
The previous standalone "NutriPhi Project Guide" was deleted in the
audit cleanup of 2026-04-09 — it had been inaccurate since the
consolidation. Pre-consolidation reference is in git history.

View file

@ -1,194 +1,20 @@
# Picture App - CLAUDE.md
# Picture — consolidated into the unified Mana app
AI image generation app using Replicate API with freemium credit system.
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/picture/apps/backend/` and `apps/picture/apps/web/`
directories have been removed. Active code now lives in:
## Project Structure
- **Backend compute routes**: [`apps/api/src/modules/picture/routes.ts`](../api/src/modules/picture/routes.ts) (Replicate / image-gen orchestration, server-side credit deduction)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/picture/`](../mana/apps/web/src/lib/modules/picture/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/picture/`](../mana/apps/web/src/routes/(app)/picture/)
- **Landing page** (still standalone): [`apps/picture/apps/landing/`](apps/landing/)
- **Mobile app**: [`apps/picture/apps/mobile/`](apps/mobile/)
```
apps/picture/
├── apps/
│ ├── backend/ # Hono/Bun server (port 3006)
│ ├── mobile/ # Expo React Native app
│ ├── web/ # SvelteKit web app
│ └── landing/ # Astro marketing page
└── packages/ # Shared code
```
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
## Quick Start
```bash
# From monorepo root
pnpm dev:picture:full # Start backend + web + auto DB setup
# Individual apps
pnpm --filter @picture/server dev # Backend only (port 3006)
pnpm --filter @picture/web dev # Web only
pnpm --filter @picture/mobile dev # Mobile only
```
## Backend Architecture
### Key Services
| Service | Purpose |
|---------|---------|
| `GenerateService` | AI image generation with freemium/credit logic |
| `ReplicateService` | Replicate API integration |
| `StorageService` | MinIO/S3 storage via `@mana/shared-storage` |
| `CreditClientService` | Credit system via `@mana-core/nestjs-integration` |
### Freemium Model
- **Free tier**: 3 free generations per user
- **Paid tier**: 10 credits per generation
- **Enforcement**: Only in staging (`NODE_ENV=staging`)
- **Development**: Fail-open (no credit enforcement)
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `REPLICATE_API_TOKEN` | Replicate API key | Yes |
| `DATABASE_URL` | PostgreSQL connection | Yes |
| `S3_ENDPOINT` | MinIO/S3 endpoint | Yes |
| `MANA_AUTH_URL` | Auth service URL | Yes |
| `MANA_SERVICE_KEY` | Service key for credits | Staging only |
| `APP_ID` | App identifier | Yes |
---
## TODO List
### Testing Required
- [ ] **Test freemium flow with new user**
- Create new user ID and verify 3 free generations work
- Verify `freeGenerationsRemaining` decrements correctly (3 → 2 → 1 → 0)
- Verify 4th generation still works in development (fail-open)
- [ ] **Test staging credit enforcement**
- Set `NODE_ENV=staging` and test credit check
- Verify HTTP 402 returned when credits insufficient
- Test with valid `MANA_SERVICE_KEY`
- [ ] **Test async generation (webhook mode)**
- Test generation without `waitForResult: true`
- Verify webhook receives completion callback
- Verify credits consumed on webhook success
- [ ] **Test error handling**
- Test with invalid model ID
- Test with invalid Replicate API token
- Test storage upload failures
- [ ] **Integration tests**
- Write Jest tests for `GenerateService`
- Mock `CreditClientService` calls
- Test all generation paths (free/paid, sync/async)
### Features to Implement
- [ ] **Add credit balance endpoint**
- GET `/api/v1/credits/balance` - Return user's credit balance
- Use `CreditClientService.getBalance()`
- [ ] **Add generation history endpoint**
- GET `/api/v1/generate/history` - User's generation history
- Include credits used per generation
- [ ] **Improve error messages**
- Add proper error codes for credit failures
- Return helpful messages for insufficient credits
- [ ] **Rate limiting**
- Add rate limits for generation endpoints
- Prevent abuse of free tier
### Web App Tasks
- [ ] **Show free generations remaining**
- Display counter in UI
- Show warning when approaching limit
- [ ] **Credit purchase flow**
- Integrate with mana-core credit purchase
- Show credit balance in header
- [ ] **Generation queue UI**
- Show pending generations
- Poll for status updates
### Mobile App Tasks
- [ ] **Implement generation screen**
- Model selection
- Prompt input with suggestions
- Generation progress indicator
- [ ] **Gallery view**
- Grid view of user's generated images
- Favorites functionality
### DevOps Tasks
- [ ] **Staging deployment**
- Deploy backend to staging server
- Configure `MANA_SERVICE_KEY` in staging
- Test credit system end-to-end
- [ ] **Monitoring**
- Add logging for credit operations
- Track generation success/failure rates
- Monitor Replicate API usage
---
## API Endpoints
### Generate
```bash
# Generate image (sync)
POST /api/v1/generate
{
"prompt": "A beautiful sunset",
"modelId": "uuid",
"waitForResult": true
}
# Check status
GET /api/v1/generate/:id/status
# Cancel generation
DELETE /api/v1/generate/:id
# Webhook (internal)
POST /api/v1/generate/webhook
```
### Models
```bash
GET /api/v1/models # List all models
GET /api/v1/models/:id # Get model details
```
### Images
```bash
GET /api/v1/images # List user's images
GET /api/v1/images/:id # Get image details
DELETE /api/v1/images/:id # Delete image
```
---
## Recent Changes
### 2025-12-10: Credit System Integration
- Added `@mana-core/nestjs-integration` for credit system
- Implemented freemium model (3 free, then 10 credits)
- Credit enforcement only in staging environment
- Updated `GenerateService` with `checkGenerationAccess()`
- Response includes `freeGenerationsRemaining` count
The previous standalone "Picture App" guide describing a per-product
backend with `GenerateService`, `ReplicateService` etc. was deleted in
the audit cleanup of 2026-04-09 — it had been inaccurate since the
consolidation. The freemium credit logic now lives in `apps/api` and
talks to `mana-credits`. Pre-consolidation reference is in git history.

View file

@ -1,167 +1,22 @@
# Planta Project Guide
# Planta — consolidated into the unified Mana app
## Project Structure
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/planta/apps/backend/` and `apps/planta/apps/web/`
directories have been removed. Active code now lives in:
```
apps/planta/
├── apps/
│ ├── backend/ # Hono/Bun compute server (@planta/server)
│ └── web/ # SvelteKit web application (@planta/web)
├── packages/
│ └── shared/ # Shared types, utils (@planta/shared)
└── package.json
```
- **Backend compute routes**: [`apps/api/src/modules/planta/routes.ts`](../api/src/modules/planta/routes.ts) (Gemini Vision plant analysis, S3 upload)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/planta/`](../mana/apps/web/src/lib/modules/planta/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/planta/`](../mana/apps/web/src/routes/(app)/planta/)
## Commands
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
### Root Level (from monorepo root)
The previous standalone "Planta Project Guide" describing a per-product
backend with its own database, schema, and watering scheduler was deleted
in the audit cleanup of 2026-04-09 — it had been inaccurate since the
consolidation. Pre-consolidation reference is in git history.
```bash
pnpm planta:dev # Run all planta apps
pnpm dev:planta:web # Start web app
pnpm dev:planta:server # Start backend server
pnpm dev:planta:app # Start web + backend together
pnpm dev:planta:full # Start auth + backend + web with DB setup
```
### Backend (apps/planta/apps/backend)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
```
### Web App (apps/planta/apps/web)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
## Technology Stack
- **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS
- **Backend**: Hono + Bun, Drizzle ORM, PostgreSQL
- **AI**: Google Gemini Vision for plant analysis
- **Storage**: MinIO (S3-compatible)
- **Auth**: Mana Auth (JWT)
- **Types**: TypeScript 5.x
## Architecture
### Core Flow
1. User uploads plant photo
2. Photo stored in S3/MinIO
3. Gemini Vision analyzes the image
4. Plant profile created with care recommendations
5. Watering schedule tracked
### Backend API Endpoints
| Endpoint | Method | Description |
| ------------------------------- | ------ | ------------------------ |
| `/api/health` | GET | Health check |
| `/api/plants` | GET | Get user's plants |
| `/api/plants` | POST | Create new plant |
| `/api/plants/:id` | GET | Get plant details |
| `/api/plants/:id` | PUT | Update plant |
| `/api/plants/:id` | DELETE | Delete plant |
| `/api/photos/upload` | POST | Upload plant photo |
| `/api/photos/:id` | DELETE | Delete photo |
| `/api/analysis/identify` | POST | Analyze photo with AI |
| `/api/analysis/:photoId` | GET | Get analysis results |
| `/api/watering/upcoming` | GET | Plants needing water |
| `/api/watering/:plantId/water` | POST | Log watering event |
### Database Schema
**plants** - User's plants
- `id` (UUID) - Primary key
- `user_id` (TEXT) - User reference
- `name` (TEXT) - Plant nickname
- `scientific_name` (TEXT) - From AI analysis
- `common_name` (TEXT) - Common name
- `light_requirements` (TEXT) - low/medium/bright/direct
- `watering_frequency_days` (INT) - Days between watering
- `humidity` (TEXT) - low/medium/high
- `care_notes` (TEXT) - Care tips
- `health_status` (TEXT) - healthy/needs_attention/sick
**plant_photos** - Plant photos
- `id` (UUID) - Primary key
- `plant_id` (UUID) - FK to plants
- `storage_path` (TEXT) - S3 path
- `public_url` (TEXT) - Public URL
- `is_primary` (BOOLEAN) - Primary photo flag
- `is_analyzed` (BOOLEAN) - Analysis flag
**plant_analyses** - AI analysis results
- `id` (UUID) - Primary key
- `photo_id` (UUID) - FK to plant_photos
- `identified_species` (TEXT) - Detected species
- `confidence` (INT) - 0-100 confidence
- `health_assessment` (TEXT) - Health status
- `watering_advice` (TEXT) - Watering recommendation
- `general_tips` (JSONB) - Care tips array
**watering_schedules** - Watering tracking
- `id` (UUID) - Primary key
- `plant_id` (UUID) - FK to plants
- `frequency_days` (INT) - Interval
- `last_watered_at` (TIMESTAMP) - Last watering
- `next_watering_at` (TIMESTAMP) - Next watering
### Environment Variables
#### Backend (.env)
```
NODE_ENV=development
PORT=3022
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/planta
MANA_AUTH_URL=http://localhost:3001
GOOGLE_GEMINI_API_KEY=xxx
CORS_ORIGINS=http://localhost:5173,http://localhost:5191
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=planta-storage
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
```
#### Web (.env)
```
PUBLIC_BACKEND_URL=http://localhost:3022
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Shared Package
### @planta/shared
- Types: `Plant`, `PlantPhoto`, `PlantAnalysis`, `WateringSchedule`
- Utils: Date helpers, care level formatters
## Code Style Guidelines
- **TypeScript**: Strict typing with interfaces
- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`)
- **Styling**: Tailwind CSS
- **Formatting**: Prettier with project config
## Important Notes
1. **Authentication**: Uses Mana Auth (JWT in Authorization header)
2. **Database**: PostgreSQL with Drizzle ORM
3. **Port**: Backend runs on port 3022 by default
4. **Storage**: Photos stored in MinIO (S3-compatible)
5. **AI**: Google Gemini Vision for plant identification
> **Note:** The orphaned `apps/planta/packages/shared/` package and the
> related sub-script entries in `apps/planta/package.json` referencing
> non-existent `@planta/server` / `@planta/web` filters are tracked as
> dead code in `docs/REFACTORING_AUDIT_2026_04.md` items #18/#29.

View file

@ -3,14 +3,6 @@
"version": "0.1.0",
"private": true,
"description": "Planta - Plant Documentation & Care App",
"scripts": {
"dev": "turbo run dev",
"dev:server": "pnpm --filter @planta/server dev",
"dev:web": "pnpm --filter @planta/web dev",
"db:push": "pnpm --filter @planta/server db:push",
"db:studio": "pnpm --filter @planta/server db:studio",
"db:seed": "pnpm --filter @planta/server db:seed"
},
"devDependencies": {
"typescript": "^5.9.3"
},

View file

@ -1,213 +1,18 @@
# Presi Project Guide
# Presi — consolidated into the unified Mana app
## Project Structure
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/presi/apps/backend/` (NestJS) and `apps/presi/apps/web/`
directories have been removed. Active code now lives in:
```
apps/presi/
├── apps/
│ ├── backend/ # NestJS API server (@presi/backend)
│ ├── web/ # SvelteKit web application (@presi/web)
│ └── landing/ # Astro marketing landing page (@presi/landing)
├── packages/
│ └── shared/ # Shared types and utils (@presi/shared)
└── package.json
```
- **Backend compute routes**: [`apps/api/src/modules/presi/routes.ts`](../api/src/modules/presi/routes.ts) (public share-link lookups, share management)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/presi/`](../mana/apps/web/src/lib/modules/presi/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/presi/`](../mana/apps/web/src/routes/(app)/presi/)
- **Landing page** (still standalone): [`apps/presi/apps/landing/`](apps/landing/)
## Commands
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
### Root Level (from monorepo root)
```bash
pnpm presi:dev # Run all presi apps
pnpm dev:presi:web # Start web app (port 5178)
pnpm dev:presi:backend # Start backend server
pnpm dev:presi:app # Start web + backend together
pnpm presi:db:push # Push schema to database
pnpm presi:db:studio # Open Drizzle Studio
pnpm presi:db:seed # Seed database with sample data
```
### Web App (apps/presi/apps/web)
```bash
pnpm dev # Start dev server (port 5178)
pnpm build # Build for production
pnpm preview # Preview production build
pnpm check # Run svelte-check
```
### Backend (apps/presi/apps/backend)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
pnpm db:seed # Seed database
```
## Technology Stack
- **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS
- **Backend**: NestJS 10, Drizzle ORM, PostgreSQL
- **Types**: TypeScript 5.x
## Architecture
### Core Features
- Create and manage presentation decks
- Add and edit slides with various content types
- Apply themes to presentations
- Share decks via share codes
- Present slides in full-screen mode
### Backend API Endpoints
| Endpoint | Method | Auth | Description |
| --------------------------- | ------ | ---- | ------------------------ |
| `/api/health` | GET | No | Health check |
| `/api/decks` | GET | Yes | Get user's decks |
| `/api/decks` | POST | Yes | Create new deck |
| `/api/decks/:id` | GET | Yes | Get deck details |
| `/api/decks/:id` | PUT | Yes | Update deck |
| `/api/decks/:id` | DELETE | Yes | Delete deck |
| `/api/decks/:id/slides` | GET | Yes | Get slides for deck |
| `/api/decks/:id/slides` | POST | Yes | Add slide to deck |
| `/api/slides/:id` | PUT | Yes | Update slide |
| `/api/slides/:id` | DELETE | Yes | Delete slide |
| `/api/slides/reorder` | PUT | Yes | Reorder slides |
| `/api/share/:code` | GET | No | Get shared deck (public) |
| `/api/share/deck/:id` | POST | Yes | Create share link |
| `/api/share/deck/:id/links` | GET | Yes | Get share links for deck |
| `/api/share/:shareId` | DELETE | Yes | Delete share link |
### Data Models
**Deck** - Presentation deck
- `id` (string) - Unique identifier
- `userId` (string) - Owner user ID
- `title` (string) - Deck title
- `description` (string?) - Optional description
- `themeId` (string?) - Theme reference
- `isPublic` (boolean) - Visibility flag
- `createdAt` / `updatedAt` (timestamps)
**Slide** - Individual slide in a deck
- `id` (string) - Unique identifier
- `deckId` (string) - Parent deck reference
- `order` (number) - Position in deck
- `content` (SlideContent) - Slide content
- `createdAt` (timestamp)
**SlideContent** - Content structure
- `type`: 'title' | 'content' | 'image' | 'split'
- `title`, `subtitle`, `body`, `imageUrl`, `bulletPoints`
**Theme** - Visual theme
- `id`, `name`, `colors`, `fonts`, `isDefault`
**SharedDeck** - Share link for deck
- `id` (string) - Unique identifier
- `deckId` (string) - Reference to deck
- `shareCode` (string) - Unique share code (12 chars)
- `expiresAt` (timestamp?) - Optional expiration
- `createdAt` (timestamp)
### Environment Variables
#### Backend (.env)
```
NODE_ENV=development
PORT=3008
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/presi
MANA_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5173,http://localhost:8081
```
#### Web (.env)
```
PUBLIC_BACKEND_URL=http://localhost:3008
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Shared Package
### @presi/shared
Located at `packages/shared/`
**Types:**
- `Deck`, `Slide`, `SlideContent`
- `Theme`, `ThemeColors`, `ThemeFonts`
- `SharedDeck` (for sharing feature)
**DTOs:**
- `CreateDeckDto`, `UpdateDeckDto`
- `CreateSlideDto`, `UpdateSlideDto`
- `ReorderSlidesDto`
## Code Style Guidelines
- **TypeScript**: Strict typing with interfaces
- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`)
- **Backend**: NestJS modules with controllers and services
- **Styling**: Tailwind CSS
- **Formatting**: Prettier with project config
## Web App Features
The SvelteKit web app provides the main user interface:
- **Authentication**: Login/Register/Forgot Password with Mana Auth
- **Deck Management**: Create, edit, delete presentation decks
- **Slide Editor**: Create slides with title, body, bullet points, images
- **Presentation Mode**: Fullscreen presentation with keyboard navigation
- Arrow keys / A/D for navigation
- F for fullscreen toggle
- ESC to exit
- Timer with start/pause
- Speaker notes toggle
- **Sharing**: Create share links for decks, public view without auth
- **Profile**: View user info and deck statistics
- **Settings**: Theme switching (light/dark/system), account info
### Web App Structure
```
src/
├── lib/
│ ├── api/client.ts # API client with auth
│ └── stores/
│ ├── auth.svelte.ts # Auth state (Svelte 5 runes)
│ └── decks.svelte.ts # Decks/slides state
├── routes/
│ ├── +layout.svelte # App layout with header
│ ├── +page.svelte # Deck list (home)
│ ├── login/ # Login page
│ ├── register/ # Register page
│ ├── forgot-password/ # Password reset page
│ ├── deck/[id]/ # Deck editor with slides
│ ├── present/[id]/ # Presentation mode
│ ├── shared/[code]/ # Public shared deck view
│ ├── profile/ # User profile page
│ └── settings/ # Settings page
└── app.css # Global styles
```
## Important Notes
1. **Authentication**: Uses Mana Auth (JWT in Authorization header)
2. **Database**: PostgreSQL with Drizzle ORM
3. **Ports**: Backend=3008, Web=5178
4. **Landing**: Deployed on Cloudflare Pages
The previous "Presi Project Guide" referenced a NestJS backend that was
fully replaced by the consolidated Hono routes in apps/api during the
NestJS → Hono sweep. Deleted in the audit cleanup of 2026-04-09.
Pre-consolidation reference is in git history.

View file

@ -2,8 +2,5 @@
"name": "questions",
"version": "0.1.0",
"private": true,
"description": "Questions app - Collect questions and research answers",
"scripts": {
"dev": "turbo run dev"
}
"description": "Questions app - Collect questions and research answers"
}

View file

@ -1,294 +1,25 @@
# Storage Project Guide
# Storage — consolidated into the unified Mana app
## Project Structure
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/storage/apps/backend/` and `apps/storage/apps/web/`
directories have been removed. Active code now lives in:
```
apps/storage/
├── apps/
│ ├── backend/ # Hono/Bun compute server (@storage/server) - Port 3016
│ ├── landing/ # Astro marketing landing page (@storage/landing)
│ └── web/ # SvelteKit web application (@storage/web) - Port 5185
├── packages/
│ └── shared/ # Shared types, utils, configs (@storage/shared)
└── package.json
```
- **Backend compute routes**: [`apps/api/src/modules/storage/routes.ts`](../api/src/modules/storage/routes.ts) (S3/MinIO upload, presigned URLs, download)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/storage/`](../mana/apps/web/src/lib/modules/storage/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/storage/`](../mana/apps/web/src/routes/(app)/storage/)
## Commands
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
### Root Level (from monorepo root)
The previous "Storage Project Guide" describing a per-product NestJS-style
backend with `FilesController`, `FoldersController`, share/version logic,
etc. was deleted in the audit cleanup of 2026-04-09 — it had been
inaccurate since the consolidation. The current consolidated implementation
is much smaller (uses `mana-sync` for metadata CRUD). The audio-player
visualizer logic referenced in the old guide lives directly in the
storage module under `apps/mana/apps/web/src/lib/modules/storage/`.
Pre-consolidation reference is in git history.
```bash
pnpm storage:dev # Run all storage apps
pnpm dev:storage:web # Start web app
pnpm dev:storage:landing # Start landing page
pnpm dev:storage:backend # Start backend server
pnpm dev:storage:app # Start web + backend together
pnpm storage:db:push # Push schema to database
pnpm storage:db:studio # Open Drizzle Studio
pnpm storage:db:seed # Seed database
```
### Backend (apps/storage/apps/backend)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
```
### Web App (apps/storage/apps/web)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
### Landing Page (apps/storage/apps/landing)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
```
## Technology Stack
- **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS
- **Landing**: Astro 5.x, Tailwind CSS
- **Backend**: NestJS 11, Drizzle ORM, PostgreSQL
- **Storage**: S3-compatible (MinIO)
- **Types**: TypeScript 5.x
## Architecture
### Backend API Endpoints
#### Files
| Endpoint | Method | Description |
| --------------------------------- | ------ | -------------------------- |
| `/api/v1/health` | GET | Health check |
| `/api/v1/files` | GET | List files (with folderId) |
| `/api/v1/files/:id` | GET | Get file details |
| `/api/v1/files/upload` | POST | Upload file (multipart) |
| `/api/v1/files/:id/download` | GET | Download file |
| `/api/v1/files/:id` | PATCH | Update file (rename) |
| `/api/v1/files/:id/move` | PATCH | Move file to folder |
| `/api/v1/files/:id` | DELETE | Soft delete file |
| `/api/v1/files/:id/favorite` | POST | Toggle favorite |
| `/api/v1/files/:id/versions` | GET | List file versions |
| `/api/v1/files/:id/versions` | POST | Upload new version |
| `/api/v1/files/:id/tags` | POST | Update file tags |
#### Folders
| Endpoint | Method | Description |
| --------------------------------- | ------ | -------------------------- |
| `/api/v1/folders` | GET | List root folders |
| `/api/v1/folders/:id` | GET | Get folder with contents |
| `/api/v1/folders/:id/tree` | GET | Get folder tree (sidebar) |
| `/api/v1/folders` | POST | Create folder |
| `/api/v1/folders/:id` | PATCH | Update folder |
| `/api/v1/folders/:id/move` | PATCH | Move folder |
| `/api/v1/folders/:id` | DELETE | Soft delete folder |
| `/api/v1/folders/:id/favorite` | POST | Toggle favorite |
#### Shares
| Endpoint | Method | Description |
| --------------------------------- | ------ | -------------------------- |
| `/api/v1/shares` | GET | List user's shares |
| `/api/v1/shares` | POST | Create share link |
| `/api/v1/shares/:id` | PATCH | Update share settings |
| `/api/v1/shares/:id` | DELETE | Revoke share |
| `/api/v1/public/shares/:token` | GET | Access shared item (public)|
| `/api/v1/public/shares/:token/download` | GET | Download shared file |
#### Tags
| Endpoint | Method | Description |
| --------------------------------- | ------ | -------------------------- |
| `/api/v1/tags` | GET | List user's tags |
| `/api/v1/tags` | POST | Create tag |
| `/api/v1/tags/:id` | PATCH | Update tag |
| `/api/v1/tags/:id` | DELETE | Delete tag |
#### Trash
| Endpoint | Method | Description |
| --------------------------------- | ------ | -------------------------- |
| `/api/v1/trash` | GET | List trash items |
| `/api/v1/trash/:id/restore` | POST | Restore item |
| `/api/v1/trash/:id` | DELETE | Permanently delete |
| `/api/v1/trash` | DELETE | Empty trash |
#### Search & Favorites
| Endpoint | Method | Description |
| --------------------------------- | ------ | -------------------------- |
| `/api/v1/search?q=...` | GET | Search files & folders |
| `/api/v1/favorites` | GET | List favorites |
### Database Schema
**files** - File metadata
- `id` (UUID) - Primary key
- `user_id` (VARCHAR) - User reference
- `name` (VARCHAR) - Display name
- `original_name` (VARCHAR) - Original filename
- `mime_type` (VARCHAR) - MIME type
- `size` (BIGINT) - File size in bytes
- `storage_path` (VARCHAR) - Full S3 path
- `storage_key` (VARCHAR) - S3 object key (unique)
- `parent_folder_id` (UUID) - Parent folder reference
- `current_version` (INTEGER) - Current version number
- `is_favorite` (BOOLEAN) - Favorite flag
- `is_deleted` (BOOLEAN) - Soft delete flag
- `deleted_at` (TIMESTAMP) - Deletion timestamp
- `created_at`, `updated_at` (TIMESTAMP)
**folders** - Folder hierarchy
- `id` (UUID) - Primary key
- `user_id` (VARCHAR) - User reference
- `name` (VARCHAR) - Folder name
- `description` (TEXT) - Optional description
- `parent_folder_id` (UUID) - Self-reference for hierarchy
- `path` (TEXT) - Materialized path (e.g., /root/subfolder)
- `depth` (INTEGER) - Depth in hierarchy
- `is_favorite` (BOOLEAN) - Favorite flag
- `is_deleted` (BOOLEAN) - Soft delete flag
- `deleted_at` (TIMESTAMP) - Deletion timestamp
- `created_at`, `updated_at` (TIMESTAMP)
**file_versions** - Version history
- `id` (UUID) - Primary key
- `file_id` (UUID) - File reference
- `version_number` (INTEGER) - Version number
- `storage_path` (VARCHAR) - S3 path for this version
- `storage_key` (VARCHAR) - S3 key for this version
- `size` (BIGINT) - Version size
- `comment` (TEXT) - Version comment
- `created_by` (VARCHAR) - User who created version
- `created_at` (TIMESTAMP)
**shares** - Sharing links
- `id` (UUID) - Primary key
- `user_id` (VARCHAR) - Owner reference
- `file_id` (UUID) - Shared file (nullable)
- `folder_id` (UUID) - Shared folder (nullable)
- `share_type` (VARCHAR) - 'file' or 'folder'
- `share_token` (VARCHAR) - Unique public token
- `access_level` (VARCHAR) - 'view', 'edit', 'download'
- `password` (VARCHAR) - Optional password hash
- `max_downloads` (INTEGER) - Download limit
- `download_count` (INTEGER) - Current downloads
- `expires_at` (TIMESTAMP) - Expiration date
- `created_at` (TIMESTAMP)
**tags** - User tags
- `id` (UUID) - Primary key
- `user_id` (VARCHAR) - User reference
- `name` (VARCHAR) - Tag name
- `color` (VARCHAR) - Tag color
- `created_at` (TIMESTAMP)
**file_tags** - Many-to-many relation
- `file_id` (UUID) - File reference
- `tag_id` (UUID) - Tag reference
### Environment Variables
#### Backend (.env)
```
NODE_ENV=development
PORT=3016
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/storage
MANA_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5173,http://localhost:5185,http://localhost:8081
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
STORAGE_S3_PUBLIC_URL=http://localhost:9000/storage-storage
MAX_FILE_SIZE=104857600
MAX_FILES_PER_UPLOAD=10
```
#### Web (.env)
```
PUBLIC_BACKEND_URL=http://localhost:3016
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Shared Packages
### @storage/shared
- Types: `File`, `Folder`, `FileVersion`, `Share`, `Tag`
- Utils: File type detection, size formatting, path utilities
## File Preview System
The FilePreviewModal supports rich inline previews for various file types:
| File Type | MIME / Extension | Preview |
|-----------|-----------------|---------|
| **Images** | `image/*` | Native `<img>` via download URL |
| **Audio** | `audio/*` | Play button → global MiniPlayer/FullPlayer with frequency visualizer, queue from folder |
| **Video** | `video/*` | Native `<video>` player with controls via presigned S3 URL |
| **PDF** | `application/pdf` | Embedded browser PDF viewer via `<iframe>` |
| **Text/Code** | `text/*`, `.json`, `.js`, `.ts`, `.xml`, `.yaml` | Fetched content in monospace code view (max 50KB) |
| **Markdown** | `.md` | Rendered HTML with headings, lists, code blocks, links |
| **Other** | everything else | File type icon only |
### Audio Player Architecture
```
audioPlayerStore (Svelte 5 runes)
HTMLAudioElement → Presigned S3 URL
Web Audio API (AnalyserNode)
FrequencyBars (Canvas visualizer)
```
**Key files:**
| File | Purpose |
|------|---------|
| `src/lib/stores/audio-player.svelte.ts` | Player state, queue, play/pause/seek/volume |
| `src/lib/audio/analyzer.ts` | Web Audio API singleton for frequency data |
| `src/lib/components/audio/FrequencyBars.svelte` | Canvas frequency bar visualizer |
| `src/lib/components/audio/MiniPlayer.svelte` | Fixed bottom player bar with visualizer |
| `src/lib/components/audio/FullPlayer.svelte` | Fullscreen player with mirrored visualizer background |
**Features:** Queue from audio files in folder, Media Session API (OS controls), presigned S3 URLs, keyboard shortcuts (Space, Escape, Arrow keys in FullPlayer).
## Code Style Guidelines
- **TypeScript**: Strict typing with interfaces
- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`)
- **Styling**: Tailwind CSS
- **Formatting**: Prettier with project config
## Important Notes
1. **Authentication**: Uses Mana Auth (JWT in Authorization header)
2. **Database**: PostgreSQL with Drizzle ORM
3. **Port**: Backend runs on port 3016, Web on port 5185 by default
4. **Storage**: Uses MinIO/S3 for file storage via @mana/shared-storage
5. **Bucket**: `storage-storage` bucket for all files
6. **Soft Delete**: Files/folders are soft-deleted first (trash), then permanently deleted
7. **Versioning**: Files support version history, each version stored separately in S3
8. **Sharing**: Public links with optional password, download limits, and expiration
> **Note:** The orphaned `apps/storage/packages/shared/` package and the
> `dev: turbo run dev` recursive script in `apps/storage/package.json`
> were addressed in audit items #2 and #18/#29.

View file

@ -2,8 +2,5 @@
"name": "storage",
"version": "0.2.0",
"private": true,
"description": "Cloud storage application (like Google Drive/Dropbox)",
"scripts": {
"dev": "turbo run dev"
}
"description": "Cloud storage application (like Google Drive/Dropbox)"
}

View file

@ -1,330 +1,20 @@
# Todo Project Guide
## Overview
**Todo** is a full-featured task management application for the Mana ecosystem. It supports projects, tasks with subtasks, labels, recurring tasks, reminders, and calendar integration.
| App | Port | URL |
|-----|------|-----|
| Backend | 3018 | http://localhost:3018 |
| Web App | 5188 | http://localhost:5188 |
| Landing Page | 4323 | http://localhost:4323 |
## Project Structure
```
apps/todo/
├── apps/
│ ├── backend/ # Hono/Bun compute server (@todo/server)
│ ├── web/ # SvelteKit web application (@todo/web)
│ └── landing/ # Astro marketing landing page (@todo/landing)
├── packages/
│ └── shared/ # Shared types, utils, constants (@todo/shared)
├── package.json
└── CLAUDE.md
```
## Commands
### Root Level (from monorepo root)
```bash
# All apps
pnpm todo:dev # Run all todo apps
# Individual apps
pnpm dev:todo:backend # Start backend server (port 3018)
pnpm dev:todo:web # Start web app (port 5188)
pnpm dev:todo:landing # Start landing page (port 4323)
pnpm dev:todo:app # Start web + backend together
# Database
pnpm todo:db:push # Push schema to database
pnpm todo:db:studio # Open Drizzle Studio
pnpm todo:db:seed # Seed initial data
```
### Backend (apps/todo/apps/backend)
```bash
pnpm dev # Start with hot reload
pnpm build # Build for production
pnpm start:prod # Start production server
pnpm db:push # Push schema to database
pnpm db:studio # Open Drizzle Studio
pnpm db:seed # Seed initial data
```
### Web App (apps/todo/apps/web)
```bash
pnpm dev # Start dev server
pnpm build # Build for production
pnpm preview # Preview production build
```
### Landing Page (apps/todo/apps/landing)
```bash
pnpm dev # Start dev server (port 4323)
pnpm build # Build for production
pnpm preview # Preview build
```
## Technology Stack
| Layer | Technology |
|-------|------------|
| **Backend** | Hono + Bun, Drizzle ORM, PostgreSQL |
| **Web** | SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS 4 |
| **Landing** | Astro 5.x, Tailwind CSS |
| **Auth** | Mana Auth (JWT) |
| **i18n** | svelte-i18n (DE, EN) |
| **Dates** | date-fns |
## Core Features
1. **Projects** - Organize tasks into color-coded projects
2. **Tasks** - Full CRUD with priority, due dates, and status
3. **Subtasks** - Checklist items within tasks
4. **Labels** - Tag tasks with colored labels
5. **Recurring Tasks** - Daily, weekly, monthly (RFC 5545 RRULE)
6. **Reminders** - Push and email notifications
7. **Calendar Integration** - Sync tasks with Calendar app
8. **Quick Add** - Natural language task creation
## Views
| View | Route | Description |
|------|-------|-------------|
| **Inbox** | `/` (default) | Tasks without a project |
| **Today** | `/today` | Due today + overdue |
| **Upcoming** | `/upcoming` | Next 7 days, grouped by date |
| **Project** | `/project/[id]` | Tasks in specific project |
| **Label** | `/label/[id]` | Tasks with specific label |
| **Completed** | `/completed` | Completed tasks archive |
| **Search** | `/search` | Full-text search |
## API Endpoints
### Projects
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/projects` | GET | List user's projects |
| `/api/v1/projects` | POST | Create project |
| `/api/v1/projects/:id` | GET | Get project details |
| `/api/v1/projects/:id` | PUT | Update project |
| `/api/v1/projects/:id` | DELETE | Delete project |
| `/api/v1/projects/:id/archive` | POST | Archive project |
| `/api/v1/projects/reorder` | PUT | Reorder projects |
### Tasks
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/tasks` | GET | Query tasks (filters) |
| `/api/v1/tasks` | POST | Create task |
| `/api/v1/tasks/:id` | GET | Get task details |
| `/api/v1/tasks/:id` | PUT | Update task |
| `/api/v1/tasks/:id` | DELETE | Delete task |
| `/api/v1/tasks/:id/complete` | POST | Mark complete |
| `/api/v1/tasks/:id/uncomplete` | POST | Mark incomplete |
| `/api/v1/tasks/:id/move` | POST | Move to project |
| `/api/v1/tasks/:id/labels` | PUT | Update labels |
| `/api/v1/tasks/inbox` | GET | Inbox tasks |
| `/api/v1/tasks/today` | GET | Today's tasks |
| `/api/v1/tasks/upcoming` | GET | Upcoming tasks |
| `/api/v1/tasks/reorder` | PUT | Reorder tasks |
### Labels
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/labels` | GET | List labels |
| `/api/v1/labels` | POST | Create label |
| `/api/v1/labels/:id` | PUT | Update label |
| `/api/v1/labels/:id` | DELETE | Delete label |
### Reminders
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/tasks/:taskId/reminders` | GET | List reminders |
| `/api/v1/tasks/:taskId/reminders` | POST | Add reminder |
| `/api/v1/reminders/:id` | DELETE | Delete reminder |
## Database Schema
> **Note**: `user_id` columns use TEXT type (not UUID) because Mana Auth generates non-UUID user IDs.
### projects
- `id` (UUID) - Primary key
- `user_id` (TEXT) - Owner (Better Auth format)
- `name` (VARCHAR) - Project name
- `color` (VARCHAR) - Hex color
- `icon` (VARCHAR) - Icon name
- `order` (INTEGER) - Sort order
- `is_archived` (BOOLEAN) - Archive flag
- `is_default` (BOOLEAN) - Inbox project
### tasks
- `id` (UUID) - Primary key
- `project_id` (UUID) - FK to projects (nullable = Inbox)
- `user_id` (TEXT) - Owner (Better Auth format)
- `title` (VARCHAR) - Task title
- `description` (TEXT) - Description
- `due_date` (TIMESTAMP) - Due date
- `priority` (VARCHAR) - low/medium/high/urgent
- `is_completed` (BOOLEAN) - Completion flag
- `order` (INTEGER) - Sort order
- `recurrence_rule` (VARCHAR) - RFC 5545 RRULE
- `subtasks` (JSONB) - Subtask array
- `metadata` (JSONB) - Extra data
### labels
- `id` (UUID) - Primary key
- `user_id` (TEXT) - Owner (Better Auth format)
- `name` (VARCHAR) - Label name
- `color` (VARCHAR) - Hex color
### task_labels
- `task_id` (UUID) - FK to tasks
- `label_id` (UUID) - FK to labels
### reminders
- `id` (UUID) - Primary key
- `task_id` (UUID) - FK to tasks
- `user_id` (TEXT) - Owner (Better Auth format)
- `minutes_before` (INTEGER) - Offset
- `type` (VARCHAR) - push/email/both
- `status` (VARCHAR) - pending/sent/failed
## Environment Variables
### Backend (.env)
```env
NODE_ENV=development
PORT=3018
DATABASE_URL=postgresql://mana:devpassword@localhost:5432/todo
MANA_AUTH_URL=http://localhost:3001
CORS_ORIGINS=http://localhost:5173,http://localhost:5186,http://localhost:8081
```
### Web (.env)
```env
PUBLIC_BACKEND_URL=http://localhost:3018
PUBLIC_MANA_AUTH_URL=http://localhost:3001
```
## Quick Add Syntax
Natural language task creation:
```
"Meeting morgen um 14 Uhr !hoch @Arbeit #wichtig"
```
Recognized patterns:
- **Date**: heute, morgen, nächsten Montag, 15.12.
- **Time**: um 14 Uhr, 14:00
- **Priority**: !hoch, !niedrig, !dringend, !!!
- **Project**: @Projektname
- **Labels**: #label1 #label2
- **Recurrence**: jeden Tag, wöchentlich, monatlich
- **Duration**: 30min, 2h, 1.5 Stunden (maps to `estimatedDuration`)
### Multi-Task Input
Split multiple tasks with keywords (`danach`, `dann`, `und dann`, `anschließend`, `außerdem`) or semicolons:
```
"Morgen um 10 Zahnarzt 1h, danach Einkaufen"
→ Task 1: Zahnarzt (morgen 10:00, 1h)
→ Task 2: Einkaufen (morgen 11:00, auto-offset)
"Meeting 14 Uhr 1h @Arbeit; Report schreiben; Mails"
→ 3 tasks, all inherit date + project from first task
```
Context inheritance: subsequent tasks inherit date, time, and project from the first task. If the first task has a duration, the next task's time is offset accordingly.
### Smart Duration (Auto-Estimation)
Duration is **automatically applied** to new tasks when no explicit duration is typed. The system uses history-based estimation (weighted by project, labels, title similarity, priority) with a configurable default as fallback. Controllable via Settings > Task-Verhalten:
- **Smarte Dauer** toggle (`smartDurationEnabled`, default: on)
- **Standard-Dauer** fallback (`defaultTaskDuration`, default: 30min)
Priority: explicit duration in text > history estimate > default fallback > none (if disabled).
## Code Style Guidelines
- **TypeScript**: Strict typing with interfaces
- **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`)
- **Styling**: Tailwind CSS with CSS variables
- **Formatting**: Prettier with project config
- **i18n**: All UI text in locale files
## Production Readiness
**Status: Production-Ready (2026-03-24)**
### Checklist
| Category | Status | Details |
|----------|--------|---------|
| **Error Handling** | ✅ | Global `+error.svelte` with i18n (5 languages), error tracking via GlitchTip |
| **Offline Support** | ✅ | Offline page with shared `OfflinePage` component |
| **PWA** | ✅ | Service worker, manifest, icons, shortcuts (New task, Kanban, Settings) |
| **Security Headers** | ✅ | CSP, X-Frame-Options via `setSecurityHeaders()` |
| **Loading States** | ✅ | Skeleton loaders: TaskList, TaskItem, KanbanBoard, KanbanColumn, Statistics |
| **i18n** | ✅ | 5 languages (DE/EN/FR/ES/IT) via svelte-i18n |
| **Meta/SEO** | ✅ | OG tags, meta description in root layout |
| **Accessibility** | ✅ | Focus trapping in all modals, ARIA roles, keyboard shortcuts |
| **Rate Limiting** | ✅ | ThrottlerGuard global (100 req/min) |
| **API Validation** | ✅ | DTOs with class-validator, RRULE DoS protection (max 5000 occurrences) |
| **Auth** | ✅ | JWT via mana-auth, client-side redirect in `onMount` |
| **Toast System** | ✅ | Toast notifications via shared `toastStore` |
| **Docker** | ✅ | Multi-stage build (web + backend), health checks, Traefik labels |
| **Tests** | ✅ | Unit tests (7 backend, 3 web), E2E tests (3 suites: auth, projects, tasks) |
| **Error Tracking** | ✅ | GlitchTip integration (client + server) |
| **Metrics** | ✅ | Prometheus via MetricsModule |
| **Context Menu** | ✅ | Shared ContextMenu on TaskList (priority, project, complete, delete) |
| **Auto-Save** | ✅ | 500ms debounce, no save/cancel buttons needed |
| **Drag & Drop** | ✅ | Task reordering in list + kanban views |
### Test Suites
```bash
# Unit tests
pnpm --filter @todo/server test
pnpm --filter @todo/web test
# E2E tests
pnpm --filter @todo/web test:e2e
```
| Type | Suite | Coverage |
|------|-------|----------|
| Unit (Backend) | `task.service.spec.ts` | Task CRUD, recurrence |
| Unit (Backend) | `project.service.spec.ts` | Project management |
| Unit (Backend) | `kanban.service.spec.ts` | Kanban operations |
| Unit (Backend) | `reminder.service.spec.ts` | Reminders |
| Unit (Backend) | `label.service.spec.ts` | Labels |
| Unit (Web) | `task-parser.test.ts` | Natural language parsing |
| Unit (Web) | `task-filters.test.ts` | Filter logic |
| Unit (Web) | `view.test.ts` | View store state |
| E2E | `auth.spec.ts` | Login, redirect |
| E2E | `projects.spec.ts` | Project CRUD |
| E2E | `tasks.spec.ts` | Task CRUD |
## Important Notes
1. **Authentication**: Uses Mana Auth (JWT in Authorization header)
2. **Database**: PostgreSQL with Drizzle ORM (port 5432)
3. **Ports**: Backend=3018, Web=5188, Landing=4323
4. **Recurrence**: Uses RFC 5545 RRULE format
5. **Calendar**: Tasks can sync bidirectionally with Calendar app
# Todo — consolidated into the unified Mana app
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/todo/apps/backend/` and `apps/todo/apps/web/` directories
have been removed. Active code now lives in:
- **Backend compute routes**: [`apps/api/src/modules/todo/routes.ts`](../api/src/modules/todo/routes.ts)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/todo/`](../mana/apps/web/src/lib/modules/todo/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/todo/`](../mana/apps/web/src/routes/(app)/todo/)
- **Landing page** (still standalone): [`apps/todo/apps/landing/`](apps/landing/)
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
The previous standalone "Todo Project Guide" describing a per-product
backend with its own DB and reminder worker was deleted in the audit
cleanup of 2026-04-09 — it had been inaccurate since the consolidation.
The reminder worker logic referenced there was migrated to apps/api or
the relevant background-job home; check git history if you need the
original reference.

View file

@ -1,76 +1,18 @@
# CLAUDE.md - Traces
# Traces — consolidated into the unified Mana app
GPS tracking app with AI city guides. Location tracking runs locally via AsyncStorage, with optional backend sync.
This product was migrated into the unified Mana monorepo. The legacy
per-product `apps/traces/apps/server/` directory has been removed.
Active code now lives in:
## Project Structure
- **Backend compute routes**: [`apps/api/src/modules/traces/routes.ts`](../api/src/modules/traces/routes.ts) (location sync, AI city-guide pipeline, GPS-to-city detection)
- **Frontend module** (local-first): [`apps/mana/apps/web/src/lib/modules/traces/`](../mana/apps/web/src/lib/modules/traces/)
- **Web route**: [`apps/mana/apps/web/src/routes/(app)/traces/`](../mana/apps/web/src/routes/(app)/traces/)
- **Mobile app**: [`apps/traces/apps/mobile/`](apps/mobile/) (GPS tracking remains a native concern)
```
apps/traces/
├── package.json # Orchestrator (name: traces)
├── apps/
│ ├── backend/ # @traces/server (NestJS, Port 3026)
│ │ └── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── db/ # Drizzle schema + connection
│ │ ├── location/ # GPS sync endpoint
│ │ ├── city/ # City CRUD + visit stats
│ │ ├── place/ # Saved places CRUD
│ │ ├── poi/ # Points of Interest
│ │ └── guide/ # AI city guide pipeline
│ └── mobile/ # @traces/mobile (Expo SDK 54)
│ ├── app/ # Expo Router screens
│ ├── components/ # UI components
│ └── utils/ # Services (location, sync, api)
└── packages/
└── traces-types/ # @traces/types (shared interfaces)
```
For monorepo-wide patterns (auth, sync, encryption, services), see the
[root `CLAUDE.md`](../../CLAUDE.md) and [`apps/mana/CLAUDE.md`](../mana/CLAUDE.md).
## Commands
```bash
# Development
pnpm dev:traces:mobile # Start Expo app
pnpm dev:traces:server # Start NestJS backend
pnpm dev:traces:full # Start auth + backend + mobile
# Database
pnpm traces:db:push # Push Drizzle schema
pnpm traces:db:studio # Open Drizzle Studio
```
## Architecture
- **Mobile**: Offline-first. All GPS data in AsyncStorage. Sync is additive.
- **Backend**: NestJS + Drizzle ORM + PostgreSQL. Auth via ManaModule.
- **AI Guides**: Uses mana-search for POI discovery, mana-llm for narratives.
- **Credits**: 5 base + 2 per POI consumed via CreditClientService.
## API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/locations/sync` | POST | Batch sync from mobile |
| `/api/v1/locations` | GET | Query locations |
| `/api/v1/cities` | GET | User's visited cities |
| `/api/v1/cities/:id` | GET | City detail + stats |
| `/api/v1/places` | GET/POST | List/create places |
| `/api/v1/places/:id` | PUT/DELETE | Update/delete place |
| `/api/v1/pois` | GET | Nearby POIs |
| `/api/v1/pois/:id` | GET | POI detail |
| `/api/v1/guides/generate` | POST | Generate AI guide |
| `/api/v1/guides` | GET | User's guides |
| `/api/v1/guides/:id` | GET/DELETE | Guide detail/delete |
## Environment Variables
Backend: `PORT=3026`, `DATABASE_URL`, `MANA_AUTH_URL`, `MANA_LLM_URL`, `MANA_SEARCH_URL`
Mobile: `EXPO_PUBLIC_TRACES_BACKEND_URL`, `EXPO_PUBLIC_MANA_AUTH_URL`
## Mobile Navigation (5 tabs)
1. **Tracking** - Live GPS tracking + map
2. **Orte** - Saved places, cities, countries
3. **Karte** - Full-screen map view
4. **Städte** - Visited cities with stats
5. **Führungen** - AI-generated city guides
The previous standalone "Traces" guide was deleted in the audit cleanup
of 2026-04-09 — it had been inaccurate since the consolidation. The
mobile-side AsyncStorage GPS pipeline still lives under `apps/traces/apps/mobile/`.
Pre-consolidation backend reference is in git history.

View file

@ -1,8 +1,5 @@
{
"name": "@mana/uload",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "turbo run dev"
}
"private": true
}

View file

@ -275,7 +275,12 @@ services:
MANA_AUTH_KEK: ${MANA_AUTH_KEK}
MANA_NOTIFY_URL: http://mana-notify:3013
MAX_DAILY_SIGNUPS: ${MAX_DAILY_SIGNUPS:-0}
CORS_ORIGINS: https://mana.how,https://calendar.mana.how,https://chat.mana.how,https://clock.mana.how,https://contacts.mana.how,https://context.mana.how,https://docs.mana.how,https://inventar.mana.how,https://cards.mana.how,https://music.mana.how,https://nutriphi.mana.how,https://photos.mana.how,https://picture.mana.how,https://planta.mana.how,https://playground.mana.how,https://presi.mana.how,https://questions.mana.how,https://skilltree.mana.how,https://storage.mana.how,https://times.mana.how,https://todo.mana.how,https://traces.mana.how,https://zitare.mana.how
# Must be a superset of TRUSTED_ORIGINS in
# services/mana-auth/src/auth/better-auth.config.ts.
# Enforced by services/mana-auth/src/auth/sso-config.spec.ts.
# All productivity modules now live under mana.how (path-based) —
# no per-module subdomain entries required here.
CORS_ORIGINS: https://mana.how,https://auth.mana.how,https://arcade.mana.how,https://whopxl.mana.how
ports:
- "3001:3001"
healthcheck:

View file

@ -2,8 +2,5 @@
"name": "arcade",
"version": "1.0.0",
"private": true,
"description": "AI-powered browser games platform",
"scripts": {
"dev": "turbo run dev"
}
"description": "AI-powered browser games platform"
}

View file

@ -4,8 +4,9 @@
* Catches unhandled errors and returns consistent JSON responses.
*/
import type { Context } from 'hono';
import type { Context, ErrorHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { logger } from '@mana/shared-logger';
/**
* Global error handler register with `app.onError(errorHandler)`.
@ -28,7 +29,12 @@ export function errorHandler(err: Error, c: Context) {
);
}
console.error('[error]', err);
logger.error('unhandled', {
path: c.req.path,
method: c.req.method,
message: err.message,
stack: err.stack,
});
return c.json(
{
@ -45,3 +51,53 @@ export function errorHandler(err: Error, c: Context) {
export function notFoundHandler(c: Context) {
return c.json({ error: 'Not found', status: 404 }, 404);
}
/**
* Service-style error handler returns the legacy `{ statusCode, message }`
* envelope used by `services/mana-{credits,user,analytics,subscriptions,
* auth,events}`. Distinct from the `{ error, status }` shape returned by
* `errorHandler` above (used by `apps/api`).
*
* Replaces 5 byte-identical copies of `src/middleware/error-handler.ts`
* across services. Wire-compatible with their existing clients
* including the `details` field that gets populated from
* `HTTPException.cause`.
*
* Usage:
* ```ts
* import { serviceErrorHandler } from '@mana/shared-hono';
* const app = new Hono();
* app.onError(serviceErrorHandler);
* ```
*
* Logs unhandled errors via `@mana/shared-logger` so structured JSON
* lines land in the production log sink instead of `console.error`.
*/
export const serviceErrorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
const cause = err.cause as Record<string, unknown> | undefined;
return c.json(
{
statusCode: err.status,
message: err.message,
...(cause ? { details: cause } : {}),
},
err.status
);
}
logger.error('unhandled', {
path: c.req.path,
method: c.req.method,
message: err.message,
stack: err.stack,
});
return c.json(
{
statusCode: 500,
message: 'Internal server error',
},
500
);
};

View file

@ -37,9 +37,10 @@ export { createDb } from './db';
export type { DbOptions } from './db';
export { healthRoute } from './health';
export { adminRoutes } from './admin';
export { errorHandler, notFoundHandler } from './error';
export { errorHandler, notFoundHandler, serviceErrorHandler } from './error';
export { getBalance, validateCredits, consumeCredits, refundCredits } from './credits';
export type { CreditBalance, CreditValidationResult } from './credits';
export { rateLimitMiddleware } from './rate-limit';
export { requestLogger, initLogger } from './logger';
export { logger } from '@mana/shared-logger';
export type { CurrentUserData, AuthVariables } from './types';

View file

@ -3,15 +3,8 @@
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.js",
"exports": {
".": "./src/index.js",
"./preset": "./src/preset.js",
"./colors": "./src/colors.js",
"./theme.css": "./src/theme-variables.css",
"./themes.css": "./src/themes.css",
"./components.css": "./src/components.css",
"./v4": "./src/tailwind-v4.css"
"./themes.css": "./src/themes.css"
},
"scripts": {
"lint": "eslint ."

View file

@ -1,243 +0,0 @@
/**
* Shared color palette for all Mana apps
*
* Theme structure:
* - Each theme has light and dark variants
* - Semantic color tokens for consistent UI
*/
export const colors = {
// Brand color used across subscription/pricing
mana: '#4287f5',
// App-specific primary colors
brand: {
memoro: '#f8d62b', // Gold
mana: '#6366f1', // Indigo
cards: '#6366f1', // Indigo
storyteller: '#6366f1', // Indigo
},
// Primary color scale (for general use)
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
// Lume Theme - Modern Gold & Dark
lume: {
light: {
primary: '#f8d62b',
primaryButton: '#f8d62b',
primaryButtonText: '#000000',
secondary: '#D4B200',
secondaryButton: '#FFE9A3',
contentBackground: '#ffffff',
contentBackgroundHover: '#f5f5f5',
contentPageBackground: '#ffffff',
menuBackground: '#dddddd',
menuBackgroundHover: '#cccccc',
pageBackground: '#dddddd',
text: '#2c2c2c',
textSecondary: '#666666',
borderLight: '#f2f2f2',
border: '#e6e6e6',
borderStrong: '#cccccc',
error: '#e74c3c',
success: '#27ae60',
warning: '#f39c12',
},
dark: {
primary: '#f8d62b',
primaryButton: '#7C6B16',
primaryButtonText: '#ffffff',
secondary: '#D4B200',
secondaryButton: '#1E1E1E',
contentBackground: '#1E1E1E',
contentBackgroundHover: '#333333',
contentPageBackground: '#121212',
menuBackground: '#101010',
menuBackgroundHover: '#333333',
pageBackground: '#101010',
text: '#ffffff',
textSecondary: '#a0a0a0',
borderLight: '#333333',
border: '#424242',
borderStrong: '#616161',
error: '#e74c3c',
success: '#2ecc71',
warning: '#f1c40f',
},
},
// Nature Theme - Soothing Green
nature: {
light: {
primary: '#4CAF50',
primaryButton: '#A08500',
primaryButtonText: '#ffffff',
secondary: '#81C784',
secondaryButton: '#F1F8E9',
contentBackground: '#F1F8E9',
contentBackgroundHover: '#E8F5E9',
contentPageBackground: '#ffffff',
menuBackground: '#E8F5E9',
menuBackgroundHover: '#C8E6C9',
pageBackground: '#FBFDF8',
text: '#1B5E20',
textSecondary: '#388E3C',
borderLight: '#E8F5E9',
border: '#C8E6C9',
borderStrong: '#A5D6A7',
error: '#E57373',
success: '#66BB6A',
warning: '#FFB74D',
},
dark: {
primary: '#4CAF50',
primaryButton: '#FF9500',
primaryButtonText: '#000000',
secondary: '#81C784',
secondaryButton: '#1E1E1E',
contentBackground: '#1E1E1E',
contentBackgroundHover: '#2E7D32',
contentPageBackground: '#121212',
menuBackground: '#252525',
menuBackgroundHover: '#2E7D32',
pageBackground: '#121212',
text: '#FFFFFF',
textSecondary: '#A5D6A7',
borderLight: '#1B5E20',
border: '#2E7D32',
borderStrong: '#388E3C',
error: '#CF6679',
success: '#81C784',
warning: '#FFD54F',
},
},
// Stone Theme - Elegant Slate
stone: {
light: {
primary: '#607D8B',
primaryButton: '#FF9500',
primaryButtonText: '#000000',
secondary: '#90A4AE',
secondaryButton: '#ECEFF1',
contentBackground: '#ECEFF1',
contentBackgroundHover: '#E0E6EA',
contentPageBackground: '#ffffff',
menuBackground: '#E0E6EA',
menuBackgroundHover: '#CFD8DC',
pageBackground: '#F5F7F9',
text: '#263238',
textSecondary: '#546E7A',
borderLight: '#ECEFF1',
border: '#CFD8DC',
borderStrong: '#B0BEC5',
error: '#EF5350',
success: '#66BB6A',
warning: '#FFA726',
},
dark: {
primary: '#78909C',
primaryButton: '#FF9500',
primaryButtonText: '#000000',
secondary: '#90A4AE',
secondaryButton: '#1E1E1E',
contentBackground: '#1E1E1E',
contentBackgroundHover: '#37474F',
contentPageBackground: '#121212',
menuBackground: '#252525',
menuBackgroundHover: '#37474F',
pageBackground: '#121212',
text: '#FFFFFF',
textSecondary: '#B0BEC5',
borderLight: '#37474F',
border: '#455A64',
borderStrong: '#546E7A',
error: '#CF6679',
success: '#81C784',
warning: '#FFD54F',
},
},
// Ocean Theme - Tranquil Blue
ocean: {
light: {
primary: '#039BE5',
primaryButton: '#FF9500',
primaryButtonText: '#000000',
secondary: '#4FC3F7',
secondaryButton: '#E1F5FE',
contentBackground: '#E1F5FE',
contentBackgroundHover: '#B3E5FC',
contentPageBackground: '#ffffff',
menuBackground: '#E1F5FE',
menuBackgroundHover: '#B3E5FC',
pageBackground: '#F5FCFF',
text: '#01579B',
textSecondary: '#0277BD',
borderLight: '#E1F5FE',
border: '#B3E5FC',
borderStrong: '#81D4FA',
error: '#EF5350',
success: '#66BB6A',
warning: '#FFA726',
},
dark: {
primary: '#039BE5',
primaryButton: '#FF9500',
primaryButtonText: '#000000',
secondary: '#4FC3F7',
secondaryButton: '#1E1E1E',
contentBackground: '#1E1E1E',
contentBackgroundHover: '#0277BD',
contentPageBackground: '#121212',
menuBackground: '#252525',
menuBackgroundHover: '#0277BD',
pageBackground: '#121212',
text: '#FFFFFF',
textSecondary: '#81D4FA',
borderLight: '#01579B',
border: '#0277BD',
borderStrong: '#0288D1',
error: '#CF6679',
success: '#81C784',
warning: '#FFD54F',
},
},
};
// Flat theme colors for direct use in Tailwind configs
export const themeColors = {
mana: colors.mana,
primary: colors.primary,
lume: {
...colors.lume.light,
dark: colors.lume.dark,
},
nature: {
...colors.nature.light,
dark: colors.nature.dark,
},
stone: {
...colors.stone.light,
dark: colors.stone.dark,
},
ocean: {
...colors.ocean.light,
dark: colors.ocean.dark,
},
};
export default colors;

View file

@ -1,236 +0,0 @@
/* Shared Component Styles for Mana Apps
* Import this in your app.css: @import '@mana/shared-tailwind/components.css';
*
* Requires theme-variables.css to be imported first for CSS variable support
*/
body {
margin: 0;
font-family: var(--font-body);
}
html {
margin: 0;
padding: 0;
}
@layer base {
h1 {
@apply text-3xl font-bold;
color: var(--color-text);
}
h2 {
@apply text-2xl font-semibold;
color: var(--color-text);
}
h3 {
@apply text-xl font-semibold;
color: var(--color-text);
}
}
@layer components {
.btn-primary {
@apply rounded-lg px-4 py-2 font-semibold transition-colors;
background-color: var(--color-primary-button);
color: var(--color-primary-button-text);
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
@apply rounded-lg px-4 py-2 font-semibold transition-colors;
background-color: var(--color-secondary-button);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background-color: var(--color-content-bg-hover);
}
.btn-danger {
@apply rounded-lg px-4 py-2 font-semibold transition-colors;
background-color: var(--color-error);
color: #ffffff;
}
.btn-danger:hover {
opacity: 0.9;
}
.input-field {
@apply w-full rounded-lg px-4 py-2 transition-colors;
background-color: var(--color-content-bg);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.input-field:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent);
}
.card {
@apply rounded-lg p-6 shadow-sm;
background-color: var(--color-content-bg);
color: var(--color-text);
}
/* Header & Navigation */
.header-style {
background-color: var(--color-menu-bg);
border-bottom: 1px solid var(--color-border);
}
.logo-text {
@apply text-2xl font-bold;
color: var(--color-primary);
}
.nav-link {
@apply transition-colors;
color: var(--color-text);
}
.nav-link:hover {
color: var(--color-primary);
}
.user-email {
@apply text-sm;
color: var(--color-text);
opacity: 0.7;
}
/* Main Content Area */
.main-content {
background-color: var(--color-page-bg);
}
/* Selected/Active State */
.bg-selected {
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
}
/* Status Badge Colors */
.status-completed {
background-color: rgba(76, 175, 80, 0.15);
color: #4caf50;
}
.status-processing {
background-color: color-mix(in srgb, var(--color-primary) 15%, transparent);
color: var(--color-primary);
}
.status-failed {
background-color: color-mix(in srgb, var(--color-error) 15%, transparent);
color: var(--color-error);
}
.status-default {
background-color: color-mix(in srgb, var(--color-text) 10%, transparent);
color: var(--color-text);
}
/* Info/Alert Boxes */
.info-box {
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--color-primary) 30%, transparent);
}
/* Loading Spinner */
.spinner-border {
border-color: var(--color-primary);
}
/* Focus Ring */
.focus\:ring-primary:focus {
--tw-ring-color: var(--color-primary);
}
.focus\:ring-2:focus {
box-shadow: 0 0 0 2px var(--tw-ring-color, var(--color-primary));
}
}
@layer utilities {
/* Theme Color Utilities - in utilities layer for @apply support */
.bg-content {
background-color: var(--color-content-bg);
}
.bg-content-hover {
background-color: var(--color-content-bg-hover);
}
.hover\:bg-content-hover:hover {
background-color: var(--color-content-bg-hover);
}
.bg-menu {
background-color: var(--color-menu-bg);
}
.bg-menu-hover {
background-color: var(--color-menu-bg-hover);
}
.hover\:bg-menu-hover:hover {
background-color: var(--color-menu-bg-hover);
}
.bg-panel {
background-color: var(--color-panel-bg);
}
.bg-page {
background-color: var(--color-page-bg);
}
.border-theme {
border-color: var(--color-border);
}
.border-theme-light {
border-color: var(--color-border-light);
}
.text-theme {
color: var(--color-text);
}
.text-theme-secondary {
color: var(--color-text);
opacity: 0.6;
}
.text-theme-muted {
color: var(--color-text);
opacity: 0.4;
}
.text-primary {
color: var(--color-primary);
}
.bg-primary {
background-color: var(--color-primary);
}
.bg-primary-button {
background-color: var(--color-primary-button);
}
.text-primary-button-text {
color: var(--color-primary-button-text);
}
.bg-secondary-button {
background-color: var(--color-secondary-button);
}
}

View file

@ -1,15 +0,0 @@
/**
* @mana/shared-tailwind
*
* Shared Tailwind CSS configuration for all Mana apps.
*
* Exports:
* - preset: Tailwind preset with colors, themes, and design tokens
* - colors: Color palette definitions
*
* Also available:
* - @mana/shared-tailwind/themes.css: CSS custom properties for runtime theming
*/
export { default as preset } from './preset.js';
export { colors, default as defaultColors } from './colors.js';

View file

@ -1,181 +0,0 @@
/**
* Shared Tailwind CSS preset for all Mana apps
*
* This preset uses HSL-based CSS variables for theming.
* Colors are defined as HSL values (e.g., "47 95% 58%") and
* wrapped with hsl() in the Tailwind config for flexibility.
*
* Usage in tailwind.config.js:
* ```
* import preset from '@mana/shared-tailwind/preset';
*
* export default {
* presets: [preset],
* content: ['./src/**\/*.{html,js,svelte,ts}'],
* // app-specific overrides...
* }
* ```
*/
/** @type {import('tailwindcss').Config} */
const preset = {
darkMode: 'class',
theme: {
extend: {
colors: {
// Brand color (consistent across all apps)
mana: '#4287f5',
// ===== HSL-Based Semantic Colors =====
// These use CSS variables set by @mana/shared-theme
// Format: hsl(var(--color-name)) where --color-name is "H S% L%"
// Page background
background: 'hsl(var(--color-background))',
// Main text color
foreground: 'hsl(var(--color-foreground))',
// Primary brand color (customizable per app)
primary: {
DEFAULT: 'hsl(var(--color-primary))',
foreground: 'hsl(var(--color-primary-foreground))',
},
// Secondary accent
secondary: {
DEFAULT: 'hsl(var(--color-secondary))',
foreground: 'hsl(var(--color-secondary-foreground))',
},
// Card/content surfaces
surface: {
DEFAULT: 'hsl(var(--color-surface))',
hover: 'hsl(var(--color-surface-hover))',
elevated: 'hsl(var(--color-surface-elevated))',
},
// Muted/disabled elements
muted: {
DEFAULT: 'hsl(var(--color-muted))',
foreground: 'hsl(var(--color-muted-foreground))',
},
// Borders
border: {
DEFAULT: 'hsl(var(--color-border))',
strong: 'hsl(var(--color-border-strong))',
},
// Semantic/feedback colors
error: 'hsl(var(--color-error))',
success: 'hsl(var(--color-success))',
warning: 'hsl(var(--color-warning))',
// Form elements
input: 'hsl(var(--color-input))',
ring: 'hsl(var(--color-ring))',
// ===== Legacy aliases (for backwards compatibility) =====
content: {
DEFAULT: 'hsl(var(--color-surface))',
hover: 'hsl(var(--color-surface-hover))',
page: 'hsl(var(--color-background))',
},
menu: {
DEFAULT: 'hsl(var(--color-muted))',
hover: 'hsl(var(--color-surface-hover))',
},
theme: {
DEFAULT: 'hsl(var(--color-foreground))',
secondary: 'hsl(var(--color-muted-foreground))',
},
'primary-btn': {
DEFAULT: 'hsl(var(--color-primary))',
text: 'hsl(var(--color-primary-foreground))',
},
},
// Border radius tokens (CSS variable support)
borderRadius: {
none: '0',
sm: 'var(--radius-sm, 0.25rem)',
DEFAULT: 'var(--radius, 0.375rem)',
md: 'var(--radius-md, 0.5rem)',
lg: 'var(--radius-lg, 0.75rem)',
xl: 'var(--radius-xl, 1rem)',
'2xl': 'var(--radius-2xl, 1.5rem)',
'3xl': 'var(--radius-3xl, 2rem)',
full: '9999px',
},
// Box shadow tokens
boxShadow: {
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',
none: 'none',
},
// Font family
fontFamily: {
sans: [
'Inter',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Helvetica Neue',
'Arial',
'sans-serif',
],
mono: ['JetBrains Mono', 'Fira Code', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
},
// Animation
animation: {
'spin-slow': 'spin 3s linear infinite',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'bounce-slow': 'bounce 2s infinite',
'fade-in': 'fadeIn 0.2s ease-out',
'fade-out': 'fadeOut 0.2s ease-in',
'slide-in': 'slideIn 0.2s ease-out',
'slide-out': 'slideOut 0.2s ease-in',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
fadeOut: {
'0%': { opacity: '1' },
'100%': { opacity: '0' },
},
slideIn: {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideOut: {
'0%': { transform: 'translateY(0)', opacity: '1' },
'100%': { transform: 'translateY(-10px)', opacity: '0' },
},
},
// Transition
transitionDuration: {
250: '250ms',
350: '350ms',
400: '400ms',
},
},
},
plugins: [],
};
export default preset;

View file

@ -1,117 +0,0 @@
/**
* Tailwind CSS v4 Configuration
*
* This file provides the CSS-first configuration for Tailwind v4.
* Import this file in your app.css instead of using tailwind.config.js
*
* Usage:
* @import "tailwindcss";
* @import "@mana/shared-tailwind/v4";
*/
/* ===== Theme Configuration ===== */
@theme {
/* Brand color */
--color-mana: #4287f5;
/* Semantic colors using CSS variables */
--color-background: hsl(var(--color-background));
--color-foreground: hsl(var(--color-foreground));
--color-primary: hsl(var(--color-primary));
--color-primary-foreground: hsl(var(--color-primary-foreground));
--color-secondary: hsl(var(--color-secondary));
--color-secondary-foreground: hsl(var(--color-secondary-foreground));
--color-surface: hsl(var(--color-surface));
--color-surface-hover: hsl(var(--color-surface-hover));
--color-surface-elevated: hsl(var(--color-surface-elevated));
--color-muted: hsl(var(--color-muted));
--color-muted-foreground: hsl(var(--color-muted-foreground));
--color-border: hsl(var(--color-border));
--color-border-strong: hsl(var(--color-border-strong));
--color-error: hsl(var(--color-error));
--color-success: hsl(var(--color-success));
--color-warning: hsl(var(--color-warning));
--color-input: hsl(var(--color-input));
--color-ring: hsl(var(--color-ring));
/* Legacy aliases */
--color-content: hsl(var(--color-surface));
--color-content-hover: hsl(var(--color-surface-hover));
--color-content-page: hsl(var(--color-background));
--color-menu: hsl(var(--color-muted));
--color-menu-hover: hsl(var(--color-surface-hover));
--color-theme: hsl(var(--color-foreground));
--color-theme-secondary: hsl(var(--color-muted-foreground));
--color-primary-btn: hsl(var(--color-primary));
--color-primary-btn-text: hsl(var(--color-primary-foreground));
/* Border radius */
--radius-none: 0;
--radius-sm: var(--radius-sm, 0.25rem);
--radius-DEFAULT: var(--radius, 0.375rem);
--radius-md: var(--radius-md, 0.5rem);
--radius-lg: var(--radius-lg, 0.75rem);
--radius-xl: var(--radius-xl, 1rem);
--radius-2xl: var(--radius-2xl, 1.5rem);
--radius-3xl: var(--radius-3xl, 2rem);
--radius-full: 9999px;
/* Box shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-DEFAULT: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
--shadow-none: none;
/* Font families */
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', Menlo, Monaco, Consolas, monospace;
/* Transition durations */
--duration-250: 250ms;
--duration-350: 350ms;
--duration-400: 400ms;
/* Animations */
--animate-spin-slow: spin 3s linear infinite;
--animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce-slow: bounce 2s infinite;
--animate-fade-in: fadeIn 0.2s ease-out;
--animate-fade-out: fadeOut 0.2s ease-in;
--animate-slide-in: slideIn 0.2s ease-out;
--animate-slide-out: slideOut 0.2s ease-in;
}
/* ===== Keyframes ===== */
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes fadeOut {
0% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes slideIn {
0% { transform: translateY(-10px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
@keyframes slideOut {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(-10px); opacity: 0; }
}

View file

@ -1,183 +0,0 @@
/* Shared Theme CSS Variables for Mana Apps
* Import this in your app.css: @import '@mana/shared-tailwind/theme.css';
*
* Features:
* - 4 Theme Variants: Lume (default), Nature, Stone, Ocean
* - Light and Dark mode for each theme
* - Uses data-theme attribute for variant switching
* - Uses .dark class for dark mode
*/
:root {
--font-body: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Mono', monospace;
}
/* Default Theme: Lume Light */
:root {
--color-primary: #f8d62b;
--color-primary-button: #f8d62b;
--color-primary-button-text: #000000;
--color-secondary: #d4b200;
--color-secondary-button: #ffe9a3;
--color-content-bg: #ffffff;
--color-content-bg-hover: #f5f5f5;
--color-content-page-bg: #ffffff;
--color-menu-bg: #dddddd;
--color-menu-bg-hover: #cccccc;
--color-panel-bg: #e8e8e8;
--color-page-bg: #dddddd;
--color-text: #2c2c2c;
--color-border-light: #f2f2f2;
--color-border: #999999;
--color-border-strong: #cccccc;
--color-error: #e74c3c;
}
/* Lume Dark */
:root.dark {
--color-primary: #f8d62b;
--color-primary-button: #7c6b16;
--color-primary-button-text: #ffffff;
--color-secondary: #d4b200;
--color-secondary-button: #1e1e1e;
--color-content-bg: #1e1e1e;
--color-content-bg-hover: #333333;
--color-content-page-bg: #121212;
--color-menu-bg: #101010;
--color-menu-bg-hover: #333333;
--color-panel-bg: #1a1a1a;
--color-page-bg: #101010;
--color-text: #ffffff;
--color-border-light: #333333;
--color-border: #424242;
--color-border-strong: #616161;
--color-error: #e74c3c;
}
/* Nature Light */
:root[data-theme='nature'] {
--color-primary: #4caf50;
--color-primary-button: #a08500;
--color-primary-button-text: #ffffff;
--color-secondary: #81c784;
--color-secondary-button: #f1f8e9;
--color-content-bg: #f1f8e9;
--color-content-bg-hover: #e8f5e9;
--color-content-page-bg: #ffffff;
--color-menu-bg: #e8f5e9;
--color-menu-bg-hover: #c8e6c9;
--color-panel-bg: #eff8f0;
--color-page-bg: #fbfdf8;
--color-text: #1b5e20;
--color-border-light: #e8f5e9;
--color-border: #c8e6c9;
--color-border-strong: #a5d6a7;
--color-error: #e57373;
}
/* Nature Dark */
:root[data-theme='nature'].dark {
--color-primary: #4caf50;
--color-primary-button: #ff9500;
--color-primary-button-text: #000000;
--color-secondary: #81c784;
--color-secondary-button: #1e1e1e;
--color-content-bg: #1e1e1e;
--color-content-bg-hover: #2e7d32;
--color-content-page-bg: #121212;
--color-menu-bg: #252525;
--color-menu-bg-hover: #2e7d32;
--color-panel-bg: #2a2a2a;
--color-page-bg: #121212;
--color-text: #ffffff;
--color-border-light: #1b5e20;
--color-border: #2e7d32;
--color-border-strong: #388e3c;
--color-error: #cf6679;
}
/* Stone Light */
:root[data-theme='stone'] {
--color-primary: #607d8b;
--color-primary-button: #ff9500;
--color-primary-button-text: #000000;
--color-secondary: #90a4ae;
--color-secondary-button: #eceff1;
--color-content-bg: #eceff1;
--color-content-bg-hover: #e0e6ea;
--color-content-page-bg: #ffffff;
--color-menu-bg: #e0e6ea;
--color-menu-bg-hover: #cfd8dc;
--color-panel-bg: #e8edf0;
--color-page-bg: #f5f7f9;
--color-text: #263238;
--color-border-light: #eceff1;
--color-border: #cfd8dc;
--color-border-strong: #b0bec5;
--color-error: #ef5350;
}
/* Stone Dark */
:root[data-theme='stone'].dark {
--color-primary: #78909c;
--color-primary-button: #ff9500;
--color-primary-button-text: #000000;
--color-secondary: #90a4ae;
--color-secondary-button: #1e1e1e;
--color-content-bg: #1e1e1e;
--color-content-bg-hover: #37474f;
--color-content-page-bg: #121212;
--color-menu-bg: #252525;
--color-menu-bg-hover: #37474f;
--color-panel-bg: #2a2a2a;
--color-page-bg: #121212;
--color-text: #ffffff;
--color-border-light: #37474f;
--color-border: #455a64;
--color-border-strong: #546e7a;
--color-error: #cf6679;
}
/* Ocean Light */
:root[data-theme='ocean'] {
--color-primary: #039be5;
--color-primary-button: #ff9500;
--color-primary-button-text: #000000;
--color-secondary: #4fc3f7;
--color-secondary-button: #e1f5fe;
--color-content-bg: #e1f5fe;
--color-content-bg-hover: #b3e5fc;
--color-content-page-bg: #ffffff;
--color-menu-bg: #e1f5fe;
--color-menu-bg-hover: #b3e5fc;
--color-panel-bg: #ecf8fe;
--color-page-bg: #f5fcff;
--color-text: #01579b;
--color-border-light: #e1f5fe;
--color-border: #b3e5fc;
--color-border-strong: #81d4fa;
--color-error: #ef5350;
}
/* Ocean Dark */
:root[data-theme='ocean'].dark {
--color-primary: #039be5;
--color-primary-button: #ff9500;
--color-primary-button-text: #000000;
--color-secondary: #4fc3f7;
--color-secondary-button: #1e1e1e;
--color-content-bg: #1e1e1e;
--color-content-bg-hover: #0277bd;
--color-content-page-bg: #121212;
--color-menu-bg: #252525;
--color-menu-bg-hover: #0277bd;
--color-panel-bg: #2a2a2a;
--color-page-bg: #121212;
--color-text: #ffffff;
--color-border-light: #01579b;
--color-border: #0277bd;
--color-border-strong: #0288d1;
--color-error: #cf6679;
}

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@mana/shared-hono": "workspace:*",
"hono": "^4.7.0",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5",

View file

@ -9,7 +9,7 @@ import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { loadConfig } from './config';
import { getDb } from './db/connection';
import { errorHandler } from './middleware/error-handler';
import { serviceErrorHandler as errorHandler } from '@mana/shared-hono';
import { jwtAuth } from './middleware/jwt-auth';
import { FeedbackService } from './services/feedback';
import { healthRoutes } from './routes/health';

View file

@ -1,29 +0,0 @@
/**
* Global error handler middleware for Hono.
*/
import type { ErrorHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
export const errorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
const cause = err.cause as Record<string, unknown> | undefined;
return c.json(
{
statusCode: err.status,
message: err.message,
...(cause ? { details: cause } : {}),
},
err.status
);
}
console.error('Unhandled error:', err);
return c.json(
{
statusCode: 500,
message: 'Internal server error',
},
500
);
};

View file

@ -12,6 +12,7 @@
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@mana/shared-hono": "workspace:*",
"hono": "^4.7.0",
"better-auth": "^1.4.3",
"drizzle-orm": "^0.38.3",

View file

@ -38,6 +38,33 @@ import {
} from '../email/send';
import { sourceAppStore, passwordResetRedirectStore } from './stores';
/**
* Single source of truth for SSO trusted origins.
*
* Better Auth rejects any cross-origin auth request whose Origin header
* isn't in this list silent login failure on mis-configured apps. When
* adding a new top-level domain (NOT a path under mana.how), update both:
*
* 1. This array
* 2. The `mana-auth` `CORS_ORIGINS` env var in
* `docker-compose.macmini.yml` (must be a superset of this list)
*
* `sso-config.spec.ts` enforces both invariants. The unified app under
* `mana.how` does NOT need per-module subdomains here modules are routed
* by path on the same origin.
*/
export const TRUSTED_ORIGINS: string[] = [
// Unified app — all productivity apps live under mana.how
'https://mana.how',
'https://auth.mana.how',
// Separate apps (not part of the unified app)
'https://arcade.mana.how', // Games
'https://whopxl.mana.how', // Games
// Local development
'http://localhost:3001',
'http://localhost:5173',
];
/**
* JWT Custom Payload Interface
*
@ -240,17 +267,8 @@ export function createBetterAuth(databaseUrl: string) {
// Better Auth will reject cross-origin requests with credentials.
// When adding a new app, add its production domain here AND to
// CORS_ORIGINS in docker-compose.macmini.yml.
trustedOrigins: [
// Unified app — all productivity apps are now under mana.how
'https://mana.how',
'https://auth.mana.how',
// Separate apps (not part of unified app)
'https://arcade.mana.how', // Games
'https://whopxl.mana.how', // Games
// Local development
'http://localhost:3001',
'http://localhost:5173',
],
// Single source of truth: TRUSTED_ORIGINS (exported below).
trustedOrigins: TRUSTED_ORIGINS,
// Plugins
plugins: [

View file

@ -10,7 +10,7 @@ import { cors } from 'hono/cors';
import { loadConfig } from './config';
import { getDb } from './db/connection';
import { createBetterAuth } from './auth/better-auth.config';
import { errorHandler } from './middleware/error-handler';
import { serviceErrorHandler as errorHandler } from '@mana/shared-hono';
import { jwtAuth } from './middleware/jwt-auth';
import { serviceAuth } from './middleware/service-auth';
import { SecurityEventsService, AccountLockoutService } from './services/security';

View file

@ -1,29 +0,0 @@
/**
* Global error handler middleware for Hono.
*/
import type { ErrorHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
export const errorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
const cause = err.cause as Record<string, unknown> | undefined;
return c.json(
{
statusCode: err.status,
message: err.message,
...(cause ? { details: cause } : {}),
},
err.status
);
}
console.error('Unhandled error:', err);
return c.json(
{
statusCode: 500,
message: 'Internal server error',
},
500
);
};

View file

@ -11,6 +11,7 @@
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@mana/shared-hono": "workspace:*",
"hono": "^4.7.0",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5",

View file

@ -9,7 +9,7 @@ import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { loadConfig } from './config';
import { getDb } from './db/connection';
import { errorHandler } from './middleware/error-handler';
import { serviceErrorHandler as errorHandler } from '@mana/shared-hono';
import { jwtAuth } from './middleware/jwt-auth';
import { serviceAuth } from './middleware/service-auth';
import { StripeService } from './services/stripe';

View file

@ -1,29 +0,0 @@
/**
* Global error handler middleware for Hono.
*/
import type { ErrorHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
export const errorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
const cause = err.cause as Record<string, unknown> | undefined;
return c.json(
{
statusCode: err.status,
message: err.message,
...(cause ? { details: cause } : {}),
},
err.status
);
}
console.error('Unhandled error:', err);
return c.json(
{
statusCode: 500,
message: 'Internal server error',
},
500
);
};

View file

@ -11,6 +11,7 @@
"db:seed": "bun run src/db/seed.ts"
},
"dependencies": {
"@mana/shared-hono": "workspace:*",
"hono": "^4.7.0",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5",

View file

@ -9,7 +9,7 @@ import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { loadConfig } from './config';
import { getDb } from './db/connection';
import { errorHandler } from './middleware/error-handler';
import { serviceErrorHandler as errorHandler } from '@mana/shared-hono';
import { jwtAuth } from './middleware/jwt-auth';
import { serviceAuth } from './middleware/service-auth';
import { StripeService } from './services/stripe';

View file

@ -1,29 +0,0 @@
/**
* Global error handler middleware for Hono.
*/
import type { ErrorHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
export const errorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
const cause = err.cause as Record<string, unknown> | undefined;
return c.json(
{
statusCode: err.status,
message: err.message,
...(cause ? { details: cause } : {}),
},
err.status
);
}
console.error('Unhandled error:', err);
return c.json(
{
statusCode: 500,
message: 'Internal server error',
},
500
);
};

View file

@ -11,6 +11,7 @@
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@mana/shared-hono": "workspace:*",
"hono": "^4.7.0",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5",

View file

@ -9,7 +9,7 @@ import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { loadConfig } from './config';
import { getDb } from './db/connection';
import { errorHandler } from './middleware/error-handler';
import { serviceErrorHandler as errorHandler } from '@mana/shared-hono';
import { jwtAuth } from './middleware/jwt-auth';
import { TagsService } from './services/tags';
import { SettingsService } from './services/settings';

View file

@ -1,29 +0,0 @@
/**
* Global error handler middleware for Hono.
*/
import type { ErrorHandler } from 'hono';
import { HTTPException } from 'hono/http-exception';
export const errorHandler: ErrorHandler = (err, c) => {
if (err instanceof HTTPException) {
const cause = err.cause as Record<string, unknown> | undefined;
return c.json(
{
statusCode: err.status,
message: err.message,
...(cause ? { details: cause } : {}),
},
err.status
);
}
console.error('Unhandled error:', err);
return c.json(
{
statusCode: 500,
message: 'Internal server error',
},
500
);
};