Initialize eng-pad-server with project documentation

- README.md: project overview, quick start, build commands
- CLAUDE.md: AI dev context, source tree, key conventions
- ARCHITECTURE.md: full system spec covering data model, auth
  (password + FIDO2/U2F), gRPC sync API, REST API, SVG/JPG/PDF
  rendering, web UI, configuration, deployment, security
- PROJECT_PLAN.md: 11 phases with discrete checkboxable steps
- PROGRESS.md: decision log and completion tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 19:42:38 -07:00
commit 0cce04b5b8
5 changed files with 672 additions and 0 deletions

313
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,313 @@
# ARCHITECTURE.md — eng-pad-server
## 1. System Overview
eng-pad-server is a read-only sync and viewing service for eng-pad
engineering notebooks. The Android app is the sole writer; the server
receives complete notebook data via gRPC and serves it through a web UI.
```
Android App eng-pad-server
+-----------+ gRPC/TLS +------------------+
| eng-pad | ───────────────> | Sync Service |
| (writer) | username/pass | (gRPC :9443) |
+-----------+ in metadata +------------------+
+──────────────+
│ SQLite DB │
+──────────────+
┌────────────┼────────────┐
▼ ▼ ▼
REST API Web UI Share Links
(:8443) (:8080) (/s/:token)
JSON htmx/SVG No auth
```
## 2. Data Model
### Users
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| username | TEXT UNIQUE | Login identifier |
| password_hash | TEXT | Argon2id hash |
| created_at | INTEGER | Epoch millis |
| updated_at | INTEGER | Epoch millis |
### WebAuthn Credentials
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| user_id | INTEGER FK | References users(id) CASCADE |
| credential_id | BLOB UNIQUE | WebAuthn credential ID |
| public_key | BLOB | COSE public key |
| name | TEXT | User-assigned label |
| sign_count | INTEGER | Signature counter |
| created_at | INTEGER | Epoch millis |
### Notebooks
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| user_id | INTEGER FK | References users(id) CASCADE |
| remote_id | INTEGER | App-side notebook ID |
| title | TEXT | Notebook title |
| page_size | TEXT | "REGULAR" or "LARGE" |
| synced_at | INTEGER | Last sync epoch millis |
UNIQUE(user_id, remote_id)
### Pages
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| notebook_id | INTEGER FK | References notebooks(id) CASCADE |
| remote_id | INTEGER | App-side page ID |
| page_number | INTEGER | 1-based page number |
UNIQUE(notebook_id, remote_id)
### Strokes
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| page_id | INTEGER FK | References pages(id) CASCADE |
| pen_size | REAL | Width in canonical points (300 DPI) |
| color | INTEGER | ARGB packed int |
| style | TEXT | "plain", "dashed", "arrow", "double_arrow" |
| point_data | BLOB | Packed LE floats: [x0,y0,x1,y1,...] |
| stroke_order | INTEGER | Z-order within page |
### Share Links
| Column | Type | Description |
|--------|------|-------------|
| id | INTEGER PK | Auto-increment |
| notebook_id | INTEGER FK | References notebooks(id) CASCADE |
| token | TEXT UNIQUE | 32-byte random, URL-safe base64 |
| expires_at | INTEGER | Epoch millis, NULL = never |
| created_at | INTEGER | Epoch millis |
## 3. Authentication
### gRPC (Android App Sync)
- Username and password sent in gRPC metadata on every RPC
- Unary interceptor verifies against Argon2id hash
- TLS required — plaintext rejected
- No tokens, no login RPC
### Web UI (Browser)
- `POST /v1/auth/login` — password → bearer token
- Token in `HttpOnly; Secure; SameSite=Strict` cookie
- 24h TTL, SHA-256 keyed lookup with cache
### FIDO2/U2F (Web UI Only)
- Register keys after password login
- Login with key as password alternative
- Multiple keys per user, user-assigned labels
- `go-webauthn/webauthn` library
### Shareable Links
- Token in URL, no auth required
- 32-byte `crypto/rand`, URL-safe base64
- Optional expiry (default: never)
- Expired → 410 Gone
- Revocable via gRPC API or web UI
## 4. gRPC API
Service: `engpad.v1.EngPadSync`
| RPC | Description | Auth |
|-----|-------------|------|
| SyncNotebook | Push complete notebook (upsert) | user/pass |
| DeleteNotebook | Remove notebook from server | user/pass |
| ListNotebooks | List user's synced notebooks | user/pass |
| CreateShareLink | Generate shareable URL | user/pass |
| RevokeShareLink | Invalidate a share link | user/pass |
| ListShareLinks | List links for a notebook | user/pass |
### Sync Semantics
`SyncNotebook` is a full replacement: all pages and strokes for the
notebook are deleted and re-inserted. The server mirrors exactly what
the tablet has. No incremental sync, no conflict resolution.
Keyed by (user_id, remote_id) where remote_id is the app-side
notebook ID.
## 5. REST API
### Auth Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | /v1/auth/login | None | Password login → token |
| POST | /v1/auth/webauthn/register/begin | Bearer | Start key registration |
| POST | /v1/auth/webauthn/register/finish | Bearer | Complete registration |
| POST | /v1/auth/webauthn/login/begin | None | Start key login |
| POST | /v1/auth/webauthn/login/finish | None | Complete key login |
### Notebook Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /v1/notebooks | Bearer | List notebooks |
| GET | /v1/notebooks/:id | Bearer | Notebook + page list |
| GET | /v1/notebooks/:id/pages/:num/svg | Bearer | Page as SVG |
| GET | /v1/notebooks/:id/pages/:num/jpg | Bearer | Page as JPG (300 DPI) |
| GET | /v1/notebooks/:id/pdf | Bearer | Full notebook PDF |
### Share Endpoints
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | /s/:token | None | Notebook view |
| GET | /s/:token/pages/:num/svg | None | Page SVG |
| GET | /s/:token/pages/:num/jpg | None | Page JPG |
| GET | /s/:token/pdf | None | Notebook PDF |
## 6. Rendering
### SVG
Strokes rendered as SVG `<path>` elements. Coordinates scaled from
300 DPI to 72 DPI (×0.24) for standard SVG/PDF point units.
- `stroke-linecap="round"`, `stroke-linejoin="round"`
- Dashed strokes: `stroke-dasharray="7.2 4.8"`
- Arrow heads: separate `<line>` elements
- No grid — grid is a tablet writing aid only
- `viewBox` matches physical page dimensions in points
### JPG
Server-side rasterization at 300 DPI using Go's `image` package.
White background, strokes rendered with the same coordinate system.
### PDF
Generated with a Go PDF library. Coordinates in 72 DPI (native PDF
points). One page per notebook page.
## 7. Web Interface
Built with Go `html/template` + htmx. Embedded via `//go:embed`.
### Pages
| Route | Template | Description |
|-------|----------|-------------|
| /login | login.html | Login form (password + WebAuthn) |
| /notebooks | notebooks.html | Notebook list |
| /notebooks/:id | notebook.html | Page grid with thumbnails |
| /notebooks/:id/pages/:num | page.html | Full page SVG viewer |
| /s/:token | notebook.html | Shared notebook (no auth) |
| /s/:token/pages/:num | page.html | Shared page viewer |
### Security
- CSRF via signed double-submit cookies
- Session cookie: HttpOnly, Secure, SameSite=Strict
- html/template auto-escaping
## 8. Configuration
```toml
[server]
listen_addr = ":8443"
grpc_addr = ":9443"
tls_cert = "/srv/eng-pad-server/certs/cert.pem"
tls_key = "/srv/eng-pad-server/certs/key.pem"
[web]
listen_addr = ":8080"
base_url = "https://pad.metacircular.net"
[database]
path = "/srv/eng-pad-server/eng-pad-server.db"
[auth]
token_ttl = "24h"
argon2_memory = 65536
argon2_time = 3
argon2_threads = 4
[webauthn]
rp_display_name = "Engineering Pad"
rp_id = "pad.metacircular.net"
rp_origins = ["https://pad.metacircular.net"]
[log]
level = "info"
```
## 9. Deployment
### Container
Multi-stage Docker build:
1. Builder: `golang:1.25-alpine`, `CGO_ENABLED=0`, stripped binary
2. Runtime: `alpine:latest`, non-root user
### systemd
| Unit | Purpose |
|------|---------|
| eng-pad-server.service | Main service |
| eng-pad-server-backup.service | Oneshot backup |
| eng-pad-server-backup.timer | Daily 02:00 UTC |
Security hardening: NoNewPrivileges, ProtectSystem=strict,
ReadWritePaths=/srv/eng-pad-server.
### Data Directory
```
/srv/eng-pad-server/
├── eng-pad-server.toml
├── eng-pad-server.db
├── certs/
│ ├── cert.pem
│ └── key.pem
└── backups/
```
## 10. Security
- TLS 1.3 minimum, no fallback
- Argon2id for password hashing
- `crypto/rand` for all tokens and nonces
- `crypto/subtle` for constant-time comparisons
- No secrets in logs
- Default deny: unauthenticated requests rejected
- Share links: scoped to single notebook, optional expiry, revocable
- Graceful shutdown: SIGINT/SIGTERM → drain → close DB → exit
## 11. CLI Commands
| Command | Purpose |
|---------|---------|
| server | Start the service |
| init | Create database, first user |
| snapshot | Database backup (VACUUM INTO) |
| status | Health check |
## 12. Future Work
- MCIAS integration for auth delegation
- Per-page share links (URL structure already supports it)
- Notebook version history (store previous syncs)
- WebSocket notifications for real-time sync status
- Thumbnail generation for notebook list

