38 Commits

Author SHA1 Message Date
12834650dc docs(changelog): set v0.1.0 release date to 2026-04-29
Some checks failed
Deploy / build (push) Has been skipped
Deploy / deploy (push) Has been skipped
CI / ci (push) Failing after 13m13s
v0.1.0
2026-04-29 01:29:58 +02:00
9fd29a31c3 feat(deploy): add netcup compose overlay (build local, edge net, no host port)
All checks were successful
Deploy / build (push) Has been skipped
Deploy / deploy (push) Has been skipped
CI / ci (push) Successful in 13m0s
2026-04-29 00:59:58 +02:00
8577a19ba1 Refresh Wave pipelines, personas, and contracts
Local-only Wave configuration sync:
- Drop one-shot contract schemas no longer referenced by any
  pipeline.
- Replace gitea/github issue-impl pipelines with the unified
  refresh/research/rewrite/scope set; add bb (bitbucket) and gl
  (gitlab) variants.
- Add scoper persona and matching scope contracts.
- Update existing personas and pipelines to current style.

No effect on the librenotes runtime; this only touches the
.wave/ tooling directory used by the local dev harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:25:18 +02:00
3b3cb57aa2 Add CHANGELOG and launch validation checklist
CHANGELOG.md follows Keep-a-Changelog. The Unreleased section
sits at the top; the v0.1.0 entry summarises every commit on
the path from the empty fork to a hosted multi-tenant build:
fork + restructure, storage/auth/tenant/httpapi packages, the
serve command, full frontend (landing, login, verify, app shell,
PWA, sync), Docker + deploy + backup tooling, docs, and the
community infrastructure. Two known-incomplete items called out
explicitly: the upstream Notesium UI is not yet wired into the
multi-tenant shell, and SMTP delivery has only been exercised
against the LogMailer.

docs/launch-checklist.md is the manual QA pass for v0.1.0:
environment prerequisites, the full sign-up to first-note flow
on Chrome / Firefox / iOS Safari / Android Chrome (with explicit
PWA-install and offline-shell verifications), documentation
accuracy spot-checks, the community-infra render checks, and a
backup-and-restore round-trip on the staging host. The QA
performer signs off at the bottom; failures are filed as
separate bugs against the milestone.

The actual v0.1.0 tag will be cut by a maintainer; tagging is
not part of this commit.

Refs #30 and #33.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:20:20 +02:00
3b8847efa1 Add first-run onboarding flow
cmd/librenotes/web/public/onboarding.js triggers after a session
is verified and runs only when the tenant-scoped key
"onboarded" is unset.

Behaviour:
- Reads the dismissal flag from authClient.tenantStore() so each
  tenant's state is isolated and survives logout-then-login on
  the same device only if they're the same user.
- Lists /api/notes; if the notebook is empty, PUTs a sample
  "welcome" note so a brand-new user has somewhere to land. We
  don't seed when notes already exist (covers signing in on a
  second device for the first time).
- Opens app.html's <dialog id="onboarding-dialog"> with showModal
  and persists "onboarded": true on submit so returning users
  never see it again.
- Seed failures are logged but do not block the dialog —
  onboarding shouldn't depend on a successful network round
  trip. The dialog just won't have a sample note to point at.

Dialog content covers the four user-guide concepts: notes-as-
markdown, [[wikilinks]], offline-first sync, tenant isolation.

Service worker precaches onboarding.js so the dialog is also
available to offline-first returning visitors.

Closes #32.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:19:13 +02:00
e3c86a92c0 Add community infrastructure: issue + PR templates, CoC
Gitea issue templates (.gitea/issue_template/):
- bug.yml: structured form requiring version, environment,
  what-happened, repro steps, expected, optional logs. Routes
  security reports to security@librete.ch instead of public
  issues.
- feature.yml: prompts for the underlying problem before the
  proposed solution, plus alternatives and out-of-scope.

Pull request template (.gitea/PULL_REQUEST_TEMPLATE.md):
checklist for tests, lint, manual exercise, docs, and changelog.
Asks for explicit reviewer notes so trade-offs surface in the
PR description rather than being lost in chat.

CODE_OF_CONDUCT.md: links to Contributor Covenant 2.1 verbatim
rather than inlining; documents scope, reporting address
(conduct@librete.ch), and points enforcement at the Covenant's
own Enforcement Guidelines.

README links the docs/ tree, CONTRIBUTING, and the CoC so new
contributors find the entry points.

Closes #31.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:18:10 +02:00
61edef9483 Add user guide, self-hosting, API, and contributing docs
docs/user-guide.md — magic-link sign-in, note basics, wikilink
syntax, keyboard shortcuts, offline behaviour, and privacy
notes (sessionStorage for tokens, tenant-scoped localStorage).

docs/self-hosting.md — system requirements, Docker Compose
quick-start, the full LIBRENOTES_* env-var matrix (which are
required, which conditional), reverse proxy snippets for Caddy
and nginx, volume layout, the in-binary healthcheck, and
update/rollback procedure.

docs/api.md — every public endpoint: auth (login/verify),
notes CRUD, /api/whoami, /healthz. Status codes per endpoint,
the optimistic-locking ?base=<unix> contract for PUT/DELETE,
note-ID regex, and the rate-limit policy.

CONTRIBUTING.md — dev setup (Nix flake .#dev, plain Go, Docker),
package layout overview, coding standards (one-way dep flow,
tenant FS gateway requirement), branch naming, commit format,
and the PR process. Also points security reports at
security@librete.ch rather than public issues.

Closes #29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:52:09 +02:00
bcccba92f7 Add deploy workflow and backup tooling
CI deployment (.gitea/workflows/deploy.yml):
- Two jobs (build, deploy) gated on the repo variable
  DEPLOY_ENABLED=true so the workflow exists but does nothing
  until secrets and host are configured.
- Build pushes two image tags per run: rolling :main + the short
  SHA on main, or vX.Y.Z + :latest on tag pushes. Immutable per
  commit/tag tags make rollback trivial.
- Deploy SSHes to DEPLOY_HOST, runs docker compose pull && up -d
  in DEPLOY_PATH, then polls HEALTH_URL for up to a minute. A
  failed health check fails the workflow, which is the alert.
- Required secrets and the rollback procedure are documented in
  docs/operations.md.

Backup tooling (scripts/):
- backup.sh: SQLite online .backup snapshot + tarball of the
  per-tenant data dir + info.txt header, all wrapped into a
  single librenotes-YYYYMMDD-HHMMSS.tar.gz. Optional BACKUP_REMOTE
  triggers an rclone copy for off-site storage.
- backup-prune.sh: enforces retention "30 daily + 12 monthly".
  Sorts archives by filename (date is in the name so lex order
  matches chronological) and keeps the newest 30 plus the newest
  archive for each of the most recent 12 months.
- backup-restore-test.sh: extracts the most recent archive into
  a tmpdir, runs sqlite3 .schema (proves DB readability), and
  asserts the notes tar has at least one entry. Failure is the
  alert. Wired into a separate weekly timer.
- librenotes-backup.{service,timer}: systemd units for the daily
  03:17 UTC run with 5min jitter; ProtectSystem=strict, only
  /var/backups/librenotes is writable.
- librenotes-backup-verify.{service,timer}: weekly Monday
  04:00 UTC restore test.

Closes #26 and #27.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:49:40 +02:00
635a03098b Add Dockerfile, Compose stacks, and /healthz endpoint
Dockerfile is multi-stage:
- build: golang:1.25-bookworm, CGO_ENABLED=0 (modernc.org/sqlite
  is pure-Go) + -trimpath + -ldflags "-s -w" so the resulting
  binary is small and reproducible-ish.
- runtime: gcr.io/distroless/static:nonroot, ~2 MB. Runs as uid
  65532. /data and /var/lib/librenotes are declared volumes so
  per-tenant notes and the SQLite database survive container
  restarts.

healthcheck subcommand: distroless static has no shell or
wget/curl, so /healthz is reachable but no client to call it. A
new "librenotes healthcheck" subcommand uses net/http to GET
$LIBRENOTES_HEALTHCHECK_URL (default 127.0.0.1:8080/healthz) and
exits non-zero on failure. Both compose files invoke it from the
HEALTHCHECK directive.

httpapi adds a tiny GET /healthz that returns {"status":"ok"}
(no DB ping yet — added when readiness probes need it).

docker-compose.yml: dev stack on :8080 with named volumes and a
LogMailer; everything via env vars, JWT secret defaulted to a
dev value.
docker-compose.prod.yml: layered overrides — pulls a registry
image, expects LIBRENOTES_* env, sets memory limits and JSON-
file log rotation.

Closes #25.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:47:42 +02:00
f166485012 Add background sync controller with conflict detection
cmd/librenotes/web/public/sync.js drives the offline-online
reconciliation flow against the notes REST API:

- start(): registers online/offline window listeners, runs an
  initial syncOnce() if currently online.
- syncOnce(): push() then pull(); emits "librenotes:sync-state"
  events with state in {online, offline, syncing, synced, error}.
- push(): walks notesCache.pending() (rows with dirty=1, including
  tombstones). PUTs use ?base=<synced_at> for optimistic locking
  and DELETEs use the same. The notes API returns 409 with the
  current server body on conflict; sync.js stashes the pair in
  conflictsById and dispatches "librenotes:sync-conflict" so the
  app shell can render a resolution dialog.
- pull(): GETs the summary list, refetches any row whose server
  updated_at exceeds the local synced_at (or that is missing
  locally), and stamps it as cleanly synced. Skips locally-dirty
  rows so push's conflict path stays authoritative.
- resolveConflict(id, "local"|"remote"|"merge", merged): replays
  the user's choice. "local" and "merge" PUT with the latest
  server base so the second attempt accepts; "remote" overwrites
  the local cache with the server copy.

app.html now includes a sync-state badge in the header and a
<dialog> for conflict resolution wired to the events. app.js
calls notesSync.start() on load and routes dialog clicks back to
resolveConflict. The dialog uses native <dialog>.showModal(),
which all current target browsers support.

style.css adds badge colour states (syncing/synced/offline/error)
and a two-column conflict layout that collapses on narrow widths.

Closes #23.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:45:25 +02:00
593e311a8a Add IndexedDB-backed offline notes cache
cmd/librenotes/web/public/notes-cache.js exposes window.notesCache
with the offline-first read/write API the rest of the app uses:

Schema (object store "notes", key path "id"):
  { id, title, content, updated_at, synced_at, dirty, deleted }
plus a denormalised dirty_idx:0|1 column because IndexedDB cannot
index booleans directly. Two indexes — by_updated_at for sorted
listing, by_dirty for the sync controller's pending-queue scan.

Per-tenant database name "librenotes-notes-{user_id}" so two
users on the same browser have fully separate offline caches and
clearAll() (called from authClient.clearSession on logout) drops
only the leaving user's data.

Public surface:
- list/get/put/remove: straight CRUD.
- markDirty(id, patch): stage an offline edit. Bumps updated_at
  to now() but preserves synced_at so the sync controller can
  detect server-side concurrent edits via the ?base=<unix> 409.
- markDeleted(id): tombstone (deleted:true, dirty:true) so the
  sync controller can replay the delete on reconnect.
- markSynced(id, serverUpdatedAt): clear dirty + record
  serverUpdatedAt as synced_at; if tombstone, drop entirely.
- pending(): returns dirty rows for the sync queue.
- estimateUsage(): wraps navigator.storage.estimate so the UI
  can warn before quota.

Quota errors are remapped to a typed err.code === "QUOTA" so the
UI can show "out of space" instead of a generic failure.

The service worker precache list now includes notes-cache.js and
sync.js so the offline shell has the cache layer too.

Closes #22.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:43:44 +02:00
cdc7f26269 Add tenant-scoped notes REST API
internal/httpapi/notes.go exposes:
- GET    /api/notes            list summaries {id, title, updated_at}
- GET    /api/notes/{id}       full {id, title, content, updated_at}
- PUT    /api/notes/{id}       create/update; ?base=<unix> for
                                optimistic-locking conflict detection
- DELETE /api/notes/{id}       remove; ?base=<unix> guards against
                                deleting a row modified after the
                                client last saw it

Backed by tenant.FS so all reads/writes go through the per-user
sandbox — path traversal is rejected at parse time (regex slug)
and again by os.Root inside the FS layer.

On-disk format is plain Markdown: first line `# Title`, rest is
content. grep / cat / vim still produce a usable view of raw
files. Title round-trips through composeNote/splitTitle.

Conflict semantics: when the client supplies ?base=<unix>, the
server compares against the file's mtime. If the file is newer,
respond 409 with the current note body so the client can present
a merge UI. Same logic on DELETE returns 409 alone.

cmd/librenotes/serve.go grows a tenantPool that memoises FS
handles per user id; defer-closes them on shutdown.

Tests cover: full CRUD round-trip, cross-tenant isolation,
unauthenticated 401s, invalid IDs (regex rejection), and the
conflict path with a real mtime advance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:42:43 +02:00
49ad467aa9 Switch pane resize handle to pointer events
internal/notesium/web/app/pane.js drove the sidebar/pane resize
via mousedown + window-level mousemove/mouseup, which doesn't
fire on touch (browsers only emulate mouse for taps, not drags).

Replaced with pointer events:
- @pointerdown on the handle (covers mouse, touch, pen).
- setPointerCapture so we keep receiving pointermove/pointerup
  events when the pointer drifts off the handle. This eliminates
  the need for document-level listeners and avoids stuck-drag
  states when the user releases outside the window.
- pointermove + pointerup + pointercancel listeners on the
  captured target only — when the capture ends they're removed
  regardless of whether the user is still on top of the handle.
- Filter on event.pointerId so a second simultaneous touch
  (e.g., a multi-finger gesture) cannot hijack the in-progress
  resize.
- event.button !== 0 guard rejects right-click / middle-click.
- touch-action: none on the handle so the browser doesn't try
  to interpret a horizontal drag as a page scroll.

CodeMirror's internal mousedown handlers in note.js / preview.js
are left alone — those are link-click guards, not drags, and
CodeMirror's own pointer support handles touch internally.

Closes #20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:30:43 +02:00
e6d3893308 Overhaul CSS for 320px–2560px viewports
style.css now has explicit breakpoints and primitives covering
the full target range:

- Global: overflow-x: hidden on body, max-width:100% on media,
  fluid typography via clamp() so headings shrink on 320px.
- .wrap: 64rem cap at desktop, 72rem at 1440px, 96rem at 2560px;
  generous side padding at large widths so text doesn't hug the
  edge on huge monitors.
- .app-shell layout primitive (grid: sidebar + content [+ aside
  on ultrawide]) ready for the eventual notes UI:
    * mobile: single column, sidebar hidden behind a toggle
      ([data-sidebar="open"] reveals it).
    * 768px+: 2-column with 16rem sidebar.
    * 1024px+: 18rem sidebar.
    * 1440px+: 20rem sidebar, content max-width 56rem so reading
      lines don't grow unbounded.
    * 2560px+: 3-column (sidebar | content | aside) so the
      editor stays at reading width while the extra real estate
      hosts backlinks/preview.
- .app-resize-handle with touch-action:none so pointer-event
  drag handlers won't conflict with browser scrolling.
- Auth card tightens on viewports under 360px.

Result: no horizontal scroll at any width; content uses ultrawide
space effectively without sacrificing legibility.

Closes #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:28:19 +02:00
7c3b40c963 Add PWA manifest, service worker, and install prompt
- manifest.webmanifest: standalone display mode, theme #2563eb,
  start_url=/app.html (so users who install land in the app
  shell, not the marketing page), scope=/. Three icons: 192px
  any-purpose, 512px any-purpose, 512px maskable for adaptive
  icons on Android.
- icons/: PNGs generated from favicon.svg.
- sw.js: cache-first for the precached app shell, network-first
  for /api/* and /auth/* (we never serve stale auth or notes).
  Versioned cache name (librenotes-shell-v1) so a SW update
  evicts old assets. skipWaiting + clients.claim so a new SW
  takes over without a manual reload.
- pwa.js: registers the SW on every page and handles
  beforeinstallprompt by showing #install-btn. Hides the button
  again on appinstalled. Defer loaded so it never blocks render.
- All HTML pages link the manifest, set the theme-color meta,
  and load pwa.js. Landing page exposes the install button next
  to the existing CTAs.

Closes #19.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:27:32 +02:00
274c7054d0 Add JWT session client and tenant-scoped storage
cmd/librenotes/web/public/auth-client.js exposes window.authClient
with the full session API used by the rest of the frontend:

Session storage (#14):
- saveSession / loadSession / clearSession / isAuthenticated
- Backed by sessionStorage, not localStorage: tokens are isolated
  per tab and cleared on tab close. localStorage would survive
  tab close on a shared device, which we want to avoid.
- loadSession returns null when expires_at has passed, so callers
  treat expired sessions as logged-out without a network round
  trip.

API wrapper (#14):
- apiFetch(url, init) attaches Authorization: Bearer <jwt> to
  every call. On 401 it clears the session and redirects to
  /login.html?next=<current-path> so the user returns where they
  started. Throws after the redirect so the caller's .then does
  not run with stale data.

Tenant-scoped localStorage (#15):
- tenantStore() returns a get/set/remove wrapper whose keys are
  prefixed "librenotes:{user_id}:". Two users on the same browser
  therefore have fully independent UI state. JSON serialisation
  with try/catch fallbacks for corrupted or quota-exceeded
  storage so a bad blob never crashes the app.
- clearTenantStore(userID) removes every key with that prefix.
  Called from clearSession() so logout wipes both the JWT and
  the user's preferences.

verify.html + verify.js complete the magic-link flow: read
?token=, POST /auth/verify, hand the response to saveSession(),
strip the token from the URL via history.replaceState. Errors
route the user back to /login.html.

app.html + app.js are a minimal authenticated landing demonstrating
the full stack end-to-end: apiFetch hits /api/whoami, tenantStore
persists a theme preference, logout clears both. The full notes
UI is left to a later phase — this is the seam.

Closes #14 and #15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:25:07 +02:00
dc5a08e682 Add magic-link login UI with client-side validation
cmd/librenotes/web/public/login.{html,js}:
- Email input with required + autocomplete + autofocus, ARIA
  attributes for screen readers (aria-describedby, aria-invalid,
  role="alert" on the error container, role="status" on success).
- Client-side regex validation runs before POST to /auth/login
  to avoid a network round-trip for obvious typos. Server is
  still the source of truth.
- Loading state disables the button and changes its label.
- Success state replaces the form with "Check your email"
  including the address, plus the 15-minute / single-use note.
- Error states map server statuses to user-friendly messages:
  429 -> "too many requests", 400 -> "invalid email", anything
  else -> generic server error. Network errors get their own
  message so users can distinguish offline from server problems.
- No external CSS or JS dependencies; works with keyboard and
  on small viewports.

Closes #13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:24:48 +02:00
ee4de51728 Add librenotes serve command and public landing page
cmd/librenotes/serve.go wires the multi-tenant HTTP server:
storage + auth + httpapi packages, configurable via flags or
LIBRENOTES_* env vars. Embeds web/public/ for unauthenticated
static content. Generates an ephemeral JWT secret with a warning
when none is supplied. Adds security headers (CSP, nosniff,
DENY-frame, no-referrer) on every response. Background goroutine
purges expired magic-link tokens every 10 minutes.

cmd/librenotes/web/public/ provides the unauthenticated frontend:
- index.html: hero, features grid, fork attribution, footer.
  Mobile-first, responsive from 320px up via clamp() and
  auto-fit grid. SEO + Open Graph tags. No JS dependency.
- privacy.html: placeholder privacy policy (full text TBD).
- style.css: shared design tokens (light/dark via [data-theme]),
  used by landing, auth pages, and the post-login app shell.
- favicon.svg: minimal mark.

The "serve" command sits alongside the original notesium CLI
verbs; main.go dispatches "serve" to the new code path and
forwards everything else to notesium.Run().

Closes #16.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:24:37 +02:00
db3b6c1b5a Add tenant-aware HTTP middleware and router
internal/httpapi/ provides:
- Tenant{UserID, Email} carried on context.Context, with
  WithTenant / TenantFrom helpers and ErrNoTenant for the
  programming-error case (route reached without middleware).
- AuthMiddleware verifies an Authorization: Bearer <jwt> on every
  request via auth.Signer.Verify (which already enforces HS256
  and rejects alg=none). On failure: 401, with the underlying
  reason logged server-side but not exposed to the client.
- RequireTenantOwnership(ownerID) compares the request's tenant
  against the resource owner; returns 403 on mismatch. Handlers
  that touch tenant-owned resources call this guard.
- Server.Routes() mounts /auth/* unauthenticated and wraps
  /api/* with the middleware. /api/whoami is included as the
  canonical example of a tenant-scoped endpoint.

Tests cover: valid JWT pass-through, missing/empty Authorization,
wrong scheme, malformed JWT, tampered signature, JWT signed with
a different secret (cross-tenant key confusion), and the 200/403
matrix for RequireTenantOwnership.

Closes #11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:19:09 +02:00
c9b8c4445b Add per-tenant filesystem isolation
internal/tenant/ provides FS, a sandboxed handle for a single
tenant's notes directory. Implementation strategy:

- Defence in depth: every relative path is validated up front
  (rejects "..", absolute paths, NUL bytes, empty), then handed
  to os.Root (Go 1.24+) which enforces the boundary at the
  syscall layer using openat(2)+RESOLVE_BENEATH on Linux. This
  closes TOCTOU races and symlink-target swapping.
- WriteFile is atomic (write to .tmp, rename in-root). Mode 0o600
  on files, 0o700 on directories. Tenant root is created with
  0o700 by Open().
- Errors are normalised: fs.ErrNotExist -> ErrNotFound, anything
  os.Root rejects as "outside" the root -> ErrInvalidPath. The
  HTTP layer can map cleanly to 404 / 400.

Tests cover the full traversal attack surface — "../", absolute
paths, mixed separators, NUL bytes, "." and "" — plus symlink
escapes and cross-tenant isolation. All vectors return errors;
none escape the root.

Closes #10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:17:38 +02:00
d9f3574913 Implement email magic-link authentication
internal/auth/ provides:
- TokenStore: 32-byte cryptographically random one-time tokens.
  Only the SHA-256 hash is persisted (so a DB leak doesn't grant
  active sessions). Comparison uses subtle.ConstantTimeCompare.
  Single-use is enforced via UPDATE ... WHERE used_at IS NULL.
- Signer: HS256 JWTs with 24h lifetime, jwt.WithValidMethods to
  reject alg=none and other downgrade attacks.
- LogMailer (dev) and SMTPMailer (prod via net/smtp) behind a
  Mailer interface.
- RateLimiter: DB-backed fixed window per email; default 5 per
  15 min for the magic-link flow.
- Service: orchestrates RequestLogin (auto-creates user on first
  login, generates token, emails magic link) and Verify (consumes
  token, updates last_login, issues JWT).
- Handlers: POST /auth/login and GET/POST /auth/verify.
  HandleLogin returns 202 even on validation failure to avoid
  account enumeration; rate-limit hits surface as 429.

Schema additions: magic_tokens (with FK + cascade) and
login_attempts. UserStore.SetStoragePath added for completeness.

Tests cover: token issue/consume, single-use, expiry, rate limit,
JWT round-trip, alg=none rejection, signature tampering, purge,
HTTP handlers (login + verify, missing/invalid token paths).

Closes #9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:16:25 +02:00
0924e3cee9 Add user model and SQLite storage
internal/storage/ provides:
- Open(path) to create or open the SQLite database with WAL journal,
  busy timeout, and foreign keys enabled
- Embedded migrations that create the users table on first run
- UserStore with Create, GetByID, GetByEmail, UpdateLastLogin, Delete
- Email normalisation (trim+lowercase) and uniqueness enforcement
  with ErrEmailTaken
- ErrNotFound on lookups and deletes
- UUIDv4 IDs auto-generated when caller leaves ID empty

Uses modernc.org/sqlite (pure-Go) so the binary stays CGO-free and
matches Dockerfile.dev's CGO_ENABLED=0.

Tests cover all CRUD operations, email uniqueness (case-insensitive),
WAL mode verification, and ErrNotFound paths.

Closes #8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 22:13:28 +02:00
b409519661 Add reproducible dev environment
- flake.nix: rebrand description, add Go 1.25, gopls, gotools,
  staticcheck, golangci-lint, gnumake to all dev shells. Add a
  plain `dev` shell (`nix develop .#dev`) that does not wrap the
  shell in the bubblewrap sandbox so contributors can use a
  standard Go toolchain.
- Dockerfile.dev: golang:1.22-bookworm with make, git, gopls and
  staticcheck, /workspace as default cwd. CGO disabled.
- README: document both nix and Docker dev paths.

flake.lock is committed for reproducibility.

Closes #6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:58:59 +02:00
a36fc1c8cc Add Gitea Actions CI workflow
Runs on push to main and pull requests against main:
- go mod download + verify
- make lint (go vet)
- make build
- make test (race detector)

Uses actions/setup-go@v5 with built-in module caching, Go 1.22.
Workflow times out at 5 minutes per the acceptance criteria.

Closes #5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:55:40 +02:00
72454f08ab Add Makefile with build, test, lint, run, and clean targets
Standard targets:
- build: compiles cmd/librenotes with version/buildtime ldflags
- test: race detector enabled, full module
- lint: go vet, plus staticcheck if available
- run: build + execute, ARGS forwarded
- clean: remove binary and test/coverage artifacts

Variables (BINARY, OUTDIR, GO, GOFLAGS, LDFLAGS, TESTFLAGS) are
overridable so the CI workflow (#5) can invoke targets with
custom output paths or flags.

Closes #38.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:55:26 +02:00
dc6ef99c3a Add README with librenotes branding and build instructions
Replaces the upstream Notesium README with librenotes-specific
content: project description, multi-tenant goals, build/run
instructions referencing cmd/librenotes, Nix-based dev setup,
fork attribution, and MIT license note.

CI badge points at the workflow that #5 will create. Module path
and directory layout match the structure landed in the previous
fork commit.

Closes #37.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:53:28 +02:00
42fac0ab33 Document fork relationship with Notesium upstream
LICENSE retains the original Notesium copyright alongside librenotes.
NOTICE records the upstream URL, fork commit hash
(aff9f460c2d864112db7f0935b4168b107289d91), fork date, and
instructions for contributors who want to add the upstream remote
and cherry-pick patches.

Closes #36.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:53:04 +02:00
094250609c Fork Notesium source and restructure into Go package layout
Initial fork of github.com/alonswartz/notesium into librenotes:
- Source moved to internal/notesium/ (package notesium)
- Thin entry point at cmd/librenotes/main.go
- Module renamed to git.librete.ch/public/librenotes
- main() exposed as notesium.Run()
- LICENSE preserved (MIT), NOTICE added with attribution
- Web assets and completion.bash co-located with embedding code
  to satisfy go:embed path constraints

Closes #3, #34, #35.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:52:25 +02:00
aee9086633 Fix Gitea pipelines to use authenticated tea CLI without --login flag
Remove redundant --login librete flags from all gt-* pipeline tea commands since
authentication is already configured via tea logins. This simplifies the commands
and prevents potential authentication issues.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-25 19:15:33 +01:00
3e10fde0e1 Add CLAUDE.md with project conventions and tool preferences
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:11:46 +01:00
1230c6a538 Add Gitea issue pipelines and prompts using tea CLI
gt-issue-impl, gt-issue-research, gt-issue-rewrite, gt-issue-update
pipelines with corresponding prompts. Mirrors the gh-issue-* variants
but uses tea CLI with --login librete for Gitea authentication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:48 +01:00
22370827ee Add GitHub issue pipelines and prompts using gh CLI
gh-issue-impl, gh-issue-research, gh-issue-rewrite, gh-issue-update
pipelines with corresponding prompts for fetch-assess, plan,
implement, and create-pr steps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:42 +01:00
fc24f9a8ab Add Wave general-purpose pipelines
ADR, changelog, code-review, debug, doc-sync, explain, feature,
hotfix, improve, onboard, plan, prototype, refactor, security-scan,
smoke-test, speckit-flow, supervise, test-gen, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:36 +01:00
bfbb7c87ad Add Gitea issue personas using tea CLI
Analyst, commenter, and enhancer personas for Gitea issue
pipelines via the tea CLI with --login librete auth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:24 +01:00
58a9bd394c Add GitHub issue personas using gh CLI
Analyst, commenter, and enhancer personas for GitHub issue
pipelines via the gh CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:19 +01:00
8233d4fdd7 Add Wave base personas for pipeline agents
Core persona definitions: auditor, craftsman, debugger, implementer,
navigator, philosopher, planner, researcher, reviewer, summarizer,
supervisor, synthesizer, validator, and others.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:14 +01:00
59411ede0f Add Wave contract schemas for pipeline validation
JSON Schema definitions for all pipeline handover contracts
including issue analysis, research, enhancement, and sync flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:07 +01:00
3a74a298a5 Initial project setup with Nix flake and gitignore
Nix devshell with gh, bubblewrap sandbox, and yolo mode.
Gitignore for .claude, .wave internals, secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:02:01 +01:00