feat: implement Havenllo single-board kanban (Go+SQLite backend, Preact/Vite frontend)
- Single fixed 'Haven' board: lists (To do/In progress/Done) + cards, no multi-board - Go 1.24 backend, CGO-free modernc.org/sqlite, embedded SPA, REST /api - Preact + Vite + TS frontend, native HTML5 drag-and-drop, optimistic UI, dark Haven theme - PVC (nfs-client) for SQLite, root container on NFS, simple nginx ingress havenllo.haven - Dockerfile multi-arch build, Gitea CI via pipeline-actions@main
This commit is contained in:
540
PLAN.md
Normal file
540
PLAN.md
Normal file
@@ -0,0 +1,540 @@
|
||||
# Havenllo implementation plan
|
||||
|
||||
## 1. Overview and goals
|
||||
|
||||
Havenllo will be a single-user kanban application for the Haven homelab. It will
|
||||
provide multiple boards, ordered lists, and ordered task cards without accounts,
|
||||
authentication, or external services. One Go HTTP server will expose a
|
||||
same-origin JSON API and serve the compiled Preact SPA from one container. Its
|
||||
only durable state will be a SQLite database stored on a 1Gi PVC.
|
||||
|
||||
The implementation will favour a polished day-to-day board: fast board loading,
|
||||
native card drag-and-drop, useful empty/loading states, keyboard-friendly inline
|
||||
editing, and a responsive horizontally scrolling kanban layout. It will not add
|
||||
an ORM, WebSockets, a separate frontend server, or a separate database service.
|
||||
|
||||
## 2. Proposed repository tree
|
||||
|
||||
```text
|
||||
.
|
||||
├── .dockerignore # Excludes Git data, node_modules, local DBs, and build artefacts from Docker context.
|
||||
├── .gitattributes # Enforces LF for Docker/YAML/workflow files edited from Windows.
|
||||
├── .gitea/
|
||||
│ └── workflows/
|
||||
│ └── main.yaml # Gitea Actions: verify, multi-arch build/push, apply, and restart.
|
||||
├── .gitignore # Ignores database, binaries, coverage, node_modules, and web/dist output.
|
||||
├── Dockerfile # Three-stage Node + Go + distroless build for the one production image.
|
||||
├── README.md # Local development, test, build, and Kubernetes deployment instructions.
|
||||
├── go.mod # Module definition; direct dependency only on modernc.org/sqlite.
|
||||
├── go.sum # Checksums generated by go mod tidy and committed.
|
||||
├── cmd/
|
||||
│ └── havenllo/
|
||||
│ └── main.go # Configuration, database setup, and HTTP server lifecycle.
|
||||
├── internal/
|
||||
│ ├── api/
|
||||
│ │ ├── handlers.go # Go 1.22 ServeMux routes and endpoint handlers.
|
||||
│ │ ├── http.go # JSON, errors, ID parsing, and validation helpers.
|
||||
│ │ ├── types.go # API DTOs and domain-to-response conversion.
|
||||
│ │ └── handlers_test.go # HTTP contract tests against temporary SQLite.
|
||||
│ ├── app/
|
||||
│ │ ├── service.go # Transactional validation, ordering, moves, and board seeding.
|
||||
│ │ └── service_test.go # Ordering, move, delete, and default-board tests.
|
||||
│ ├── database/
|
||||
│ │ ├── database.go # SQLite options, migrations, and initial seed data.
|
||||
│ │ ├── migrations.go # Versioned schema migrations.
|
||||
│ │ └── database_test.go # Migration/idempotency and foreign-key tests.
|
||||
│ ├── domain/
|
||||
│ │ └── models.go # Board, List, Card, snapshot, and domain errors.
|
||||
│ └── store/
|
||||
│ ├── sqlite.go # Parameterized SQL repository methods; no ORM.
|
||||
│ └── sqlite_test.go # Persistence and ordering-query tests.
|
||||
├── web/
|
||||
│ ├── embed.go # Embeds dist/ into the Go binary and exposes an fs.FS.
|
||||
│ ├── index.html # Vite HTML shell, title, theme-color, and root element.
|
||||
│ ├── package.json # Preact runtime; Vite/TS/Preact preset development tools.
|
||||
│ ├── package-lock.json # Generated locked npm dependency graph, committed.
|
||||
│ ├── tsconfig.json # Strict TypeScript configuration.
|
||||
│ ├── vite.config.ts # Preact plugin and deterministic web/dist output.
|
||||
│ ├── dist/
|
||||
│ │ └── .gitkeep # Placeholder for pre-build Go embedding; replaced in image builds.
|
||||
│ └── src/
|
||||
│ ├── main.tsx # Preact bootstrap and global error boundary.
|
||||
│ ├── App.tsx # Board selection, fetch, optimistic state hook, and shell.
|
||||
│ ├── api.ts # Typed fetch client, endpoint methods, and API error parsing.
|
||||
│ ├── types.ts # TypeScript representations of resources and errors.
|
||||
│ ├── board-state.ts # Reducer/selectors, pending mutations, rollback, and toast state.
|
||||
│ ├── drag.ts # Native drag payload, drop-index, and position helpers.
|
||||
│ ├── styles.css # Design tokens and responsive component styling.
|
||||
│ └── components/
|
||||
│ ├── Header.tsx # Wordmark, board picker, create board, and status indicator.
|
||||
│ ├── BoardCanvas.tsx # List rail, empty state, and add-list affordance.
|
||||
│ ├── ListColumn.tsx # List editing, count, drop zones, and inline card creation.
|
||||
│ ├── CardItem.tsx # Draggable card, inline title edit, done control, actions.
|
||||
│ ├── CardEditor.tsx # Description editor and destructive card action.
|
||||
│ ├── ConfirmDialog.tsx # Accessible confirmation dialog for deletes.
|
||||
│ └── Toast.tsx # Non-blocking mutation feedback.
|
||||
└── deploy/
|
||||
└── havenllo.yaml # PVC, Deployment, Service, and Ingress multi-document manifest.
|
||||
```
|
||||
|
||||
No application source files will be created during this planning step. The
|
||||
`web/dist` directory is ignored except for its tracked placeholder; the Docker
|
||||
build replaces it with Vite output before Go evaluates the embed directive.
|
||||
|
||||
## 3. Data model and SQLite schema
|
||||
|
||||
IDs are SQLite `INTEGER PRIMARY KEY` values. `position` is a floating-point
|
||||
sort key; records are ordered by `(position, id)`. A new sibling receives the
|
||||
current maximum plus `1024`; a drag chooses a value between its new neighbours.
|
||||
The service renumbers a sibling group to `1024` increments inside the same
|
||||
transaction whenever positions become too close or invalid.
|
||||
|
||||
Timestamps are UTC RFC 3339 strings written by Go. SQLite foreign keys are
|
||||
enabled for every connection, and migrations run transactionally before the
|
||||
server accepts traffic.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS boards (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL CHECK (length(trim(name)) BETWEEN 1 AND 120),
|
||||
created_at TEXT NOT NULL,
|
||||
position REAL NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lists (
|
||||
id INTEGER PRIMARY KEY,
|
||||
board_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL CHECK (length(trim(name)) BETWEEN 1 AND 120),
|
||||
position REAL NOT NULL,
|
||||
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cards (
|
||||
id INTEGER PRIMARY KEY,
|
||||
list_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL CHECK (length(trim(title)) BETWEEN 1 AND 240),
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
position REAL NOT NULL,
|
||||
done INTEGER NOT NULL DEFAULT 0 CHECK (done IN (0, 1)),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (list_id) REFERENCES lists(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_boards_position ON boards(position, id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lists_board_position ON lists(board_id, position, id);
|
||||
CREATE INDEX IF NOT EXISTS idx_cards_list_position ON cards(list_id, position, id);
|
||||
```
|
||||
|
||||
Migration 1 atomically seeds a `Haven` board with `To do`, `In progress`, and
|
||||
`Done` lists. Creating later boards seeds the same three lists. Deleting a board
|
||||
cascades its lists and cards, but the API rejects deletion of the final board
|
||||
with `409 Conflict` so the app remains usable.
|
||||
|
||||
The database comes from `HAVENLLO_DATABASE_PATH` (default `/data/havenllo.db`).
|
||||
Connection setup uses WAL, a 5-second busy timeout, `foreign_keys=ON`, and one
|
||||
open connection. This single-replica deployment intentionally keeps SQLite a
|
||||
safe, simple single-writer database on the NFS-backed PVC.
|
||||
|
||||
## 4. REST API contract
|
||||
|
||||
All endpoints are same-origin under `/api`, accept and return JSON, and use
|
||||
integer resource IDs. Mutation requests require `Content-Type: application/json`.
|
||||
Unknown fields are rejected; names and titles are trimmed; descriptions allow up
|
||||
to 10,000 characters. PATCH DTOs use pointer fields so `done: false` is distinct
|
||||
from an omitted field.
|
||||
|
||||
Every error has this shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "validation_error",
|
||||
"message": "title is required"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Success statuses are `200`, `201`, and `204`. The API returns `400` for malformed
|
||||
JSON/IDs, `404` for absent records, `409` for final-board deletion, `415` for an
|
||||
incorrect media type, and `422` for business validation. Unexpected database
|
||||
errors are logged server-side and produce a non-sensitive `500` response.
|
||||
|
||||
### Resource shapes
|
||||
|
||||
A board is `{ "id": 1, "name": "Haven", "position": 1024,
|
||||
"created_at": "2026-07-14T12:00:00Z" }`.
|
||||
|
||||
A list is `{ "id": 10, "board_id": 1, "name": "To do", "position": 1024,
|
||||
"card_count": 2 }`. `card_count` is included only in list collection responses.
|
||||
|
||||
A card is:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 101,
|
||||
"list_id": 10,
|
||||
"title": "Back up the router config",
|
||||
"description": "Export it after the firmware update.",
|
||||
"position": 1024,
|
||||
"done": false,
|
||||
"created_at": "2026-07-14T12:00:00Z",
|
||||
"updated_at": "2026-07-14T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
The primary board-load response is a snapshot:
|
||||
|
||||
```json
|
||||
{
|
||||
"board": { "id": 1, "name": "Haven", "position": 1024, "created_at": "2026-07-14T12:00:00Z" },
|
||||
"lists": [
|
||||
{ "id": 10, "board_id": 1, "name": "To do", "position": 1024, "cards": [] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Health
|
||||
|
||||
| Method | Path | Behaviour and response |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/health` | Pings SQLite and returns `200 {"status":"ok"}`; used by Kubernetes probes. |
|
||||
|
||||
### Boards
|
||||
|
||||
| Method | Path | Request body | Success response |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/boards` | — | `200 {"boards":[Board...]}`, ordered by position. |
|
||||
| `POST` | `/api/boards` | `{"name":"Homelab"}` | `201` BoardSnapshot with three starter lists. |
|
||||
| `GET` | `/api/boards/{boardID}` | — | `200` BoardSnapshot with lists and cards in render order. |
|
||||
| `PATCH` | `/api/boards/{boardID}` | `{"name":"…","position":2048}`; either field optional | `200 {"board":Board}`. |
|
||||
| `DELETE` | `/api/boards/{boardID}` | — | `204`, unless it is the final board (`409`). |
|
||||
|
||||
### Lists
|
||||
|
||||
| Method | Path | Request body | Success response |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/boards/{boardID}/lists` | — | `200 {"lists":[List...]}`, with card counts but not card bodies. |
|
||||
| `POST` | `/api/boards/{boardID}/lists` | `{"name":"Waiting","position":4096}`; position optional | `201 {"list":List}`. |
|
||||
| `GET` | `/api/lists/{listID}` | — | `200 {"list":List,"cards":[Card...]}`. |
|
||||
| `PATCH` | `/api/lists/{listID}` | `{"name":"…","board_id":2,"position":1024}`; all optional | `200 {"list":List}`. |
|
||||
| `DELETE` | `/api/lists/{listID}` | — | `204`; its cards are cascade-deleted after UI confirmation. |
|
||||
|
||||
### Cards
|
||||
|
||||
| Method | Path | Request body | Success response |
|
||||
| --- | --- | --- | --- |
|
||||
| `GET` | `/api/lists/{listID}/cards` | — | `200 {"cards":[Card...]}`, ordered by position. |
|
||||
| `POST` | `/api/lists/{listID}/cards` | `{"title":"Replace UPS battery","description":"…","position":4096}`; description/position optional | `201 {"card":Card}`. |
|
||||
| `GET` | `/api/cards/{cardID}` | — | `200 {"card":Card}`. |
|
||||
| `PATCH` | `/api/cards/{cardID}` | `{"title":"…","description":"…","done":true,"list_id":11,"position":1536}`; all optional | `200 {"card":Card}`. |
|
||||
| `DELETE` | `/api/cards/{cardID}` | — | `204`. |
|
||||
|
||||
The browser calculates target position from adjacent cards at the drop location.
|
||||
The service independently validates it, normalizes sibling positions when
|
||||
needed, and returns the authoritative resource. A card list move and position
|
||||
update happen in one transaction.
|
||||
|
||||
## 5. Frontend architecture and interaction design
|
||||
|
||||
The frontend uses Preact, TypeScript, and Vite with only Preact and the Preact
|
||||
Vite preset as dependencies. It uses no component, state, icon, or drag library.
|
||||
`web/embed.go` exposes the completed Vite output as an embedded `fs.FS`, so the
|
||||
SPA and API are always same-origin and need no CORS configuration.
|
||||
|
||||
`App` loads boards, restores the selected board from `localStorage` when it is
|
||||
still valid (otherwise picks the first), and fetches one BoardSnapshot. Its
|
||||
`useReducer`-based board state owns the snapshot, selection, pending mutation
|
||||
IDs, rollback snapshots, and toast/error state. Every create/edit/move action
|
||||
first performs an immutable optimistic update, calls `api.ts`, reconciles with
|
||||
the canonical response, and restores state with an error toast if it fails.
|
||||
Controls disable only for their pending resource, rather than freezing the board.
|
||||
|
||||
`Header` contains the Havenllo wordmark, board picker, create-board action, and
|
||||
small connection/status feedback. `BoardCanvas` maps lists to columns and ends
|
||||
with an inline Add list affordance. `ListColumn` provides list-name inline edit,
|
||||
card count, card drop zones, and inline add-card input. `CardItem` provides
|
||||
click-to-edit title, a done control, and a small actions control; `CardEditor`
|
||||
edits descriptions and hosts destructive actions. `ConfirmDialog` is required
|
||||
before deleting a card, list, or board, and `Toast` gives non-blocking feedback.
|
||||
|
||||
Cards use native HTML5 drag-and-drop. The drag payload holds card and source-list
|
||||
IDs. Drop zones before every card plus one at the end provide an unambiguous
|
||||
target index. Drag-over uses an accent outline and local preview; drop calculates
|
||||
the new fractional position, updates optimistically, and sends one PATCH. A
|
||||
failed call restores the pre-drag snapshot. The usual add/edit/done controls stay
|
||||
fully usable on phones, while the board uses a horizontally scrolling rail,
|
||||
280px minimum columns, and 44px touch targets; this remains useful on mobile
|
||||
browsers with limited native DnD support.
|
||||
|
||||
Plain CSS in `styles.css` will use variables and deliberately named component
|
||||
classes. The visual system is deep slate/navy with restrained teal/indigo accents,
|
||||
12–16px rounded surfaces, subtle borders, layered soft shadows, system font
|
||||
stacks, clear focus rings, and reduced-motion support. Done cards, skeletons,
|
||||
empty states, inline save/cancel affordances, and error states will feel designed
|
||||
rather than like bare CRUD.
|
||||
|
||||
## 6. Backend architecture
|
||||
|
||||
`cmd/havenllo/main.go` reads `HAVENLLO_LISTEN_ADDR` (default `:8080`) and
|
||||
`HAVENLLO_DATABASE_PATH` (default `/data/havenllo.db`), configures standard
|
||||
library logging, opens/migrates SQLite, constructs the service and HTTP handler,
|
||||
and gracefully shuts down on SIGINT/SIGTERM.
|
||||
|
||||
`database` owns parent-directory creation, SQLite connection options, and ordered
|
||||
migrations. `store` contains parameterized SQL and record scanning, with no ORM.
|
||||
`app.Service` owns cross-record validation, default list creation, ordering and
|
||||
renumbering, and final-board rules. This keeps every state-changing operation
|
||||
transactional and independently testable.
|
||||
|
||||
`api` uses Go 1.22 `http.ServeMux` method/path patterns and `PathValue`. It
|
||||
decodes bounded JSON bodies with `DisallowUnknownFields`, maps typed domain errors
|
||||
to the documented JSON shape, and never exposes SQL errors. It registers `/api/`
|
||||
before static files: hashed Vite assets are served directly, and embedded
|
||||
`index.html` is a fallback only for non-API SPA routes. An unknown API route
|
||||
always produces JSON 404, never the SPA HTML. CORS headers are intentionally not
|
||||
set because all browser traffic is same-origin.
|
||||
|
||||
Tests cover migration/seed behaviour, cascade effects, position normalization,
|
||||
invalid moves, final-board protection, JSON validation, and essential status/
|
||||
response shapes. Temporary SQLite files and an injectable static filesystem keep
|
||||
tests independent of the PVC and production frontend bundle.
|
||||
|
||||
## 7. Dockerfile strategy
|
||||
|
||||
The Dockerfile has three stages:
|
||||
|
||||
1. A `node:22-alpine` frontend stage copies package manifests first, runs
|
||||
`npm ci`, then builds `web/` with `npm run build` into `web/dist`.
|
||||
2. A `golang:1.24-alpine` stage copies `go.mod`/`go.sum`, runs `go mod download`,
|
||||
copies source and the frontend stage's `web/dist`, then runs
|
||||
`CGO_ENABLED=0 go build`. Buildx arguments `TARGETOS` and `TARGETARCH` are
|
||||
passed to the build so the pure-Go `modernc.org/sqlite` binary targets each
|
||||
requested architecture without CGO.
|
||||
3. `gcr.io/distroless/static-debian12:nonroot` receives only the statically
|
||||
linked `havenllo` binary, exposes 8080, and runs as its non-root user.
|
||||
|
||||
The Dockerfile declares `BUILDPLATFORM`, `TARGETOS`, and `TARGETARCH` for clean
|
||||
`linux/amd64,linux/arm64` remote Buildx builds. `.dockerignore` excludes local
|
||||
`web/node_modules`, existing `web/dist`, Git metadata, test output, and `*.db`.
|
||||
The stage-to-stage copy supplies the actual bundle, so ignoring any local dist
|
||||
cannot break embedding. The final image contains no Node runtime, Go toolchain,
|
||||
shell, npm cache, source tree, or database.
|
||||
|
||||
## 8. Exact Kubernetes manifest
|
||||
|
||||
`deploy/havenllo.yaml` will contain the following four documents. Each resource
|
||||
explicitly targets the existing `default` namespace. One replica and a `Recreate`
|
||||
strategy prevent concurrent SQLite writers or a conflicting RWO attachment during
|
||||
an update. `fsGroup` matches the non-root distroless user so the provisioned NFS
|
||||
volume remains writable.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: havenllo-data
|
||||
namespace: default
|
||||
annotations:
|
||||
nfs.io/storage-path: havenllo-data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: nfs-client
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: havenllo
|
||||
namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/name: havenllo
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: havenllo
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: havenllo
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: havenllo
|
||||
image: git.ivanch.me/ivanch/havenllo:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: HAVENLLO_DATABASE_PATH
|
||||
value: /data/havenllo.db
|
||||
- name: HAVENLLO_LISTEN_ADDR
|
||||
value: :8080
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: http
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 2
|
||||
failureThreshold: 3
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: havenllo-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: havenllo
|
||||
namespace: default
|
||||
labels:
|
||||
app.kubernetes.io/name: havenllo
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: havenllo
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: havenllo
|
||||
namespace: default
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: internal-ca
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- havenllo.haven
|
||||
secretName: havenllo-tls
|
||||
rules:
|
||||
- host: havenllo.haven
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: havenllo
|
||||
port:
|
||||
number: 8080
|
||||
```
|
||||
|
||||
There is no ConfigMap or Secret: the only environment values are non-sensitive,
|
||||
and all durable state is the mounted SQLite file. The image needs no pull secret
|
||||
when the cluster is already configured for `git.ivanch.me`; if that is not true,
|
||||
the implementation will reference an existing pull-secret by name rather than
|
||||
commit registry credentials.
|
||||
|
||||
## 9. Build and deploy flow
|
||||
|
||||
`.gitea/workflows/main.yaml` will run on pushes to `main` and manual dispatch.
|
||||
It will keep YAML files LF-only via `.gitattributes` and use descriptive `name:`
|
||||
values for every Gitea job.
|
||||
|
||||
1. A `verify` job on `runner-slim-amd64` checks out the repository, runs Go
|
||||
tests, runs `npm ci` and the TypeScript/Vite production build, and performs a
|
||||
client-side `kubectl apply --dry-run=client` validation of the manifest when
|
||||
kubectl is available.
|
||||
2. A `build` job uses
|
||||
`https://git.ivanch.me/ivanch/pipeline-actions/build-and-push@main` with
|
||||
`image: git.ivanch.me/ivanch/havenllo`, `image_tag: latest`, registry-password
|
||||
and remote-builder SSH-key secrets. It retains the action's default
|
||||
`linux/amd64,linux/arm64` platforms, builds on `docker-build.haven`, and pushes
|
||||
`git.ivanch.me/ivanch/havenllo:latest`.
|
||||
3. A dependent `deploy` job uses
|
||||
`https://git.ivanch.me/ivanch/pipeline-actions/kubectl-apply@main` with
|
||||
`manifest: deploy/havenllo.yaml` and `${{ secrets.KUBE_CONFIG }}`. It then uses
|
||||
`https://git.ivanch.me/ivanch/pipeline-actions/deploy-restart@main` with
|
||||
`deployment_name: havenllo`, `namespace: default`, and the same kubeconfig.
|
||||
Applying before restart reconciles PVC/Service/Ingress changes before the
|
||||
`latest` image is restarted.
|
||||
4. Deployment verification is `kubectl -n default rollout status deployment/havenllo`,
|
||||
followed by `kubectl -n default get certificate,ingress havenllo` and opening
|
||||
`https://havenllo.haven`. cert-manager creates `havenllo-tls` through the
|
||||
existing `internal-ca` ClusterIssuer.
|
||||
|
||||
The Gitea repository requires `REGISTRY_PASSWORD`, `SSH_KEY_DOCKERBUILD`, and
|
||||
`KUBE_CONFIG` secrets. No secret values are committed.
|
||||
|
||||
## 10. Ordered implementation checklist
|
||||
|
||||
1. Add `.gitignore`, `.dockerignore`, `.gitattributes`, and README. Confirm no
|
||||
local database or generated frontend build is tracked and workflow YAML will
|
||||
remain LF-only on Windows.
|
||||
2. Add `go.mod` with only `modernc.org/sqlite`, run `go mod tidy`, and define the
|
||||
domain models and errors.
|
||||
3. Implement SQLite connection options, migration tracking, the schema above,
|
||||
and one-time `Haven`/starter-list seeding. Add migration and foreign-key tests.
|
||||
4. Implement parameterized SQLite store methods and the transactional service:
|
||||
validation, float ordering/normalization, list/card moves, board starter lists,
|
||||
cascade-aware deletion, and final-board protection. Add store/service tests.
|
||||
5. Implement the standard-library API router, JSON/error helpers, health check,
|
||||
all documented handlers, and graceful process entry point. Add HTTP contract
|
||||
tests for normal, invalid, missing, and conflict cases.
|
||||
6. Scaffold Vite/Preact, the typed client, reducer-based optimistic board state,
|
||||
board selector persistence, and loading/error states.
|
||||
7. Implement the visual components and CSS system: header, list rail, inline
|
||||
creation/editing, count badges, done state, descriptions, confirmations,
|
||||
toasts, empty states, and responsive interaction styling.
|
||||
8. Add native card drag/drop previews and drop zones for same-list and cross-list
|
||||
moves. Manually test error rollback, keyboard controls, and mobile-sized views.
|
||||
9. Add `web/embed.go` and Go static-file/SPA fallback routing. Verify a production
|
||||
frontend build is embedded and API misses return JSON rather than HTML.
|
||||
10. Add the multi-stage Dockerfile; build locally for the host platform and verify
|
||||
the container creates/uses `/data/havenllo.db`, serves `/`, and passes
|
||||
`/api/health` with a read-only root filesystem.
|
||||
11. Add the exact deployment manifest and Gitea workflow using the documented
|
||||
pipeline-actions URLs. Validate YAML, a client-side apply, and LF-only files.
|
||||
12. Push `main`; allow the multi-architecture image to build and deploy; verify
|
||||
rollout, certificate, and ingress; then smoke-test create, edit, complete,
|
||||
drag, refresh persistence, and deletion confirmation at `havenllo.haven`.
|
||||
Reference in New Issue
Block a user