131
CLAUDE.md Normal file
View File

@@ -0,0 +1,131 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
eng-pad-server is a Go service that receives engineering notebook data from the eng-pad Android app via gRPC, stores it in SQLite, and serves read-only views through a web UI. It supports password + FIDO2/U2F authentication and shareable links.
## Build Commands
```bash
make all # vet → lint → test → build
make eng-pad-server # build binary with version injection
make build # compile all packages
make test # run tests
make vet # go vet
make lint # golangci-lint
make proto # regenerate gRPC code
make proto-lint # buf lint + breaking changes
make clean # remove binary
# Run a single test:
go test -run TestFunctionName ./internal/...
```
## Architecture
- **Go 1.25+**, pure-Go dependencies, `CGO_ENABLED=0`
- **gRPC API**: receives notebook sync from the Android app (password auth per-request over TLS)
- **REST API**: JSON over HTTPS for web viewing, auth endpoints
- **Web UI**: Go `html/template` + htmx, SVG page rendering
- **SQLite**: via `modernc.org/sqlite`, WAL mode, foreign keys
- **Auth**: Argon2id passwords + FIDO2/U2F via `go-webauthn/webauthn`
- **Router**: chi (lightweight, stdlib-compatible)
## Project Documents
- **ARCHITECTURE.md** — full system specification
- **PROJECT_PLAN.md** — implementation steps with checkboxes
- **PROGRESS.md** — completion tracking and decisions
**Keep PROJECT_PLAN.md and PROGRESS.md in sync.**
## Source Tree
```
eng-pad-server/
├── cmd/
│ └── eng-pad-server/ CLI entry point (cobra)
│ ├── main.go
│ ├── server.go server subcommand
│ └── init.go init subcommand
├── internal/
│ ├── auth/
│ │ ├── argon2.go Password hashing
│ │ ├── tokens.go Bearer token generation/validation
│ │ └── webauthn.go FIDO2/U2F integration
│ ├── config/
│ │ └── config.go TOML configuration
│ ├── db/
│ │ ├── db.go Database setup, pragmas
│ │ └── migrations.go Schema migrations
│ ├── grpcserver/
│ │ ├── server.go gRPC server setup
│ │ ├── sync.go SyncNotebook handler
│ │ ├── share.go Share link RPCs
│ │ └── interceptors.go Auth interceptor
│ ├── server/
│ │ ├── server.go REST server setup
│ │ ├── routes.go Route registration
│ │ ├── auth.go Login/register endpoints
│ │ ├── notebooks.go Notebook/page endpoints
│ │ └── middleware.go Auth middleware
│ ├── render/
│ │ ├── svg.go Page → SVG rendering
│ │ ├── jpg.go Page → JPG rendering
│ │ └── pdf.go Notebook → PDF rendering
│ ├── share/
│ │ └── share.go Share link token management
│ └── webserver/
│ ├── server.go Web UI server setup
│ ├── routes.go Template routes
│ └── handlers.go htmx handlers
├── proto/engpad/
│ └── v1/
│ └── sync.proto gRPC service definition
├── gen/engpad/
│ └── v1/ Generated Go gRPC code
├── web/
│ ├── embed.go //go:embed directive
│ ├── templates/
│ │ ├── layout.html Shared HTML skeleton
│ │ ├── login.html Login page
│ │ ├── notebooks.html Notebook list
│ │ ├── notebook.html Single notebook view
│ │ └── page.html Page viewer (SVG embed)
│ └── static/
│ └── htmx.min.js
├── deploy/
│ ├── docker/
│ │ └── docker-compose.yml
│ ├── systemd/
│ │ ├── eng-pad-server.service
│ │ └── eng-pad-server-backup.timer
│ ├── scripts/
│ │ └── install.sh
│ └── examples/
│ └── eng-pad-server.toml
├── Dockerfile
├── Makefile
├── buf.yaml
├── .golangci.yaml
├── .gitignore
├── CLAUDE.md This file
├── README.md
├── ARCHITECTURE.md Full system spec
├── PROJECT_PLAN.md Implementation steps
└── PROGRESS.md Completion tracking
```
## Key Conventions
- Stroke point data: packed little-endian floats `[x0,y0,x1,y1,...]` — same binary format as the Android app
- Canonical coordinates: 300 DPI. Scaled to 72 DPI for SVG/PDF output (×0.24)
- Page sizes: REGULAR (2550×3300 pts), LARGE (3300×5100 pts)
- Stroke styles: "plain", "dashed", "arrow", "double_arrow"
- No grid rendering — grid is a tablet writing aid only
- Share link tokens: 32-byte `crypto/rand`, URL-safe base64
- gRPC auth: username+password in metadata, verified per-request
- Web auth: password login → bearer token in session cookie
- TLS 1.3 minimum, no exceptions

27
PROGRESS.md Normal file
View File

@@ -0,0 +1,27 @@
# PROGRESS.md — eng-pad-server Implementation Progress
This file tracks completed work and decisions. Updated after every step.
See PROJECT_PLAN.md for the full step list.
## Completed
_(none yet)_
## In Progress
Phase 0: Project Setup
## Decisions
- **Language**: Go (Metacircular standard)
- **Database**: SQLite via `modernc.org/sqlite` (pure Go, no CGo)
- **Auth**: Argon2id passwords + FIDO2/U2F via `go-webauthn/webauthn`
- **gRPC auth**: username/password in metadata per-request (no tokens)
- **Web auth**: password → bearer token in session cookie
- **Rendering**: SVG for web viewing, JPG/PDF for export
- **Sync model**: full notebook replacement (upsert), no incremental sync
- **Share links**: 32-byte random token, optional expiry, scoped to notebook
- **Grid**: not rendered server-side (tablet writing aid only)
- **Coordinate system**: 300 DPI canonical, scaled to 72 DPI for SVG/PDF
- **FIDO2/U2F**: web UI login only, not gRPC sync
- **Server is read-only**: mirrors tablet exactly, no content modification

148
PROJECT_PLAN.md Normal file
View File

@@ -0,0 +1,148 @@
# PROJECT_PLAN.md — eng-pad-server Implementation Steps
This file tracks all implementation steps. Check off steps as they are
completed and log them in PROGRESS.md.
## Phase 0: Project Setup
- [ ] 0.1: Initialize Go module (`git.wntrmute.dev/kyle/eng-pad-server`)
- [ ] 0.2: Create Makefile with standard targets
- [ ] 0.3: Configure `.golangci.yaml`
- [ ] 0.4: Create `.gitignore`
- [ ] 0.5: Create example config `deploy/examples/eng-pad-server.toml`
- **Verify:** `make build`
## Phase 1: Database + Config
- [ ] 1.1: TOML config loading
- `internal/config/config.go`
- [ ] 1.2: SQLite database setup (WAL, foreign keys, busy timeout)
- `internal/db/db.go`
- [ ] 1.3: Schema migrations (users, notebooks, pages, strokes, share_links, webauthn_credentials)
- `internal/db/migrations.go`
- [ ] 1.4: Unit tests for migrations
- **Verify:** `make test`
## Phase 2: Auth — Password
- [ ] 2.1: Argon2id password hashing + verification
- `internal/auth/argon2.go`
- [ ] 2.2: Bearer token generation, storage, validation
- `internal/auth/tokens.go`
- [ ] 2.3: User creation (for `init` command)
- [ ] 2.4: Unit tests for auth
- **Verify:** `make test`
## Phase 3: CLI
- [ ] 3.1: Cobra CLI scaffold
- `cmd/eng-pad-server/main.go`
- [ ] 3.2: `init` command — create DB, prompt for admin user
- `cmd/eng-pad-server/init.go`
- [ ] 3.3: `server` command — start gRPC + REST + web servers
- `cmd/eng-pad-server/server.go`
- [ ] 3.4: `snapshot` command — VACUUM INTO backup
- [ ] 3.5: `status` command — health check
- **Verify:** `make all && ./eng-pad-server init`
## Phase 4: gRPC Sync Service
- [ ] 4.1: Proto definitions
- `proto/engpad/v1/sync.proto`
- [ ] 4.2: Generate Go code
- `make proto`
- [ ] 4.3: gRPC server setup with TLS
- `internal/grpcserver/server.go`
- [ ] 4.4: Auth interceptor (username/password from metadata)
- `internal/grpcserver/interceptors.go`
- [ ] 4.5: SyncNotebook handler (upsert: delete + re-insert)
- `internal/grpcserver/sync.go`
- [ ] 4.6: DeleteNotebook handler
- [ ] 4.7: ListNotebooks handler
- [ ] 4.8: Unit tests for sync
- **Verify:** `make test` + manual gRPC test with `grpcurl`
## Phase 5: Rendering
- [ ] 5.1: SVG rendering — strokes to SVG path elements
- `internal/render/svg.go`
- [ ] 5.2: JPG rendering — rasterize page at 300 DPI
- `internal/render/jpg.go`
- [ ] 5.3: PDF rendering — notebook to multi-page PDF
- `internal/render/pdf.go`
- [ ] 5.4: Unit tests — verify SVG output, JPG dimensions, PDF page count
- **Verify:** `make test`
## Phase 6: REST API
- [ ] 6.1: chi router setup with TLS
- `internal/server/server.go`, `routes.go`
- [ ] 6.2: Auth middleware (bearer token validation)
- `internal/server/middleware.go`
- [ ] 6.3: Login endpoint
- `internal/server/auth.go`
- [ ] 6.4: Notebook/page endpoints (JSON metadata)
- `internal/server/notebooks.go`
- [ ] 6.5: Rendering endpoints (SVG, JPG, PDF)
- [ ] 6.6: Unit tests for API
- **Verify:** `make test` + manual curl
## Phase 7: Share Links
- [ ] 7.1: Token generation + storage
- `internal/share/share.go`
- [ ] 7.2: gRPC RPCs — CreateShareLink, RevokeShareLink, ListShareLinks
- `internal/grpcserver/share.go`
- [ ] 7.3: REST endpoints — /s/:token routes
- [ ] 7.4: Expiry enforcement (check on access, periodic cleanup)
- [ ] 7.5: Unit tests
- **Verify:** `make test`
## Phase 8: Web UI
- [ ] 8.1: Template skeleton — layout.html, navigation
- `web/templates/layout.html`
- [ ] 8.2: Login page (password + WebAuthn)
- `web/templates/login.html`
- [ ] 8.3: Notebook list page
- `web/templates/notebooks.html`
- [ ] 8.4: Notebook view page (page grid with SVG thumbnails)
- `web/templates/notebook.html`
- [ ] 8.5: Page viewer (embedded SVG, export buttons)
- `web/templates/page.html`
- [ ] 8.6: Shared notebook/page views (same templates, no auth chrome)
- [ ] 8.7: Web server setup + embed
- `internal/webserver/`, `web/embed.go`
- **Verify:** manual browser test
## Phase 9: FIDO2/U2F (WebAuthn)
- [ ] 9.1: WebAuthn integration with `go-webauthn/webauthn`
- `internal/auth/webauthn.go`
- [ ] 9.2: Registration endpoints (begin/finish)
- [ ] 9.3: Login endpoints (begin/finish)
- [ ] 9.4: Key management UI (list keys, add key, remove key)
- [ ] 9.5: Unit tests
- **Verify:** manual test with security key
## Phase 10: Deployment
- [ ] 10.1: Dockerfile (multi-stage, non-root)
- [ ] 10.2: systemd units (service, backup timer)
- `deploy/systemd/`
- [ ] 10.3: Install script
- `deploy/scripts/install.sh`
- [ ] 10.4: Graceful shutdown (SIGINT/SIGTERM)
- **Verify:** `make docker && docker run`
## Phase 11: Android App Sync Integration
_(Implemented in the eng-pad repo, not here)_
- [ ] 11.1: gRPC client dependency (protobuf-lite)
- [ ] 11.2: SyncClient.kt — gRPC channel + stub
- [ ] 11.3: SyncManager.kt — serialize notebook to proto, call sync
- [ ] 11.4: Sync settings screen (server URL, username, password)
- [ ] 11.5: Notebook overflow menu — "Sync to server"
- [ ] 11.6: Library — "Sync all" button
- [ ] 11.7: Sync status indicator on notebook cards

