feat: implement Havenllo single-board kanban (Go+SQLite backend, Preact/Vite frontend)
Some checks failed
Build and deploy Havenllo / Verify Go and frontend (push) Failing after 7s
Build and deploy Havenllo / Build and push image (push) Has been skipped
Build and deploy Havenllo / Apply and restart Havenllo (push) Has been skipped

- 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:
Hermes
2026-07-14 10:13:51 -03:00
commit 041c3fab4b
44 changed files with 5931 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
package database
import (
"context"
"database/sql"
"fmt"
"time"
)
type migration struct {
version int
apply func(context.Context, *sql.Tx) error
}
var migrations = []migration{
{version: 1, apply: migrationOne},
}
func migrate(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL
)`); err != nil {
return fmt.Errorf("create migrations table: %w", err)
}
for _, m := range migrations {
var exists int
if err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", m.version).Scan(&exists); err != nil {
return fmt.Errorf("read migration %d: %w", m.version, err)
}
if exists != 0 {
continue
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin migration %d: %w", m.version, err)
}
if err := m.apply(ctx, tx); err != nil {
_ = tx.Rollback()
return fmt.Errorf("apply migration %d: %w", m.version, err)
}
if _, err := tx.ExecContext(ctx, "INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)", m.version, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
_ = tx.Rollback()
return fmt.Errorf("record migration %d: %w", m.version, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit migration %d: %w", m.version, err)
}
}
return nil
}
func migrationOne(ctx context.Context, tx *sql.Tx) error {
statements := []string{
`CREATE TABLE lists (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL CHECK (length(trim(name)) BETWEEN 1 AND 120),
position REAL NOT NULL
)`,
`CREATE TABLE 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 idx_lists_position ON lists(position, id)`,
`CREATE INDEX idx_cards_list_position ON cards(list_id, position, id)`,
}
for _, statement := range statements {
if _, err := tx.ExecContext(ctx, statement); err != nil {
return err
}
}
for i, name := range []string{"To do", "In progress", "Done"} {
if _, err := tx.ExecContext(ctx, "INSERT INTO lists (name, position) VALUES (?, ?)", name, float64(i+1)*1024); err != nil {
return err
}
}
return nil
}