Files
havenllo/project-context.md
Hermes 500ae85e47
All checks were successful
Build and deploy Havenllo / Verify Go and frontend (push) Successful in 36s
Build and deploy Havenllo / Build and push image (push) Successful in 31s
Build and deploy Havenllo / Apply and restart Havenllo (push) Successful in 6s
new ui :)
2026-07-14 13:08:21 -03:00

10 KiB

Project Context

This file is the canonical project context for AI agents working on Havenllo. Read it before making changes and keep it current whenever architecture, APIs, data, dependencies, runtime behavior, or product decisions change.

Purpose

Havenllo (Haven + Trello) is a lightweight, single-user, no-authentication kanban app for Haven homelab tasks. It has one fixed board with ordered lists and cards. One Go binary serves both its JSON API and embedded Preact SPA; SQLite on a Kubernetes PVC is the only durable state.

Technology Stack

  • Go module havenllo, Go 1.24.
  • Backend: modernc.org/sqlite, pure Go and CGO-free.
  • Frontend runtime: preact, commonmark, and dompurify; build tooling: @preact/preset-vite, @types/commonmark, @types/node, typescript, and vite.
  • Card movement: custom Pointer Events interaction; no DnD library.
  • Static SPA assets are embedded with Go embed from web/dist.
  • Runtime port: 8080; HAVENLLO_LISTEN_ADDR defaults to :8080 and HAVENLLO_DATABASE_PATH defaults to /data/havenllo.db.
  • Docker: node:22-alpine frontend build, golang:1.24-alpine CGO-free build, then gcr.io/distroless/static-debian12.
  • Deployment: one Recreate Deployment replica, nfs-client PVC, plain nginx ingress havenllo.haven in default, no TLS.

Repository Organization

Backend

  • cmd/havenllo/main.go: reads environment, opens/migrates SQLite, wires service/API/embedded SPA, and shuts down gracefully on SIGINT/SIGTERM.
  • internal/domain/models.go: domain List, Card, Snapshot, sentinel errors, and validation error type. Active cards have no completion boolean.
  • internal/database/database.go: parent-directory setup, WAL/busy timeout/foreign-key connection settings, and migrations.
  • internal/database/migrations.go: transactional migration 1 creates lists/cards and seeds To do, In progress, and Done. Its historical cards.done column remains physically present but is dormant.
  • internal/store/sqlite.go: parameterized SQL queries and scans only. Card queries intentionally omit cards.done.
  • internal/app/service.go: transactional validation, ordering, normalization, last-list protection, list/card CRUD, and atomic move/reorder operations. It neither reads nor writes done.
  • internal/api/types.go: request DTOs with pointer fields for optional scalar values.
  • internal/api/http.go: bounded JSON decoding with unknown-field rejection, JSON error responses, and typed status mapping.
  • internal/api/handlers.go: Go 1.22 ServeMux routes for health, one board, lists, cards, and embedded SPA fallback. /api/ always returns JSON errors and needs no CORS.

Frontend

  • web/package.json: frontend scripts and the Preact/CommonMark/sanitization dependencies.
  • web/src/main.tsx: Preact bootstrap.
  • web/src/types.ts: API resource and patch types. Card and CardPatch contain no done member.
  • web/src/api.ts: typed same-origin /api fetch client and API error handling.
  • web/src/board-state.ts: reducer, optimistic board transforms, rollback state, and toasts.
  • web/src/drag.ts: movePosition, which calculates a fractional position from a target index after removing the dragged card.
  • web/src/markdown.ts: safe CommonMark renderer using commonmark plus DOMPurify, and plain-text extraction for card summaries.
  • web/src/App.tsx: board loading, optimistic mutations, selected-card state, and component wiring.
  • web/src/components/Header.tsx: Havenllo operations-board brand, local-ready state, refresh, and list-management toggle.
  • web/src/components/BoardCanvas.tsx: control-room board intro, live/edit badge, derived summary metrics, command-deck guidance, centered-or-scrollable rail, list-management add control, and board-level Pointer Events drag lifecycle. It detects whole-column targets, insertion indices, pointer cancellation, and Escape cancellation.
  • web/src/components/ListColumn.tsx: stage marker/kicker, column header/count, drop target and insertion marker, inline card creation, and management-only rename/delete controls.
  • web/src/components/CardItem.tsx: focusable card tile with a dedicated drag grip, current-list status chip, plain-text Markdown excerpt, and open-card affordance. Clicking outside the grip opens details.
  • web/src/components/CardDetailModal.tsx: accessible card dialog with title editing, current-list status, native move-to-list selector, CommonMark Write/Preview description editor, timestamps, and delete confirmation.
  • web/src/components/ConfirmDialog.tsx: accessible destructive-action confirmation.
  • web/src/components/Toast.tsx: auto-dismissing non-blocking feedback.
  • web/src/styles.css: dark night-shift control-room theme with graphite surfaces, acid-lime/aurora accents, subtle grid/orbit decoration, stage-specific columns, centered short rails, horizontal mobile scrolling, pointer drop feedback, Markdown typography, focus states, 44px touch targets, and reduced-motion support.

