mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:01:10 +02:00
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:
parent
3a3cd126cf
commit
919fcca4b7
63 changed files with 1072 additions and 5398 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) ──────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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), {
|
||||
|
|
|
|||
|
|
@ -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) ──────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) ────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) ─────
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()}"`,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 ─────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"name": "@mana/moodlit",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev"
|
||||
}
|
||||
"private": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"name": "@mana/news",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev"
|
||||
}
|
||||
"private": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
{
|
||||
"name": "@mana/uload",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run dev"
|
||||
}
|
||||
"private": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 ."
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue