Phase 0+1: Repo-Skelett für Cards-Greenfield
Strategie B (beschlossen 2026-05-08): Cards wird als eigenständige
föderierte App neu gebaut, ohne Code-Übernahme aus mana-monorepo.
Skelett enthält:
- apps/api: Hono+Bun mit /healthz, /version, Manifest-Endpoint, leere
pgSchema('cards'), Drizzle-Config, erstem Vitest
- apps/web: SvelteKit 2 + Svelte 5 (runes), Vite auf 3082
- packages/cards-domain: Pure-TS, CardType-Discriminated-Union,
SubIndex-Granularität für Reviews, Future-CardType-Set vorbereitet
- infrastructure/docker-compose.yml: Postgres 16 auf 5435
- app-manifest.json: v1.0.0, Verein-owned, beta-tier
- .github/workflows/ci.yml
- docs/LESSONS_FROM_MANA_MONOREPO.md (Read-Day-Output, 15 Lehren)
Pre-Flight für Phase 2 (Auth-Föderation): DNS cardecky.mana.how,
GitHub-Repo mana-ev/cards, Cards-App-Registrierung in mana-auth,
NPM_AUTH_TOKEN für Verdaccio.
Plan: mana/docs/playbooks/CARDS_GREENFIELD.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
8605b1b517
37 changed files with 1197 additions and 0 deletions
13
apps/api/drizzle.config.ts
Normal file
13
apps/api/drizzle.config.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import type { Config } from 'drizzle-kit';
|
||||
|
||||
export default {
|
||||
schema: './src/db/schema/*.ts',
|
||||
out: './src/db/migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL ?? 'postgresql://cards:cards@localhost:5435/cards',
|
||||
},
|
||||
schemaFilter: ['cards'],
|
||||
verbose: true,
|
||||
strict: true,
|
||||
} satisfies Config;
|
||||
32
apps/api/package.json
Normal file
32
apps/api/package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "@cards/api",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Cards-API — Hono+Bun-Backend für die Greenfield-Cards-App. Spricht mana-Plattform-Services über HTTP.",
|
||||
"scripts": {
|
||||
"dev": "bun run --hot src/index.ts",
|
||||
"start": "bun run src/index.ts",
|
||||
"build": "tsc -p tsconfig.json --noEmit",
|
||||
"type-check": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "echo 'lint configured later (eslint flat-config)'",
|
||||
"clean": "rm -rf dist .turbo coverage",
|
||||
"drizzle:generate": "drizzle-kit generate",
|
||||
"drizzle:push": "drizzle-kit push --force",
|
||||
"drizzle:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cards/domain": "workspace:*",
|
||||
"hono": "^4.6.0",
|
||||
"drizzle-orm": "0.38",
|
||||
"postgres": "^3.4.0",
|
||||
"zod": "3",
|
||||
"zod-to-json-schema": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"drizzle-kit": "0.30",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
13
apps/api/src/db/schema/index.ts
Normal file
13
apps/api/src/db/schema/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Drizzle-Schemas für die `cards`-Datenbank.
|
||||
//
|
||||
// Phase-3-Aufgabe (siehe CARDS_GREENFIELD.md): hier landen
|
||||
// `decks`, `cards`, `reviews`, `study_sessions`, `tags`, `media_refs`,
|
||||
// `import_jobs` als pgSchema('cards').table(...) Definitionen.
|
||||
//
|
||||
// Schema-Skizze in mana/docs/playbooks/CARDS_GREENFIELD.md §"Drizzle-Schema-Skizze".
|
||||
// Card-Type-Granularität (subIndex pro Karte) aus
|
||||
// docs/LESSONS_FROM_MANA_MONOREPO.md mitnehmen.
|
||||
|
||||
import { pgSchema } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const cardsSchema = pgSchema('cards');
|
||||
26
apps/api/src/index.ts
Normal file
26
apps/api/src/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Hono } from 'hono';
|
||||
|
||||
import { manifestRoute } from './routes/manifest.ts';
|
||||
import { healthRoute } from './routes/health.ts';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.route('/', healthRoute);
|
||||
app.route('/.well-known/mana-app.json', manifestRoute);
|
||||
|
||||
app.get('/', (c) =>
|
||||
c.json({
|
||||
app: 'cards',
|
||||
version: process.env.CARDS_API_VERSION ?? '0.0.0',
|
||||
see: '/.well-known/mana-app.json',
|
||||
})
|
||||
);
|
||||
|
||||
const port = Number(process.env.CARDS_API_PORT ?? 3081);
|
||||
|
||||
console.log(`[cards-api] listening on http://localhost:${port}`);
|
||||
|
||||
export default {
|
||||
port,
|
||||
fetch: app.fetch,
|
||||
};
|
||||
27
apps/api/src/routes/health.ts
Normal file
27
apps/api/src/routes/health.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Hono } from 'hono';
|
||||
|
||||
export const healthRoute = new Hono();
|
||||
|
||||
healthRoute.get('/healthz', (c) => c.json({ status: 'ok' }));
|
||||
|
||||
healthRoute.get('/healthz/details', (c) =>
|
||||
c.json({
|
||||
status: 'ok',
|
||||
app: 'cards',
|
||||
version: process.env.CARDS_API_VERSION ?? '0.0.0',
|
||||
uptime_s: Math.floor(process.uptime()),
|
||||
mana_packages: {
|
||||
// In Phase 5 mit @mana/shared-app-tpl ersetzen.
|
||||
'@mana/shared-share-protocol': 'TBD',
|
||||
'@mana/shared-app-tpl': 'TBD',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
healthRoute.get('/version', (c) =>
|
||||
c.json({
|
||||
app: 'cards',
|
||||
version: process.env.CARDS_API_VERSION ?? '0.0.0',
|
||||
build: process.env.CARDS_BUILD_SHA ?? 'dev',
|
||||
})
|
||||
);
|
||||
7
apps/api/src/routes/manifest.ts
Normal file
7
apps/api/src/routes/manifest.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Hono } from 'hono';
|
||||
|
||||
import manifest from '../../../../app-manifest.json' with { type: 'json' };
|
||||
|
||||
export const manifestRoute = new Hono();
|
||||
|
||||
manifestRoute.get('/', (c) => c.json(manifest));
|
||||
24
apps/api/tests/health.test.ts
Normal file
24
apps/api/tests/health.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { Hono } from 'hono';
|
||||
import { healthRoute } from '../src/routes/health.ts';
|
||||
|
||||
describe('health routes', () => {
|
||||
const app = new Hono();
|
||||
app.route('/', healthRoute);
|
||||
|
||||
it('GET /healthz returns ok', async () => {
|
||||
const res = await app.request('/healthz');
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { status: string };
|
||||
expect(body.status).toBe('ok');
|
||||
});
|
||||
|
||||
it('GET /version returns app + version + build', async () => {
|
||||
const res = await app.request('/version');
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { app: string; version: string; build: string };
|
||||
expect(body.app).toBe('cards');
|
||||
expect(body.version).toBeTruthy();
|
||||
expect(body.build).toBeTruthy();
|
||||
});
|
||||
});
|
||||
14
apps/api/tsconfig.json
Normal file
14
apps/api/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["bun-types"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "tests/**/*", "drizzle.config.ts"],
|
||||
"exclude": ["node_modules", "dist", ".turbo"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue