docs: add container operations and project guide
This commit is contained in:
parent
44c7cdbf1f
commit
352ffe0bf2
9 changed files with 245 additions and 0 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
node_modules
|
||||||
|
**/dist
|
||||||
|
.git
|
||||||
|
data
|
||||||
|
backups
|
||||||
|
coverage
|
||||||
|
.env*
|
||||||
|
test-results
|
||||||
|
playwright-report
|
||||||
35
Dockerfile
Normal file
35
Dockerfile
Normal 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
65
README.md
Normal 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
14
deploy/nginx.conf
Normal 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
50
docker-compose.yml
Normal 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
32
docs/architecture.md
Normal 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
20
docs/operations.md
Normal 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
10
scripts/backup.ps1
Normal 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
10
scripts/restore.ps1
Normal 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"
|
||||||
Loading…
Add table
Reference in a new issue