53
README.md Normal file
View File

@@ -0,0 +1,53 @@
eng-pad-server
==============
Read-only sync and web viewer for [eng-pad](https://git.wntrmute.dev/kyle/eng-pad)
engineering notebooks.
The Android app pushes complete notebooks to this server via gRPC. The
server stores them and serves read-only views through a web UI with
SVG rendering. Shareable links allow unauthenticated access to specific
notebooks.
## Features
- **gRPC sync**: receive notebook data from the Android app over TLS
- **Web viewer**: browse notebooks, view pages as SVG, export JPG/PDF
- **Authentication**: password (Argon2id) + FIDO2/U2F security keys
- **Shareable links**: token-based URLs with optional expiry
## Quick Start
```bash
# Build
make eng-pad-server
# Generate example config
cp eng-pad-server.toml.example /srv/eng-pad-server/eng-pad-server.toml
# Edit configuration (TLS certs, database path, etc.)
# Initialize (creates database, prompts for admin user)
./eng-pad-server init
# Run
./eng-pad-server server
```
## Build
```bash
make all # vet → lint → test → build
make test # run tests
make lint # golangci-lint
make proto # regenerate gRPC code from .proto files
make proto-lint # buf lint + breaking change detection
```
## Documentation
- [ARCHITECTURE.md](ARCHITECTURE.md) — full system specification
- [CLAUDE.md](CLAUDE.md) — AI development context
## License
Private. All rights reserved.