docs(01): create phase 1 plans -- walking skeleton + portal shell
Three plans for Foundation & Portal Shell phase: - 01-01: Monorepo scaffold, Docker Compose with 3-network segmentation, NestJS API, Next.js app, PostgreSQL - 01-02: Design tokens (yellow #ffed00 primary), i18n (DE/EN), theme switching, responsive portal layout - 01-03: Visual verification checkpoint for human approval Includes SKELETON.md documenting architectural decisions for subsequent phases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 | - |
|
||||
|
||||
@@ -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.
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation-portal-shell/01-RESEARCH.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Monorepo scaffold with root configs and shared package</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<read_first>
|
||||
.planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 6: Turborepo Configuration, Project Structure, Installation section)
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Monorepo root with pnpm workspace, Turborepo, Biome, TypeScript base config, and shared package all type-check successfully.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: NestJS API with health endpoint and Prisma schema</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<read_first>
|
||||
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)
|
||||
</read_first>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm install && pnpm turbo type-check --filter=@tessera/api 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Next.js app + Docker Compose stack with network segmentation</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<read_first>
|
||||
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)
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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)
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## 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 |
|
||||
|
||||
<threat_model>
|
||||
## 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 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -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.
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Design token system + i18n framework + theme provider setup</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<read_first>
|
||||
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)
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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')"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Portal layout components -- header, sidebar, app shell, interactions</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<read_first>
|
||||
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)
|
||||
</read_first>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## 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 |
|
||||
|
||||
<threat_model>
|
||||
## 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 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/01-foundation-portal-shell/01-02-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -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.
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/01-foundation-portal-shell/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation-portal-shell/01-02-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Rebuild Docker stack and verify automated checks</name>
|
||||
<files></files>
|
||||
<read_first>
|
||||
docker-compose.yml (verify configuration)
|
||||
.planning/phases/01-foundation-portal-shell/01-02-SUMMARY.md (what was built)
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Docker stack is running and all automated checks pass, ready for human visual verification.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
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
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
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
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" if the portal looks and works correctly, or describe specific issues that need fixing.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## Artifacts This Phase Produces
|
||||
|
||||
This plan produces no new code artifacts -- it verifies the artifacts from Plans 01 and 02.
|
||||
|
||||
<verification>
|
||||
All automated and human verification steps complete. The portal shell is confirmed visually correct and functionally working.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Human has verified all 7 checklist areas
|
||||
- Human typed "approved" (or issues have been addressed)
|
||||
- Docker stack remains running and healthy
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/01-foundation-portal-shell/01-03-SUMMARY.md` when done
|
||||
</output>
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user