diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 59a7058..3aa9e22 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -32,7 +32,11 @@ Decimal phases appear between their surrounding integers in numeric order. 3. User can toggle between light and dark theme and the preference persists 4. User can switch between German and English interface language 5. All UI strings are rendered through the i18n framework (no hardcoded text) -**Plans**: TBD +**Plans**: 3 plans +Plans: +- [ ] 01-01-PLAN.md -- Walking Skeleton: Monorepo + Docker Compose stack with NestJS, Next.js, PostgreSQL, Traefik +- [ ] 01-02-PLAN.md -- Portal Shell: Design tokens, i18n (DE/EN), theme switching, responsive layout with header + sidebar +- [ ] 01-03-PLAN.md -- Visual Verification: Human confirms portal appearance and interactions **UI hint**: yes ### Phase 2: Authentication & Multi-Tenancy @@ -103,11 +107,11 @@ Decimal phases appear between their surrounding integers in numeric order. ## Progress **Execution Order:** -Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 +Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. Foundation & Portal Shell | 0/TBD | Not started | - | +| 1. Foundation & Portal Shell | 0/3 | Planned | - | | 2. Authentication & Multi-Tenancy | 0/TBD | Not started | - | | 3. Module System & Domaincheck | 0/TBD | Not started | - | | 4. Marketplace & Portal Navigation | 0/TBD | Not started | - | diff --git a/.planning/phases/01-foundation-portal-shell/01-01-PLAN.md b/.planning/phases/01-foundation-portal-shell/01-01-PLAN.md new file mode 100644 index 0000000..04602f4 --- /dev/null +++ b/.planning/phases/01-foundation-portal-shell/01-01-PLAN.md @@ -0,0 +1,354 @@ +--- +phase: 01-foundation-portal-shell +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - pnpm-workspace.yaml + - turbo.json + - tsconfig.base.json + - biome.json + - .gitignore + - .env + - .env.example + - .dockerignore + - docker-compose.yml + - docker-compose.dev.yml + - packages/shared/package.json + - packages/shared/tsconfig.json + - packages/shared/src/index.ts + - apps/api/package.json + - apps/api/tsconfig.json + - apps/api/src/main.ts + - apps/api/src/app.module.ts + - apps/api/src/health/health.module.ts + - apps/api/src/health/health.controller.ts + - apps/api/prisma/schema.prisma + - apps/api/Dockerfile + - apps/web/package.json + - apps/web/tsconfig.json + - apps/web/next.config.ts + - apps/web/postcss.config.mjs + - apps/web/src/app/layout.tsx + - apps/web/src/app/page.tsx + - apps/web/src/app/globals.css + - apps/web/Dockerfile +autonomous: true +requirements: + - INFRA-01 + - INFRA-02 + - INFRA-03 + +must_haves: + truths: + - "docker compose up starts all services without errors" + - "Browser request to localhost:80 returns a rendered Next.js page" + - "GET http://localhost:80/api/health returns JSON with status ok" + - "PostgreSQL container passes pg_isready health check" + - "Next.js container cannot directly reach PostgreSQL (network segmentation)" + artifacts: + - path: "docker-compose.yml" + provides: "Multi-service orchestration with 3 networks" + contains: "internal: true" + - path: "apps/api/src/health/health.controller.ts" + provides: "Health check endpoint" + exports: ["HealthController"] + - path: "apps/web/src/app/page.tsx" + provides: "Root page rendering" + min_lines: 5 + - path: "pnpm-workspace.yaml" + provides: "Monorepo workspace definition" + contains: "apps/*" + - path: "apps/api/prisma/schema.prisma" + provides: "Database schema foundation" + contains: "datasource db" + key_links: + - from: "docker-compose.yml" + to: "apps/api/Dockerfile" + via: "build context" + pattern: "build.*apps/api" + - from: "docker-compose.yml" + to: "apps/web/Dockerfile" + via: "build context" + pattern: "build.*apps/web" + - from: "apps/api/src/main.ts" + to: "apps/api/src/app.module.ts" + via: "NestFactory.create" + pattern: "NestFactory\\.create.*AppModule" +--- + +## Phase Goal + +**As a** user, **I want to** access a running portal application with responsive layout, theme switching, and bilingual interface, **so that** I have the structural frame into which all workflow modules will be placed. + + +Walking Skeleton: Establish the complete Docker Compose infrastructure with pnpm monorepo, NestJS API (health endpoint + Prisma), Next.js frontend (minimal page), PostgreSQL database, and Traefik reverse proxy -- all connected via three segregated Docker networks per INFRA-01, INFRA-02, INFRA-03. + +Purpose: Prove the full stack works end-to-end. After this plan, `docker compose up` serves a web page via Traefik, the API responds, and PostgreSQL is healthy -- the foundation every subsequent phase builds upon. + +Output: Running Docker Compose stack accessible at localhost:80, monorepo with build tooling, both apps containerized. + + + +@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md +@/home/vicolab/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/01-foundation-portal-shell/01-RESEARCH.md + + + + + + Task 1: Monorepo scaffold with root configs and shared package + + package.json + pnpm-workspace.yaml + turbo.json + tsconfig.base.json + biome.json + .gitignore + .env + .env.example + .dockerignore + packages/shared/package.json + packages/shared/tsconfig.json + packages/shared/src/index.ts + + + .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 6: Turborepo Configuration, Project Structure, Installation section) + + + Initialize the monorepo root. Run `corepack enable && corepack prepare pnpm@9 --activate` to enable pnpm. + + Create root `package.json` with: + - `"name": "tessera"`, `"private": true` + - `"packageManager": "pnpm@9.15.0"` + - `"scripts"`: `"dev": "turbo dev"`, `"build": "turbo build"`, `"lint": "turbo lint"`, `"type-check": "turbo type-check"` + - devDependencies: `turbo`, `@biomejs/biome`, `typescript` + + Create `pnpm-workspace.yaml` with packages: `["apps/*", "packages/*"]`. + + Create `turbo.json` per RESEARCH Pattern 6: tasks for build (dependsOn ^build, outputs dist/**/**.next/**), dev (cache false, persistent true), lint (outputs []), type-check (dependsOn ^build, outputs []). + + Create `tsconfig.base.json` with compilerOptions: strict true, esModuleInterop true, skipLibCheck true, forceConsistentCasingInFileNames true, resolveJsonModule true, isolatedModules true, moduleResolution "bundler", module "ESNext", target "ES2022", declaration true, declarationMap true, sourceMap true, composite false. + + Create `biome.json` with $schema, organizeImports enabled, formatter (indentStyle "space", indentWidth 2, lineWidth 100), linter enabled with recommended rules. + + Create `.gitignore` covering: node_modules, .next, dist, .turbo, .env (NOT .env.example), *.log, .DS_Store, coverage, .pnpm-store. + + Create `.env` with DB_PASSWORD=tessera_dev, DATABASE_URL=postgresql://tessera:tessera_dev@db:5432/tessera, NODE_ENV=development. Create `.env.example` as template (same keys, placeholder values). + + Create `.dockerignore` excluding: node_modules, .next, dist, .turbo, .git, .env, *.md, coverage. + + Create `packages/shared/package.json` with name "@tessera/shared", version "0.0.1", main "src/index.ts", types "src/index.ts", scripts (type-check: tsc --noEmit), devDependencies typescript. Create `packages/shared/tsconfig.json` extending ../../tsconfig.base.json with include ["src"]. Create `packages/shared/src/index.ts` exporting a simple APP_NAME constant "Tessera" and a HealthResponse type interface with status string and timestamp string fields. + + Run `pnpm install` at root to generate lockfile. + + + cd /home/vicolab/projects/tessera-ctl && test -f pnpm-lock.yaml && test -f turbo.json && test -f packages/shared/src/index.ts && pnpm turbo type-check --filter=@tessera/shared 2>&1 | tail -5 + + + - pnpm-lock.yaml exists at root (proves pnpm install succeeded) + - turbo.json contains "tasks" key with "build", "dev", "lint", "type-check" + - packages/shared/src/index.ts exports APP_NAME and HealthResponse interface + - `pnpm turbo type-check --filter=@tessera/shared` exits with code 0 + - tsconfig.base.json has "strict": true + - biome.json has "formatter" and "linter" sections + - .env contains DATABASE_URL with postgresql:// prefix + + Monorepo root with pnpm workspace, Turborepo, Biome, TypeScript base config, and shared package all type-check successfully. + + + + Task 2: NestJS API with health endpoint and Prisma schema + + apps/api/package.json + apps/api/tsconfig.json + apps/api/nest-cli.json + apps/api/src/main.ts + apps/api/src/app.module.ts + apps/api/src/health/health.module.ts + apps/api/src/health/health.controller.ts + apps/api/prisma/schema.prisma + apps/api/Dockerfile + + + packages/shared/src/index.ts (HealthResponse type) + .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 5: NestJS Docker Build, NestJS Health Endpoint, Standard Stack) + + + Create `apps/api/package.json` with name "@tessera/api", scripts: "build": "nest build", "start": "node dist/main.js", "start:dev": "nest start --watch", "type-check": "tsc --noEmit". Dependencies: @nestjs/core@^11, @nestjs/common@^11, @nestjs/platform-express@^11, @nestjs/config@^4, rxjs@^7, reflect-metadata@^0.2, @prisma/client@^7. DevDependencies: prisma@^7, @nestjs/cli@^11, typescript@^5.5, @types/node@^22, @types/express@^5. + + Create `apps/api/tsconfig.json` extending ../../tsconfig.base.json with compilerOptions: module "commonjs", target "ES2021", outDir "./dist", rootDir "./src", emitDecoratorMetadata true, experimentalDecorators true. Include ["src/**/*"], exclude ["node_modules", "dist"]. + + Create `apps/api/nest-cli.json` with $schema, collection @nestjs/schematics, sourceRoot "src", compilerOptions deleteOutDir true. + + Create `apps/api/src/main.ts`: import NestFactory from @nestjs/core, import AppModule. Create app with NestFactory.create(AppModule), enable CORS (origin: true for dev), listen on port 3001. Log "Tessera API running on port 3001". + + Create `apps/api/src/app.module.ts`: NestJS module importing ConfigModule.forRoot() (isGlobal: true) and HealthModule. + + Create `apps/api/src/health/health.module.ts`: NestJS module declaring and exporting HealthController. + + Create `apps/api/src/health/health.controller.ts`: Controller with prefix "health", single GET handler returning { status: "ok", timestamp: new Date().toISOString() } matching the HealthResponse interface from @tessera/shared (import the type for documentation but return plain object -- NestJS serializes it). + + Create `apps/api/prisma/schema.prisma`: datasource db (provider "postgresql", url env("DATABASE_URL")), generator client (provider "prisma-client-js"). Add a minimal Tenant model with id (uuid, default uuid()), name (String), slug (String, unique), createdAt (DateTime, default now()), updatedAt (DateTime, updatedAt) -- this seeds the multi-tenancy foundation. + + Create `apps/api/Dockerfile` per RESEARCH Pattern 5: multi-stage (base with corepack/pnpm, deps stage, builder stage running pnpm build, runner stage as non-root user "nestjs" exposing port 3001, CMD node dist/main.js). Use monorepo root as build context -- COPY the full workspace, install, then build only api. + + Actually for the Dockerfile, since we use monorepo root context: COPY root package.json, pnpm-workspace.yaml, pnpm-lock.yaml, then apps/api/ and packages/shared/, then pnpm install --frozen-lockfile --filter=@tessera/api..., then pnpm --filter=@tessera/api build. + + Run `cd apps/api && pnpm install` (from monorepo root `pnpm install` should suffice since workspace). + + + cd /home/vicolab/projects/tessera-ctl && pnpm install && pnpm turbo type-check --filter=@tessera/api 2>&1 | tail -5 + + + - apps/api/src/main.ts imports NestFactory and calls listen(3001) + - apps/api/src/health/health.controller.ts has @Controller('health') and @Get() decorators + - apps/api/prisma/schema.prisma contains "datasource db" with provider "postgresql" + - apps/api/prisma/schema.prisma contains "model Tenant" with id, name, slug fields + - apps/api/Dockerfile has USER nestjs and EXPOSE 3001 + - `pnpm turbo type-check --filter=@tessera/api` exits with code 0 + - apps/api/package.json has @nestjs/core and @prisma/client in dependencies + + NestJS API compiles, has a /health endpoint returning {status, timestamp}, Prisma schema defines PostgreSQL datasource with Tenant model, and Dockerfile is ready for multi-stage build. + + + + Task 3: Next.js app + Docker Compose stack with network segmentation + + apps/web/package.json + apps/web/tsconfig.json + apps/web/next.config.ts + apps/web/postcss.config.mjs + apps/web/src/app/layout.tsx + apps/web/src/app/page.tsx + apps/web/src/app/globals.css + apps/web/Dockerfile + docker-compose.yml + docker-compose.dev.yml + + + apps/api/Dockerfile (to understand build context pattern) + .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 1: Docker Network Segmentation, Pattern 4: Next.js Docker Build, docker-compose.yml example) + + + Create `apps/web/package.json` with name "@tessera/web", scripts: "dev": "next dev --turbopack", "build": "next build", "start": "next start", "type-check": "tsc --noEmit". Dependencies: next@^16, react@^19, react-dom@^19. DevDependencies: tailwindcss@^4, @tailwindcss/postcss@^4, postcss@^8, typescript@^5.5, @types/react@^19, @types/react-dom@^19. + + Create `apps/web/tsconfig.json` extending ../../tsconfig.base.json with compilerOptions: jsx "preserve", lib ["dom", "dom.iterable", "esnext"], module "esnext", moduleResolution "bundler", allowJs true, noEmit true, incremental true, plugins [{ name: "next" }], paths {"@/*": ["./src/*"]}. Include ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]. + + Create `apps/web/next.config.ts` with output: "standalone" as const. Minimal for now -- next-intl plugin added in Plan 02. + + Create `apps/web/postcss.config.mjs` exporting plugins: {"@tailwindcss/postcss": {}}. + + Create `apps/web/src/app/globals.css` with just `@import "tailwindcss";` -- full design tokens added in Plan 02. + + Create `apps/web/src/app/layout.tsx`: basic RootLayout with html (lang="de"), body with className for basic font. Import globals.css. Export metadata with title "Tessera" and description. + + Create `apps/web/src/app/page.tsx`: simple page rendering an h1 "Tessera" and a paragraph indicating the portal is loading. This is the skeleton placeholder -- Plan 02 replaces with full portal shell. + + Create `apps/web/Dockerfile` per RESEARCH Pattern 4: multi-stage (base with corepack/pnpm, deps, builder with standalone output, runner as non-root "nextjs" user, EXPOSE 3000, CMD node server.js). Use monorepo root context same strategy as API: COPY root configs, apps/web/, packages/shared/, install filtered, build. + + Create `docker-compose.yml` per RESEARCH Pattern 1 with three networks: + - services: traefik (image traefik:v3.4, ports 80:80, command --api.insecure=true --providers.docker=true --providers.docker.exposedbydefault=false, networks frontend-net, volumes /var/run/docker.sock:/var/run/docker.sock:ro) + - web (build context . dockerfile apps/web/Dockerfile, networks frontend-net + backend-net, depends_on api condition service_healthy, labels traefik.enable=true + traefik.http.routers.web.rule=PathPrefix(/) + traefik.http.services.web.loadbalancer.server.port=3000 + traefik.http.routers.web.priority=1) + - api (build context . dockerfile apps/api/Dockerfile, networks backend-net + data-net, depends_on db condition service_healthy, environment DATABASE_URL from .env, healthcheck curl -f http://localhost:3001/health interval 10s timeout 5s retries 3, labels traefik.enable=true + traefik.http.routers.api.rule=PathPrefix(/api) + traefik.http.services.api.loadbalancer.server.port=3001 + traefik.http.routers.api.priority=2 + traefik.http.middlewares.api-strip.stripprefix.prefixes=/api + traefik.http.routers.api.middlewares=api-strip) + - db (image postgres:16-alpine, networks data-net, volumes pgdata:/var/lib/postgresql/data, environment POSTGRES_USER=tessera POSTGRES_PASSWORD=${DB_PASSWORD:-tessera_dev} POSTGRES_DB=tessera, healthcheck pg_isready -U tessera interval 5s timeout 3s retries 5) + - networks: frontend-net (bridge), backend-net (bridge), data-net (bridge, internal: true) + - volumes: pgdata + + Create `docker-compose.dev.yml` with volume mounts for hot reload: web volumes ./apps/web/src:/app/apps/web/src, api volumes ./apps/api/src:/app/apps/api/src. Override web command to "pnpm --filter @tessera/web dev" and api command to "pnpm --filter @tessera/api start:dev". + + Run `pnpm install` at root to install web dependencies. Then run `docker compose build` to verify images build successfully. Then `docker compose up -d` and verify with curl. + + + cd /home/vicolab/projects/tessera-ctl && docker compose build --no-cache 2>&1 | tail -10 && docker compose up -d && sleep 15 && curl -s http://localhost:80 | grep -o "Tessera" | head -1 && curl -s http://localhost:80/api/health | grep -o '"status":"ok"' && docker compose exec db pg_isready -U tessera && docker compose down + + + - docker compose build completes without errors for all services (web, api, db, traefik) + - docker compose up -d starts all 4 containers (traefik, web, api, db) + - curl http://localhost:80 returns HTML containing "Tessera" + - curl http://localhost:80/api/health returns JSON with "status":"ok" + - docker compose exec db pg_isready -U tessera exits with code 0 + - docker-compose.yml defines three networks: frontend-net, backend-net, data-net + - data-net has "internal: true" (PostgreSQL unreachable from outside) + - web service is on frontend-net and backend-net (NOT data-net) + - api service is on backend-net and data-net (NOT frontend-net) + + Full Docker Compose stack runs: Traefik proxies to Next.js (port 80) and NestJS API (/api/*), PostgreSQL is healthy on internal network, network segmentation enforced. + + + + +## Artifacts This Phase Produces + +| Symbol/File | Type | Consumed By | +|---|---|---| +| `docker-compose.yml` | Docker Compose config | All subsequent phases (service foundation) | +| `docker-compose.dev.yml` | Dev overrides | Local development workflow | +| `pnpm-workspace.yaml` | Monorepo config | All package operations | +| `turbo.json` | Build orchestration | All build/dev/lint commands | +| `tsconfig.base.json` | TypeScript config | All apps and packages | +| `biome.json` | Linter/formatter config | All code quality checks | +| `@tessera/shared` (packages/shared) | Shared types package | apps/web, apps/api | +| `HealthResponse` type | Interface | apps/api health controller | +| `APP_NAME` constant | String export | UI components | +| `apps/api/src/main.ts` | NestJS entrypoint | Docker CMD | +| `apps/api/src/health/health.controller.ts` | Health endpoint | Docker healthcheck, monitoring | +| `apps/api/prisma/schema.prisma` | Database schema | Prisma migrations, Phase 2 RLS | +| `apps/web/src/app/layout.tsx` | Root layout | All pages (Plan 02 adds providers) | +| `apps/web/src/app/page.tsx` | Dashboard page | Plan 02 replaces content | +| `apps/web/Dockerfile` | Frontend image | docker-compose.yml | +| `apps/api/Dockerfile` | Backend image | docker-compose.yml | + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Browser -> Traefik | External traffic enters Docker stack | +| Traefik -> Next.js | Reverse proxy to frontend container | +| Next.js -> NestJS | Frontend calls backend API | +| NestJS -> PostgreSQL | Backend accesses database on internal network | +| Docker socket -> Traefik | Container management API exposure | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-01-01 | Elevation of Privilege | Docker socket in Traefik | mitigate | Mount /var/run/docker.sock:ro (read-only) | +| T-01-02 | Information Disclosure | PostgreSQL port | mitigate | data-net has internal:true, no port mapping to host | +| T-01-03 | Information Disclosure | .env file with DB credentials | mitigate | .env in .gitignore, .env.example has placeholder values only | +| T-01-04 | Elevation of Privilege | Container running as root | mitigate | Explicit USER directive (nextjs/nestjs) in both Dockerfiles, no sudo | +| T-01-05 | Tampering | Dependency confusion | mitigate | pnpm strict mode, lockfile committed, --frozen-lockfile in Docker builds | +| T-01-SC | Tampering | npm installs | mitigate | All packages pass legitimacy audit in RESEARCH.md; no [ASSUMED]/[SUS] packages requiring human checkpoint | + + + +1. `docker compose up -d` starts all services without error +2. `curl http://localhost:80` returns rendered HTML with "Tessera" +3. `curl http://localhost:80/api/health` returns `{"status":"ok","timestamp":"..."}` +4. `docker compose exec db pg_isready -U tessera` exits 0 +5. `docker compose exec web ping db` fails (network segmentation working) +6. `pnpm turbo type-check` passes for all packages + + + +- Docker Compose stack starts with 4 healthy containers (traefik, web, api, db) +- Browser at localhost:80 shows rendered Next.js page +- API health endpoint returns 200 with JSON body +- PostgreSQL is isolated on internal network +- Monorepo builds and type-checks cleanly +- All files follow the project structure defined in RESEARCH.md + + + +Create `.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md` when done + diff --git a/.planning/phases/01-foundation-portal-shell/01-02-PLAN.md b/.planning/phases/01-foundation-portal-shell/01-02-PLAN.md new file mode 100644 index 0000000..06520d7 --- /dev/null +++ b/.planning/phases/01-foundation-portal-shell/01-02-PLAN.md @@ -0,0 +1,347 @@ +--- +phase: 01-foundation-portal-shell +plan: 02 +type: execute +wave: 2 +depends_on: + - 01-01 +files_modified: + - apps/web/package.json + - apps/web/next.config.ts + - apps/web/src/app/globals.css + - apps/web/src/app/layout.tsx + - apps/web/src/app/page.tsx + - apps/web/src/i18n/request.ts + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json + - apps/web/src/lib/stores/sidebar-store.ts + - apps/web/src/components/layout/app-shell.tsx + - apps/web/src/components/layout/header.tsx + - apps/web/src/components/layout/sidebar.tsx + - apps/web/src/components/layout/sidebar-footer.tsx + - apps/web/src/components/locale-switcher.tsx + - apps/web/src/components/theme-toggle.tsx +autonomous: true +requirements: + - PRTAL-01 + - PRTAL-04 + - UI-01 + - UI-02 + - UI-03 + +must_haves: + truths: + - "User sees a sticky header with logo/branding left, page title center, and user menu area right (D-07, D-08, D-09)" + - "User sees a left sidebar that collapses to icon-width and expands to ~240px (D-01, D-02)" + - "On mobile, sidebar is hidden with a hamburger button to open (D-03)" + - "Sidebar shows Dashboard and Marketplace entries with accordion categories (D-05, D-15)" + - "Sidebar has a footer area with settings icon and user info placeholder (D-06)" + - "User can toggle between light and dark theme and the preference persists across refresh (UI-01)" + - "User can switch between German and English, preference stored in cookie (UI-02)" + - "All visible UI text is rendered via t() -- no hardcoded strings (UI-03)" + - "Main content area shows empty dashboard state with placeholder text per D-16" + - "Layout adapts responsively to different screen sizes (PRTAL-04)" + artifacts: + - path: "apps/web/src/components/layout/header.tsx" + provides: "Sticky header with branding, title, user menu" + min_lines: 30 + - path: "apps/web/src/components/layout/sidebar.tsx" + provides: "Collapsible sidebar with accordion categories" + min_lines: 50 + - path: "apps/web/src/components/layout/app-shell.tsx" + provides: "Portal layout composition (header + sidebar + main)" + min_lines: 20 + - path: "apps/web/src/components/theme-toggle.tsx" + provides: "Light/dark theme toggle button" + min_lines: 15 + - path: "apps/web/src/components/locale-switcher.tsx" + provides: "DE/EN language switcher" + min_lines: 15 + - path: "apps/web/src/messages/de.json" + provides: "German translations" + contains: "Dashboard" + - path: "apps/web/src/messages/en.json" + provides: "English translations" + contains: "Dashboard" + - path: "apps/web/src/app/globals.css" + provides: "Design tokens with OKLCH colors and dark mode" + contains: "oklch" + - path: "apps/web/src/lib/stores/sidebar-store.ts" + provides: "Sidebar collapse/expand state with persistence" + exports: ["useSidebarStore"] + - path: "apps/web/src/i18n/request.ts" + provides: "Locale resolution from NEXT_LOCALE cookie" + contains: "getRequestConfig" + key_links: + - from: "apps/web/src/app/layout.tsx" + to: "apps/web/src/components/layout/app-shell.tsx" + via: "Component composition" + pattern: "AppShell" + - from: "apps/web/src/app/layout.tsx" + to: "next-themes" + via: "ThemeProvider wrapper" + pattern: "ThemeProvider" + - from: "apps/web/src/app/layout.tsx" + to: "next-intl" + via: "NextIntlClientProvider wrapper" + pattern: "NextIntlClientProvider" + - from: "apps/web/src/components/layout/sidebar.tsx" + to: "apps/web/src/lib/stores/sidebar-store.ts" + via: "Zustand store consumption" + pattern: "useSidebarStore" + - from: "apps/web/src/components/locale-switcher.tsx" + to: "NEXT_LOCALE cookie" + via: "document.cookie set" + pattern: "NEXT_LOCALE" +--- + +## Phase Goal + +**As a** user, **I want to** access a running portal application with responsive layout, theme switching, and bilingual interface, **so that** I have the structural frame into which all workflow modules will be placed. + + +Portal Shell: Build the complete portal UI with design token system (yellow #ffed00 primary, dark gray-blue dark mode per D-10, D-14), i18n framework (DE/EN via next-intl cookie-based per UI-02, UI-03), theme switching (light/dark via next-themes per UI-01), and responsive layout (sticky header per D-07/D-08/D-09, collapsible sidebar per D-01 through D-06, main content area with empty dashboard state per D-15/D-16) -- implementing all 17 locked user decisions. + +Purpose: After this plan, the user sees and interacts with the full portal frame. They can collapse the sidebar, toggle theme, switch language, and see the empty dashboard -- every visual and interactive element of Phase 1. + +Output: Fully styled, responsive, bilingual portal shell with theme support, ready for module content in later phases. + + + +@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md +@/home/vicolab/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/01-foundation-portal-shell/01-RESEARCH.md +@.planning/phases/01-foundation-portal-shell/01-CONTEXT.md +@.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md + + + + + + Task 1: Design token system + i18n framework + theme provider setup + + apps/web/package.json + apps/web/next.config.ts + apps/web/src/app/globals.css + apps/web/src/app/layout.tsx + apps/web/src/i18n/request.ts + apps/web/src/messages/de.json + apps/web/src/messages/en.json + + + apps/web/package.json (current dependencies from Plan 01) + apps/web/next.config.ts (current config from Plan 01) + apps/web/src/app/layout.tsx (current layout from Plan 01) + apps/web/src/app/globals.css (current CSS from Plan 01) + .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 2: next-intl, Pattern 3: Design Tokens, Root Layout with Providers, Message JSON Structure, next.config.ts example) + .planning/phases/01-foundation-portal-shell/01-CONTEXT.md (D-10 through D-14 for colors/style) + + + Install additional dependencies in apps/web: `pnpm add next-intl next-themes zustand` (from the web directory or via --filter @tessera/web from root). + + Update `apps/web/next.config.ts` to use the next-intl plugin: import createNextIntlPlugin from "next-intl/plugin", call withNextIntl("./src/i18n/request.ts"), wrap the nextConfig (output: "standalone") with it, export default. + + Replace `apps/web/src/app/globals.css` with the full design token system from RESEARCH Pattern 3. Include: + - `@import "tailwindcss";` at top + - `@theme inline` block mapping CSS variable names to Tailwind utility classes (--color-primary, --color-primary-foreground, --color-accent, --color-accent-foreground, --color-background, --color-foreground, --color-card, --color-card-foreground, --color-muted, --color-muted-foreground, --color-border, --color-input, --color-ring, --color-sidebar-*, --radius-sm, --radius-md, --radius-lg) + - `:root` block with light mode OKLCH values: --radius 0.5rem (8px per D-13), --primary oklch(0.91 0.19 102) (yellow #ffed00 per D-10), --primary-foreground oklch(0.20 0.02 90) dark text on yellow, --background oklch(0.99 0 0), --foreground oklch(0.15 0 0), plus all card/secondary/muted/accent/border/input/ring tokens + - `.dark` block with dark mode values: --background oklch(0.17 0.01 260) dark gray-blue per D-14 discretion, --foreground oklch(0.95 0 0), --primary stays oklch(0.91 0.19 102) vibrant yellow, all other dark variants as in RESEARCH + - Add --font-sans variable set to "Inter, system-ui, -apple-system, sans-serif" (discretion: Inter for clean modern style) + - Add sidebar-specific tokens: --sidebar-width: 240px (D-02), --sidebar-width-collapsed: 64px, --header-height: 60px (D-08, middle of 56-64px range) + - Add base body styles: font-family var(--font-sans), antialiased + + Create `apps/web/src/i18n/request.ts` per RESEARCH Pattern 2: import getRequestConfig from "next-intl/server", import cookies from "next/headers". Export default getRequestConfig that reads NEXT_LOCALE cookie (fallback "de"), dynamically imports messages from ../messages/${locale}.json, returns { locale, messages }. + + Create `apps/web/src/messages/de.json` with the full German translation structure from RESEARCH: common (appName, loading, save, cancel), header (search placeholder, userMenu), sidebar (dashboard, marketplace, settings, collapse, expand), dashboard (empty: "Keine Widgets aktiv", addWidget: "Widget hinzufuegen"), theme (light: "Hell", dark: "Dunkel", system: "System"), locale (de: "Deutsch", en: "English"). Add additional keys: sidebar.categories (label), header.breadcrumb (home: "Startseite"). + + Create `apps/web/src/messages/en.json` with matching English translations: common (appName, loading, save, cancel), header (search, userMenu), sidebar (dashboard, marketplace, settings, collapse, expand), dashboard (empty: "No active widgets", addWidget: "Add widget"), theme (light, dark, system), locale (de: "Deutsch", en: "English"). Mirror all keys from de.json exactly. + + Update `apps/web/src/app/layout.tsx` to the full provider-wrapped layout: async function RootLayout, call getLocale() and getMessages() from "next-intl/server". Wrap children in html (lang={locale}, suppressHydrationWarning) > body (className with font-sans, antialiased, bg-background, text-foreground) > ThemeProvider (attribute="class", defaultTheme="system", enableSystem, disableTransitionOnChange) > NextIntlClientProvider (messages={messages}) > {children}. Import globals.css. Export metadata with title "Tessera", description from t or static. + + + cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/web 2>&1 | tail -5 && node -e "const de = require('./apps/web/src/messages/de.json'); const en = require('./apps/web/src/messages/en.json'); const deKeys = JSON.stringify(Object.keys(de).sort()); const enKeys = JSON.stringify(Object.keys(en).sort()); if(deKeys !== enKeys) { console.error('MISMATCH:', deKeys, enKeys); process.exit(1); } console.log('i18n keys match')" + + + - apps/web/src/app/globals.css contains @import "tailwindcss" and @theme inline block + - globals.css :root contains --primary with oklch(0.91 0.19 102) value (yellow #ffed00 per D-10) + - globals.css .dark contains --background with oklch(0.17 0.01 260) value (dark gray-blue per D-14) + - globals.css contains --radius: 0.5rem (8px per D-13) + - globals.css contains --sidebar-width: 240px (per D-02) + - globals.css contains --header-height: 60px (per D-08) + - apps/web/src/i18n/request.ts imports getRequestConfig and reads NEXT_LOCALE cookie + - de.json and en.json have identical top-level key structure + - de.json contains "dashboard.empty": "Keine Widgets aktiv" (per D-16 German) + - en.json contains "dashboard.empty": "No active widgets" + - apps/web/src/app/layout.tsx wraps children in ThemeProvider and NextIntlClientProvider + - layout.tsx calls getLocale() and getMessages() from next-intl/server + - layout.tsx html tag has lang={locale} and suppressHydrationWarning + - apps/web/next.config.ts uses createNextIntlPlugin wrapping the config + - `pnpm turbo type-check --filter=@tessera/web` exits with code 0 + + Design token system with OKLCH colors (yellow primary, dark gray-blue dark mode), i18n framework with DE/EN message files and cookie-based locale, and provider-wrapped root layout are all in place and type-check. + + + + Task 2: Portal layout components -- header, sidebar, app shell, interactions + + apps/web/src/lib/stores/sidebar-store.ts + apps/web/src/components/layout/app-shell.tsx + apps/web/src/components/layout/header.tsx + apps/web/src/components/layout/sidebar.tsx + apps/web/src/components/layout/sidebar-footer.tsx + apps/web/src/components/locale-switcher.tsx + apps/web/src/components/theme-toggle.tsx + apps/web/src/app/page.tsx + + + apps/web/src/app/layout.tsx (provider structure from Task 1) + apps/web/src/app/globals.css (design tokens and CSS variables from Task 1) + apps/web/src/messages/de.json (translation keys available) + apps/web/src/lib/stores/sidebar-store.ts (if exists, otherwise will create) + .planning/phases/01-foundation-portal-shell/01-CONTEXT.md (ALL decisions D-01 through D-17) + .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Zustand Sidebar Store example) + + + Create `apps/web/src/lib/stores/sidebar-store.ts` per RESEARCH example: Zustand store with persist middleware. Interface SidebarState with isCollapsed (boolean), isMobileOpen (boolean), toggle() (toggles isCollapsed), setMobileOpen(open: boolean). Persist to localStorage key "tessera-sidebar". Default isCollapsed: false, isMobileOpen: false. + + Create `apps/web/src/components/theme-toggle.tsx` ("use client"): Import useTheme from "next-themes", useTranslations from "next-intl". Render a button that cycles through light/dark/system themes. Display current theme icon (sun for light, moon for dark, monitor for system) using inline SVG or unicode symbols. Use t("theme.light"), t("theme.dark"), t("theme.system") for aria-label/tooltip. Style with Tailwind: rounded-md (per D-13 border-radius), hover:bg-muted, transition-colors. Include mounted state check to avoid hydration mismatch (common next-themes pattern: const [mounted, setMounted] = useState(false), useEffect to set mounted true, render placeholder while not mounted). + + Create `apps/web/src/components/locale-switcher.tsx` ("use client"): Per RESEARCH Pattern 2 locale-switcher example. Import useRouter from "next/navigation", useTranslations from "next-intl", useLocale from "next-intl". Button displays current locale label (t("locale.de") or t("locale.en")). On click: set document.cookie NEXT_LOCALE to opposite locale with path=/ max-age=31536000, then router.refresh() inside startTransition. Style with Tailwind matching the theme-toggle button style. + + Create `apps/web/src/components/layout/header.tsx` ("use client"): Per D-07, D-08, D-09. Fixed/sticky header at top. Height var(--header-height) via h-[var(--header-height)] or h-15 (60px). CSS: sticky top-0 z-50 bg-background border-b border-border. + + Layout is a flex row with three sections: + - Left: Logo area. Render the Tessera logo text (use t("common.appName")) styled with primary color (text-primary font-bold). Include a mobile hamburger button (visible only on md:hidden) that calls useSidebarStore().setMobileOpen(true). + - Center: Breadcrumb / page title. Render t("header.breadcrumb.home") or a simple "Dashboard" text. Use text-muted-foreground, truncate for overflow. + - Right: Actions area with theme-toggle, locale-switcher, and a user avatar placeholder (circular div with bg-muted, will be wired to auth in Phase 2). Flex row with gap-2. + + All strings via useTranslations(). No hardcoded text per UI-03. + + Create `apps/web/src/components/layout/sidebar.tsx` ("use client"): Per D-01 through D-06. + + Outer container: fixed left-0, top-[var(--header-height)], height calc(100vh - var(--header-height)), bg-background, border-r border-border, transition-all duration-200. Width toggles between var(--sidebar-width) when expanded and var(--sidebar-width-collapsed) when collapsed. Use useSidebarStore() for isCollapsed state. + + Mobile behavior (D-03): On screens below md breakpoint, sidebar is position fixed, full height, translated off-screen by default (transform -translate-x-full). When isMobileOpen is true, translate-x-0 with a backdrop overlay (fixed inset-0 bg-black/50 z-40). Tapping overlay calls setMobileOpen(false). + + Content structure: + - Top section: Navigation items. Two fixed entries visible in empty state (discretion: Dashboard and Marketplace). Each entry is a flex row with icon (inline SVG or emoji placeholder) + label text. When collapsed (D-01), only icons show (label hidden via overflow-hidden or conditional render). Active item (Dashboard per D-15) has bg-primary/10 text-primary styling. + - Categories section (D-05): An accordion-style expandable section labeled t("sidebar.categories"). When expanded shows "No modules" placeholder. Use a simple disclosure pattern (button toggles visibility of child div) -- no external accordion library needed. + - Toggle button: At bottom of nav section, a button to collapse/expand. Calls useSidebarStore().toggle(). Shows collapse icon (chevron-left) when expanded, expand icon (chevron-right) when collapsed. Label: t("sidebar.collapse") / t("sidebar.expand"), hidden when collapsed. + + All strings via useTranslations("sidebar"). No hardcoded text per UI-03. + + Create `apps/web/src/components/layout/sidebar-footer.tsx` ("use client"): Per D-06. Footer area at bottom of sidebar. Contains: + - Settings icon (gear/cog) as a button/link. When sidebar collapsed, just the icon. When expanded, icon + t("sidebar.settings") label. + - User info placeholder: small avatar circle + username text (placeholder "User" for now -- wired in Phase 2). When collapsed, just avatar. When expanded, avatar + name. + Flex column with gap-2, border-t border-border, p-3. + + Create `apps/web/src/components/layout/app-shell.tsx`: Server component or client component composing the layout. Renders Header at top, Sidebar on left, main content area filling remaining space. Main area: ml-[var(--sidebar-width)] when sidebar expanded, ml-[var(--sidebar-width-collapsed)] when collapsed. On mobile (below md): ml-0 (sidebar overlays). Transition-all for smooth sidebar animation. Padding p-6 on main content. The children prop renders inside main. + + Since app-shell needs to read sidebar state for margin calculation, make it "use client" and import useSidebarStore. Alternatively, use CSS-only approach with a data attribute on a parent. Prefer the Zustand approach for consistency. + + Update `apps/web/src/app/page.tsx`: The dashboard page. Per D-15 (dashboard is start page) and D-16 (empty state). Render within the app-shell. Display a centered empty state: an icon (layout-grid or similar), t("dashboard.empty") text ("Keine Widgets aktiv" in German), and a button styled with bg-primary text-primary-foreground rounded-md (per D-13) showing t("dashboard.addWidget"). Button is non-functional for now (dashboard implementation is Phase 5). Use useTranslations("dashboard"). + + Wire everything together: In layout.tsx (from Task 1), the children are wrapped by providers. In page.tsx, render AppShell wrapping the dashboard content. AppShell renders Header, Sidebar (with SidebarFooter inside), and main area with {children}. + + Responsive behavior (PRTAL-04): Test at Tailwind breakpoints. Below md (~768px): sidebar hidden, hamburger visible in header. Above md: sidebar visible, hamburger hidden. Above lg (~1024px): full sidebar width. + + Visual style (D-12): Modern/clean with whitespace. Use shadow-sm on header (subtle shadow per D-12). Cards and containers use rounded-lg (per D-13). Spacing generous (p-4, gap-4 minimum). + + + cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/web 2>&1 | tail -5 && grep -r "\"use client\"" apps/web/src/components/ --include="*.tsx" -l | wc -l && grep -rn ">[A-Z][a-z]" apps/web/src/components/ apps/web/src/app/page.tsx --include="*.tsx" | grep -v "import\|from\|//\|className\|type\|interface\|export\|const\|SVG\|path\|svg\|function\|return\|useState\|useEffect\|use client\|next" | head -20 + + + - apps/web/src/lib/stores/sidebar-store.ts exports useSidebarStore with isCollapsed, isMobileOpen, toggle, setMobileOpen + - sidebar-store.ts uses persist middleware with key "tessera-sidebar" + - apps/web/src/components/layout/header.tsx is a "use client" component + - header.tsx has sticky positioning (className contains "sticky" and "top-0") + - header.tsx contains three sections: logo/branding left, breadcrumb center, actions right + - header.tsx includes hamburger button visible only on mobile (md:hidden class) + - header.tsx renders ThemeToggle and LocaleSwitcher components + - apps/web/src/components/layout/sidebar.tsx reads isCollapsed from useSidebarStore + - sidebar.tsx implements width transition between expanded (~240px) and collapsed (~64px) states + - sidebar.tsx shows Dashboard entry with active styling (bg-primary or similar) + - sidebar.tsx shows Marketplace entry + - sidebar.tsx has accordion-style categories section per D-05 + - sidebar.tsx on mobile uses translate-x transform with backdrop overlay + - apps/web/src/components/layout/sidebar-footer.tsx renders settings icon and user placeholder + - apps/web/src/components/layout/app-shell.tsx composes Header, Sidebar, and main content area + - app-shell.tsx adjusts main content margin based on sidebar collapse state + - apps/web/src/app/page.tsx shows empty dashboard state with t("dashboard.empty") and t("dashboard.addWidget") + - apps/web/src/components/theme-toggle.tsx uses useTheme() from next-themes with mounted guard + - apps/web/src/components/locale-switcher.tsx sets NEXT_LOCALE cookie and calls router.refresh() + - All component text uses useTranslations() calls -- no hardcoded user-visible strings (UI-03) + - `pnpm turbo type-check --filter=@tessera/web` exits with code 0 + + Complete portal shell with sticky header (logo, breadcrumb, theme/locale/user), collapsible sidebar (nav items, accordion categories, footer with settings), responsive layout (mobile hamburger + overlay), empty dashboard state, theme toggle persisting to localStorage, and locale switcher persisting to cookie -- all text via i18n framework. + + + + +## Artifacts This Phase Produces + +| Symbol/File | Type | Consumed By | +|---|---|---| +| `useSidebarStore` | Zustand store | sidebar.tsx, app-shell.tsx, header.tsx (hamburger) | +| `apps/web/src/components/layout/header.tsx` | React component | app-shell.tsx | +| `apps/web/src/components/layout/sidebar.tsx` | React component | app-shell.tsx | +| `apps/web/src/components/layout/sidebar-footer.tsx` | React component | sidebar.tsx | +| `apps/web/src/components/layout/app-shell.tsx` | React component | page.tsx (and all future pages) | +| `apps/web/src/components/theme-toggle.tsx` | React component | header.tsx | +| `apps/web/src/components/locale-switcher.tsx` | React component | header.tsx | +| `apps/web/src/i18n/request.ts` | i18n config | next.config.ts plugin | +| `apps/web/src/messages/de.json` | German translations | All components via t() | +| `apps/web/src/messages/en.json` | English translations | All components via t() | +| `apps/web/src/app/globals.css` | Design token system | All styled components | +| CSS variables: `--primary`, `--sidebar-width`, `--header-height` | Design tokens | All layout components | + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Cookie (NEXT_LOCALE) | Client-set locale cookie consumed server-side | +| localStorage (tessera-sidebar, theme) | Client-persisted UI state | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-02-01 | Tampering | NEXT_LOCALE cookie | accept | Cookie only controls UI language (de/en). Invalid values fall back to "de". No security impact. | +| T-02-02 | Tampering | localStorage sidebar state | accept | Controls only UI collapse state. No data or auth implications. | +| T-02-03 | Information Disclosure | SSR-rendered locale/theme | accept | No sensitive data in theme/locale preferences | +| T-02-SC | Tampering | npm installs (next-intl, next-themes, zustand) | mitigate | All packages pass legitimacy audit in RESEARCH.md with OK verdict | + + + +1. `pnpm turbo type-check --filter=@tessera/web` passes +2. `pnpm turbo build --filter=@tessera/web` completes successfully +3. Docker rebuild and `docker compose up` serves the portal shell at localhost:80 +4. Clicking theme toggle cycles light/dark/system -- preference survives page refresh +5. Clicking locale switcher changes all text between DE and EN -- preference survives page refresh +6. Clicking sidebar collapse button animates sidebar to icon width +7. On narrow viewport (< 768px), sidebar is hidden and hamburger button appears in header +8. `grep -rn ">[A-Z]" apps/web/src/components/ --include="*.tsx"` returns no hardcoded user-visible strings + + + +- Portal shell renders with header (sticky, ~60px, logo + breadcrumb + actions) +- Sidebar collapses from 240px to 64px icon-width with smooth transition +- Mobile sidebar overlays with backdrop on hamburger click +- Theme toggle persists dark/light preference across browser refresh +- Language switch changes all UI text between German and English +- Empty dashboard shows "Keine Widgets aktiv" (DE) / "No active widgets" (EN) with add button +- All user-visible text comes from i18n message files (no hardcoded strings) +- Design uses yellow #ffed00 primary, dark gray-blue dark mode, 8px border-radius +- Modern/clean visual style with whitespace, subtle shadows per D-12 + + + +Create `.planning/phases/01-foundation-portal-shell/01-02-SUMMARY.md` when done + diff --git a/.planning/phases/01-foundation-portal-shell/01-03-PLAN.md b/.planning/phases/01-foundation-portal-shell/01-03-PLAN.md new file mode 100644 index 0000000..e8e8cd0 --- /dev/null +++ b/.planning/phases/01-foundation-portal-shell/01-03-PLAN.md @@ -0,0 +1,162 @@ +--- +phase: 01-foundation-portal-shell +plan: 03 +type: execute +wave: 3 +depends_on: + - 01-02 +files_modified: [] +autonomous: false +requirements: + - PRTAL-01 + - PRTAL-04 + - UI-01 + - UI-02 + +must_haves: + truths: + - "Human has verified the portal shell looks correct in browser" + - "Theme switching visually works (light/dark/system)" + - "Language switching visually works (DE/EN)" + - "Responsive layout adapts correctly on mobile and desktop" + - "Sidebar collapse/expand animates smoothly" +--- + +## Phase Goal + +**As a** user, **I want to** access a running portal application with responsive layout, theme switching, and bilingual interface, **so that** I have the structural frame into which all workflow modules will be placed. + + +Visual Verification: Human confirms the portal shell meets all design decisions (D-01 through D-17) and functional requirements (PRTAL-01, PRTAL-04, UI-01, UI-02, UI-03) by interacting with the running application in a browser. + +Purpose: Catch visual/interaction issues that automated checks cannot detect -- color accuracy, animation smoothness, layout proportions, overall aesthetic quality. + +Output: Human approval or issue list for correction. + + + +@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md +@/home/vicolab/.claude/gsd-core/templates/summary.md + + + +@.planning/phases/01-foundation-portal-shell/01-CONTEXT.md +@.planning/phases/01-foundation-portal-shell/01-02-SUMMARY.md + + + + + + Task 1: Rebuild Docker stack and verify automated checks + + + docker-compose.yml (verify configuration) + .planning/phases/01-foundation-portal-shell/01-02-SUMMARY.md (what was built) + + + Run `docker compose build` to rebuild images with the latest portal shell code. + Run `docker compose up -d` to start all services. + Wait for all containers to be healthy. + Run automated verification: curl localhost:80 returns HTML, curl localhost:80/api/health returns JSON with status ok, pg_isready passes. + Run `pnpm turbo type-check` to confirm all packages type-check. + Run the hardcoded-string grep check: `grep -rn ">[A-Z][a-z]" apps/web/src/components/ apps/web/src/app/page.tsx --include="*.tsx"` and review any matches (filter out non-user-visible strings like className values, imports, type annotations). + Leave the stack running for human verification. + + + cd /home/vicolab/projects/tessera-ctl && docker compose up -d --build 2>&1 | tail -5 && sleep 20 && curl -sf http://localhost:80 > /dev/null && echo "Web: OK" && curl -sf http://localhost:80/api/health && echo "" && echo "API: OK" && docker compose exec db pg_isready -U tessera && echo "DB: OK" + + + - All 4 containers are running (docker compose ps shows traefik, web, api, db) + - curl localhost:80 returns 200 with HTML content + - curl localhost:80/api/health returns {"status":"ok","timestamp":"..."} + - pg_isready exits 0 + - pnpm turbo type-check exits 0 + + Docker stack is running and all automated checks pass, ready for human visual verification. + + + + + Complete Tessera portal shell with: + - Sticky header (logo left, page title center, theme toggle + locale switch + user avatar right) per D-07/D-08/D-09 + - Collapsible left sidebar (240px expanded, ~64px collapsed icon-only) per D-01/D-02/D-04 + - Sidebar with Dashboard + Marketplace nav items, accordion categories, footer with settings + user info per D-05/D-06/D-15 + - Empty dashboard main content ("Keine Widgets aktiv" + add button) per D-16 + - Yellow #ffed00 primary color, dark gray-blue dark mode per D-10/D-14 + - Modern/clean visual style with rounded corners (~8px), subtle shadows per D-12/D-13 + - German/English language switching via cookie per UI-02/UI-03 + - Light/dark/system theme toggle per UI-01 + - Responsive layout: mobile hides sidebar with hamburger per D-03/PRTAL-04 + + + Open http://localhost:80 in your browser and check the following: + + **1. Overall Layout (D-07, D-12, PRTAL-01)** + - Header at top with logo "Tessera" on the left + - Page title / breadcrumb in center area + - Theme toggle, language switch, and user avatar placeholder on the right + - Header is sticky (scroll down if content exists -- it stays at top, D-09) + - Clean modern style with whitespace and subtle shadows + + **2. Sidebar Behavior (D-01, D-02, D-04, D-05, D-06)** + - Sidebar on left side, approximately 240px wide + - Click the collapse/expand button -- sidebar animates to icon-only width (~64px) + - In expanded state: Dashboard and Marketplace labels visible with icons + - Dashboard entry appears active/highlighted (D-15) + - Categories accordion exists and can be expanded (shows "no modules" placeholder) + - Footer area at bottom of sidebar has settings icon and user placeholder + - Click collapse -- icons remain visible, labels disappear + + **3. Theme Switching (UI-01, D-10, D-14)** + - Click theme toggle to switch to Dark mode + - Background changes to dark gray-blue (NOT pure black) + - Yellow primary color (#ffed00) remains vibrant on dark background + - Switch back to Light mode -- white/light background + - Refresh the page -- theme preference persists + + **4. Language Switching (UI-02, UI-03)** + - Default language should be German (Deutsch) + - All text is in German: "Dashboard", "Marktplatz", "Einstellungen", "Keine Widgets aktiv" + - Click language switch to English + - All text changes: "Dashboard", "Marketplace", "Settings", "No active widgets" + - Refresh the page -- language preference persists + + **5. Responsive Behavior (D-03, PRTAL-04)** + - Resize browser window to narrow width (< 768px) + - Sidebar should disappear + - Hamburger menu button should appear in the header + - Click hamburger -- sidebar slides in as overlay with backdrop + - Click backdrop -- sidebar closes + - Resize back to wide -- sidebar reappears normally + + **6. Empty Dashboard (D-15, D-16)** + - Main content area shows "Keine Widgets aktiv" (or English equivalent) + - An "Add widget" button is visible (non-functional is OK for Phase 1) + + **7. Color and Style (D-10, D-12, D-13)** + - Primary/brand color is yellow (#ffed00) visible on active states, buttons + - Corners are rounded (~8px radius) + - Overall feel: clean, modern, like Notion/Linear aesthetic + + Type "approved" if the portal looks and works correctly, or describe specific issues that need fixing. + + + + +## Artifacts This Phase Produces + +This plan produces no new code artifacts -- it verifies the artifacts from Plans 01 and 02. + + +All automated and human verification steps complete. The portal shell is confirmed visually correct and functionally working. + + + +- Human has verified all 7 checklist areas +- Human typed "approved" (or issues have been addressed) +- Docker stack remains running and healthy + + + +Create `.planning/phases/01-foundation-portal-shell/01-03-SUMMARY.md` when done + diff --git a/.planning/phases/01-foundation-portal-shell/SKELETON.md b/.planning/phases/01-foundation-portal-shell/SKELETON.md new file mode 100644 index 0000000..de87700 --- /dev/null +++ b/.planning/phases/01-foundation-portal-shell/SKELETON.md @@ -0,0 +1,55 @@ +# Walking Skeleton — Tessera + +**Phase:** 1 +**Generated:** 2026-06-18 + +## Capability Proven End-to-End + +A user can open a browser, see the Tessera portal shell with header, collapsible sidebar, and main content area, toggle between light and dark theme (persisted), and switch between German and English interface language -- all served from a Docker Compose stack with PostgreSQL, NestJS API, and Next.js frontend on segregated networks. + +## Architectural Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Monorepo | pnpm 9 + Turborepo 2.9 | Strict deps, workspace protocol, parallel builds with caching | +| Frontend | Next.js 16 App Router (standalone output) | React 19, Server Components, Turbopack, multi-tenant middleware support | +| Backend | NestJS 11 + Express 5 | Modular architecture maps to Tessera module system, DI, guards | +| Data layer | PostgreSQL 16 + Prisma 7 | RLS for multi-tenancy, TypeScript-first ORM, declarative migrations | +| Styling | Tailwind CSS 4 + shadcn/ui (CLI v4) | CSS-first config, design tokens via CSS variables, accessible components | +| i18n | next-intl 4.13 (without i18n routing) | Cookie-based locale, Server Component native, App Router built-in | +| Theme | next-themes 0.4 | SSR-safe, system preference detection, .dark class toggle | +| Client state | Zustand 5 | Sidebar toggle, UI prefs, 1.1kb, localStorage persistence | +| Deployment | Docker Compose with Traefik 3.x reverse proxy | Three-network segmentation (frontend/backend/data), health checks | +| Directory layout | apps/web, apps/api, packages/shared monorepo | Clear separation, independent Dockerfiles, shared types | +| Linting | Biome 2.x | Single tool replaces ESLint + Prettier, 100x faster | + +## Stack Touched in Phase 1 + +- [x] Project scaffold (pnpm monorepo, Turborepo, Biome, TypeScript) +- [x] Routing — Next.js App Router with root page (dashboard placeholder) +- [x] Database — PostgreSQL container with Prisma schema (health check read) +- [x] UI — Theme toggle, language switch, collapsible sidebar (interactive) +- [x] Deployment — Docker Compose full-stack with `docker compose up` command + +## Out of Scope (Deferred to Later Slices) + +- Authentication and user accounts (Phase 2) +- Multi-tenancy and RLS policies (Phase 2) +- Module system and dynamic loading (Phase 3) +- Marketplace and sidebar module listing (Phase 4) +- Dashboard widgets and drag-and-drop grid (Phase 5) +- Desktop Tauri wrapper (Phase 6) +- Configurable accent color UI (Phase 2 -- infrastructure prepared via CSS variable) +- Redis cache/sessions (Phase 2) +- Keycloak identity provider (Phase 2) +- E2E tests with Playwright (deferred -- smoke tests via curl suffice for skeleton) + +## Subsequent Slice Plan + +Each later phase adds one vertical slice on top of this skeleton without altering its architectural decisions: + +- Phase 2: Authenticated user can log in, see their name, operate within tenant boundary +- Phase 3: Admin can activate a module, user can interact with Domaincheck +- Phase 4: User can browse marketplace, activate modules, navigate via sidebar +- Phase 5: User can configure personal dashboard with draggable widgets +- Phase 6: User can install and run Tessera as a native desktop app