Embedding bridge

  • web/embed.go: embeds web/dist and exports it as web.Files after stripping the dist/ prefix. web/dist is generated and ignored except for its tracked .gitkeep placeholder, which keeps Go embeds valid before Vite runs; web/vite.config.ts recreates that placeholder after every build. Docker rebuilds the real bundle.

Backend Architecture

main.go -> api.Handler -> app.Service -> store -> database (SQLite)
                    ^                   |
                 domain <---------------+
  • database owns connections and migrations.
  • store owns SQL text and scanning only.
  • app.Service owns validation, transactions, ordering, and cross-resource behavior.
  • api owns routing, JSON, and HTTP error shapes.
  • domain contains shared dependency-free vocabulary.

Data Model

There is no boards table: one fixed board has two tables.

  • lists(id INTEGER PK, name TEXT, position REAL): ordered columns. First-run seed: To do, In progress, Done.
  • cards(id INTEGER PK, list_id INTEGER FK->lists ON DELETE CASCADE, title TEXT, description TEXT DEFAULT '', position REAL, done INTEGER 0/1, created_at TEXT, updated_at TEXT): ordered cards. done is retained only as unsupported historical storage; active code never reads or writes it. A card's sole active status is its current list name.

position is a float sort key, ordered by (position, id). New siblings use max + 1024; pointer drops select a value between adjacent cards. The service renormalizes a sibling group to 1024 increments when gaps become too small. Foreign keys are enabled on every connection; list deletion cascades cards; deletion of the final list returns 409.

API Contract

All endpoints are same-origin under /api, exchange JSON, use integer IDs, require Content-Type: application/json for mutations, and reject unknown fields.

Method Path Purpose
GET /api/health SQLite liveness response: 200 {"status":"ok"}.
GET /api/board Full board: { "lists": [ListWithCards] }.
GET /api/lists List summaries with card_count, without card bodies.
POST /api/lists Create: {"name","position"?} -> 201 {list}.
GET /api/lists/{id} One list and its cards.
PATCH /api/lists/{id} Update: {"name"?,"position"?} -> {list}.
DELETE /api/lists/{id} 204; cascades cards, or 409 for the final list.
GET /api/lists/{id}/cards Ordered cards in one list.
POST /api/lists/{id}/cards Create: {"title","description"?,"position"?} -> 201 {card}.
GET /api/cards/{id} One card.
PATCH /api/cards/{id} Update or move: {"title"?,"description"?,"list_id"?,"position"?} -> {card} atomically.
DELETE /api/cards/{id} 204.

Card JSON has id, list_id, title, description, position, created_at, and updated_at; it never has done. Sending done is an unknown field and returns 400.

Errors use { "error": { "code", "message" } }. Success uses 200, 201, or 204; malformed JSON is 400, missing resources 404, final-list deletion 409, invalid media type 415, validation errors 422, and unexpected failures 500.

Frontend Behavior

  • The SPA loads /api/board on mount; there is no board picker.
  • Start a pointer drag from a card's grip. The entire destination column accepts it; the nearest card midpoint controls the insertion marker and fractional position. Mouse and touch both work. Pointer cancel or Escape aborts without a request. Drops update optimistically and roll back with a toast on failure.
  • The card dialog also has a native Move to list select, which appends the card through the existing PATCH /api/cards/{id} behavior and provides a keyboard-friendly alternative.
  • Status is derived solely from the card's current list. Tiles show the list-name chip; the dialog shows the same read-only status. Done has no special backend meaning.
  • Descriptions remain raw strings in the API/database. The dialog writes CommonMark source and previews safe, sanitized CommonMark; card tiles show a compact plain-text rendering.
  • The Edit toggle exposes list add/rename/delete controls. Card add, edit, delete, movement, and detail viewing remain available outside list-management mode.
  • Short rails are horizontally centered on wide screens. Overflowing rails begin at the first column and use responsive horizontal scrolling and mobile scroll snapping.

Build and Deploy

  • Local verification: go test ./..., CGO_ENABLED=0 go build ./..., then cd web && npm ci && npm run build.
  • web/dist and web/node_modules are git-ignored. Docker's frontend stage always rebuilds dist.
  • The image is git.ivanch.me/ivanch/havenllo:latest, built multi-arch on docker-build.haven.
  • .gitea/workflows/main.yaml verifies Go, frontend, and offline deployment YAML before build/deploy pipeline actions. The verify job must remain cluster-free.
  • Manual deployment: kubectl apply -f deploy/havenllo.yaml -n default, then restart the Deployment. The app is served at http://havenllo.haven.

Constraints and Conventions

  • Single fixed board; lists and cards only.
  • SQLite has one writer on an NFS PVC: one replica, Recreate strategy, root container user on the NFS mount.
  • Same-origin /api, embedded SPA, no auth, no CORS, and no external services.
  • Keep the image and dependency surface small: no CGO, ORM, frontend framework beyond Preact, or DnD/state/icon libraries. commonmark and dompurify are the intentional runtime exception for safe, full Markdown descriptions.