docs: add container operations and project guide

This commit is contained in:
Christian Johannesen 2026-10-18 16:05:50 +02:00
parent 44c7cdbf1f
commit 352ffe0bf2
9 changed files with 245 additions and 0 deletions

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
node_modules
**/dist
.git
data
backups
coverage
.env*
test-results
playwright-report

35
Dockerfile Normal file
View file

@ -0,0 +1,35 @@
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package*.json tsconfig.base.json ./
COPY apps/api/package.json apps/api/package.json
COPY apps/web/package.json apps/web/package.json
COPY apps/worker/package.json apps/worker/package.json
COPY packages/contracts/package.json packages/contracts/package.json
COPY packages/database/package.json packages/database/package.json
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-bookworm-slim AS api
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/api/dist ./apps/api/dist
COPY --from=build /app/apps/api/package.json ./apps/api/package.json
COPY --from=build /app/packages/contracts/dist ./packages/contracts/dist
COPY --from=build /app/packages/contracts/package.json ./packages/contracts/package.json
COPY --from=build /app/packages/database/dist ./packages/database/dist
COPY --from=build /app/packages/database/package.json ./packages/database/package.json
USER node
CMD ["node", "apps/api/dist/server.js"]
FROM api AS worker
COPY --from=build /app/apps/worker/dist ./apps/worker/dist
COPY --from=build /app/apps/worker/package.json ./apps/worker/package.json
CMD ["node", "apps/worker/dist/index.js"]
FROM nginx:1.29-alpine AS web
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/apps/web/dist /usr/share/nginx/html
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost/healthz || exit 1

65
README.md Normal file
View file

@ -0,0 +1,65 @@
# Tech Library
Tech Library is a modular, source-backed reference for modern web, app, and systems development. The Danish PWA serves layered English technical entries for readers at every experience level. Official-source monitoring proposes changes; a human editor always reviews them before publication.
## Included
- 32 curated starter topics across web, frontend, backend, data, mobile/desktop, DevOps, testing/observability, and security
- faceted browsing and SQLite FTS5 search
- immutable revisions, publication pointers, rollback, and claim-level provenance
- integrated editorial workspace with secure session and CSRF protection
- idempotent monitoring of official sources with a daily scheduler and manual trigger
- installable React/Vite PWA, Fastify API, SQLite/Drizzle data layer, and independent worker
- Docker Compose deployment, health checks, persistent data, backup and restore guidance
## Quick start
Requirements: Node.js 22+ and npm 10+.
```powershell
npm install
npm run db:migrate
npm run db:seed
$env:ADMIN_PASSWORD_HASH = npm run --silent password:hash -w @tech-library/api -- "development password"
$env:SESSION_SECRET = "replace-this-with-a-long-random-development-secret"
npm run dev
```
Open `http://localhost:5173`. The API runs on port 3001. Local content is stored under `data/` and is intentionally ignored by Git.
## Commands
| Command | Purpose |
| --- | --- |
| `npm run dev` | Run web, API, and source worker |
| `npm run check` | Type-check, test, and build every workspace |
| `npm run db:migrate` | Apply idempotent SQLite migrations |
| `npm run db:seed` | Add the curated starter catalog |
| `docker compose up -d --build` | Build and run the production topology |
## Repository structure
```text
apps/web React, Vite, Radix UI, TanStack Query, Zustand, PWA
apps/api Fastify REST API, editorial policy, authentication
apps/worker Official-source monitoring and structured findings
packages/contracts Shared Zod schemas and public TypeScript types
packages/database Drizzle schema, migrations, seed catalog
deploy Reverse-proxy configuration
docs Architecture decisions and operations
scripts Backup and restore helpers
```
See [architecture](docs/architecture.md) and [operations](docs/operations.md) for system boundaries and production guidance.
## Editorial flow
1. The worker fetches enabled official sources and normalizes volatile page content.
2. A new fingerprint produces one deduplicated finding; failures only update source health.
3. An administrator reviews the source and creates an immutable draft revision.
4. A revision must be approved before it can be published.
5. Publication updates the active pointer and search index in one transaction; rollback selects a prior approved revision.
## Development policy
Use short feature branches and focused Conventional Commits. Tests and documentation belong in the same change as the behavior they describe. Comments should explain decisions or non-obvious constraints rather than restating code.

14
deploy/nginx.conf Normal file
View file

@ -0,0 +1,14 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
location = /healthz { access_log off; return 200 "ok\n"; }
location /api/ { proxy_pass http://api:3001; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; }
location / { try_files $uri $uri/ /index.html; }
}

50
docker-compose.yml Normal file
View file

@ -0,0 +1,50 @@
services:
web:
build:
context: .
target: web
ports:
- "8080:80"
depends_on:
api:
condition: service_healthy
restart: unless-stopped
api:
build:
context: .
target: api
environment:
NODE_ENV: production
PORT: 3001
DATABASE_URL: /data/tech-library.db
WEB_ORIGIN: ${WEB_ORIGIN:-http://localhost:8080}
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
ADMIN_PASSWORD_HASH: ${ADMIN_PASSWORD_HASH:?Set ADMIN_PASSWORD_HASH}
SESSION_SECRET: ${SESSION_SECRET:?Set SESSION_SECRET}
volumes:
- tech-library-data:/data
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3001/health').then(r=>{if(!r.ok)process.exit(1)})"]
interval: 15s
timeout: 5s
retries: 5
restart: unless-stopped
worker:
build:
context: .
target: worker
environment:
NODE_ENV: production
DATABASE_URL: /data/tech-library.db
SOURCE_POLL_INTERVAL_HOURS: ${SOURCE_POLL_INTERVAL_HOURS:-24}
volumes:
- tech-library-data:/data
depends_on:
api:
condition: service_healthy
restart: unless-stopped
volumes:
tech-library-data:

32
docs/architecture.md Normal file
View file

@ -0,0 +1,32 @@
# Architecture
Tech Library is split into deployable applications and reusable packages so content, presentation, automation, and infrastructure can evolve independently.
```text
React/Vite PWA ──REST──> Fastify API ──> SQLite
▲ ▲
│ │
Admin review Source worker
```
## Boundaries
- `apps/web` owns Danish presentation, routing, query caching, and local UI state.
- `apps/api` owns authentication, authorization, publication rules, and versioned REST resources.
- `apps/worker` owns official-source collection, normalization, fingerprinting, and findings.
- `packages/contracts` owns runtime validation and shared public types.
- `packages/database` owns schema, migrations, connection policy, and curated seed data.
Published revisions are immutable. `topics.active_revision_id` is the only publication pointer, so rollback does not rewrite history. Findings and draft revisions never become public merely because a worker produced them.
## ADR-001: SQLite and one writer host
SQLite is authoritative for v1 and runs on a persistent local volume. WAL mode supports concurrent readers while API and worker serialize short writes. Do not place the database on a network filesystem. Database access stays behind the database package and repository layer to preserve a future migration path.
## ADR-002: Dynamic content delivery
The PWA contains no production catalog bundle. Public content is fetched from the API, allowing editors to publish a revision without rebuilding the frontend. The service worker caches only the application shell and static assets.
## ADR-003: Human publication gate
Automated adapters may create source findings, never published revisions. A signed admin session and matching CSRF token are required for every mutating editorial request.

20
docs/operations.md Normal file
View file

@ -0,0 +1,20 @@
# Operations
## Deployment
1. Generate an Argon2id hash with `npm run password:hash -w @tech-library/api -- "a strong password"`.
2. Copy `.env.example` to `.env`, set the hash, a random 32+ character session secret, and the public `WEB_ORIGIN`.
3. Run `docker compose up -d --build`.
4. Verify `http://host:8080/healthz` and the API health check through Docker Compose.
Terminate TLS at a trusted reverse proxy and forward HTTPS to port 8080. Restrict filesystem access to the Docker volume and secrets file.
## Backup and restore
Run `scripts/backup.ps1` against a stopped instance or a host-installed `sqlite3` client. The command uses SQLite's online backup operation. Retain encrypted off-host copies and test restoration regularly.
Before restoration, stop API and worker, validate the target path, run `scripts/restore.ps1 -Backup <file>`, and restart the services. Confirm `/health` and a known topic afterward.
## Monitoring
All services expose structured container logs. Alert on API health failure, worker `source_poll.failed` events, a growing pending-findings queue, backup failure, and low volume capacity. Source fetch errors remain attached to subscriptions and never change published content.

10
scripts/backup.ps1 Normal file
View file

@ -0,0 +1,10 @@
param(
[string]$Database = ".\data\tech-library.db",
[string]$Destination = ".\backups"
)
$ErrorActionPreference = "Stop"
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$target = Join-Path $Destination "tech-library-$timestamp.db"
sqlite3 $Database ".backup '$target'"
Write-Host "Backup created: $target"

10
scripts/restore.ps1 Normal file
View file

@ -0,0 +1,10 @@
param(
[Parameter(Mandatory = $true)][string]$Backup,
[string]$Database = ".\data\tech-library.db"
)
$ErrorActionPreference = "Stop"
if (-not (Test-Path -LiteralPath $Backup)) { throw "Backup not found: $Backup" }
$parent = Split-Path -Parent $Database
if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null }
Copy-Item -LiteralPath $Backup -Destination $Database -Force
Write-Host "Database restored to: $Database"