josh 15ed329743
All checks were successful
CI / test (pull_request) Successful in 9m32s
fix: db volume ownership and explicit error handling for write failures
Root cause of the 500 on create/update/delete: the non-root app user in
the Docker container lacked write permission to the volume mount point.
Docker volume mounts are owned by root by default; the app user (added
in a previous commit) could read the database but not write to it.

Fixes:

1. Dockerfile — RUN mkdir -p /app/data before chown so the directory
   exists in the image with correct ownership. Docker uses this as a
   seed when initialising a new named volume, ensuring the app user
   owns the mount point from the start.

   NOTE: existing volumes from before the non-root user was introduced
   will still be root-owned. Fix with:
     docker run --rm -v catalyst-data:/data alpine chown -R 1000:1000 /data

2. server/routes.js — replace bare `throw e` in POST/PUT catch blocks
   with console.error (route context + error) + explicit 500 response.
   Add try-catch to DELETE handler which previously had none. Unexpected
   DB errors now log the route they came from and return a clean JSON
   body instead of relying on the generic Express error handler.

3. server/db.js — wrap the boot init() call in try-catch. Fatal startup
   errors (e.g. data directory not writable) now print a clear message
   pointing to the cause before exiting, instead of a raw stack trace.

TDD: tests written first (RED), then fixed (GREEN). Six new tests in
tests/api.test.js verify that unexpected DB errors on POST, PUT, and
DELETE return 500 with { error: 'internal server error' } and call
console.error with the route context string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 11:11:00 -04:00
2026-03-28 01:44:09 -04:00
2026-03-28 02:35:00 -04:00
2026-03-28 02:35:00 -04:00
2026-03-28 02:35:00 -04:00
2026-03-28 02:35:00 -04:00
2026-03-28 02:35:00 -04:00
2026-03-28 09:52:57 -04:00
2026-03-28 02:35:00 -04:00
2026-03-28 02:35:00 -04:00

Catalyst

A self-hosted infrastructure registry. Track every VM, container, and service across your homelab — their state, stack, and which internal services are running on them.


Features

  • Dashboard — filterable, searchable instance list with state and stack badges
  • Detail pages — per-instance view with service flags, Tailscale IP, and timestamps
  • Full CRUD — add, edit, and delete instances via a clean modal interface
  • Production safeguard — only development instances can be deleted; production instances must be demoted first
  • REST API — every operation is a plain HTTP call; no magic, no framework lock-in
  • Persistent storage — SQLite database on a Docker named volume; survives restarts and upgrades
  • Zero native dependencies — SQLite via Node's built-in node:sqlite. No compilation, no binaries.

Quick start

docker run -d \
  --name catalyst \
  -p 3000:3000 \
  -v catalyst-data:/app/data \
  gitea.thewrightserver.net/josh/catalyst:latest

Or with the included Compose file:

docker compose up -d

Open http://localhost:3000.


REST API

All endpoints are under /api. Request and response bodies are JSON.

Instances

GET /api/instances

Returns all instances, sorted by name. All query parameters are optional.

Parameter Type Description
search string Partial match on name or vmid
state string Exact match: deployed, testing, degraded
stack string Exact match: production, development
GET /api/instances?search=plex&state=deployed
[
  {
    "vmid": 117,
    "name": "plex",
    "state": "deployed",
    "stack": "production",
    "tailscale_ip": "100.64.0.1",
    "atlas": 1, "argus": 0, "semaphore": 0,
    "patchmon": 1, "tailscale": 1, "andromeda": 0,
    "hardware_acceleration": 1,
    "created_at": "2024-01-15T10:30:00.000Z",
    "updated_at": "2024-03-10T14:22:00.000Z"
  }
]

GET /api/instances/stacks

Returns a sorted array of distinct stack names present in the registry.

GET /api/instances/stacks
→ ["development", "production"]

GET /api/instances/:vmid

Returns a single instance by VMID.

Status Condition
200 Instance found
404 No instance with that VMID
400 VMID is not a valid integer

POST /api/instances

Creates a new instance. Returns the created record.

Status Condition
201 Created successfully
400 Validation error (see errors array in response)
409 VMID already exists

Request body:

Field Type Required Notes
name string yes
vmid integer yes Must be > 0, unique
state string yes deployed, testing, or degraded
stack string yes production or development
tailscale_ip string no Defaults to ""
atlas 0|1 no Defaults to 0
argus 0|1 no
semaphore 0|1 no
patchmon 0|1 no
tailscale 0|1 no
andromeda 0|1 no
hardware_acceleration 0|1 no

PUT /api/instances/:vmid

Replaces all fields on an existing instance. Accepts the same body shape as POST. The vmid in the body may differ from the URL — this is how you change a VMID.

Status Condition
200 Updated successfully
400 Validation error
404 No instance with that VMID
409 New VMID conflicts with an existing instance

DELETE /api/instances/:vmid

Deletes an instance. Only instances on the development stack may be deleted.

Status Condition
204 Deleted successfully
404 No instance with that VMID
422 Instance is on the production stack
400 VMID is not a valid integer

Development

npm install
npm test          # run all tests once
npm run test:watch  # watch mode
npm start         # start the server on :3000

Tests are split across three files:

File What it covers
tests/db.test.js SQLite data layer — all CRUD operations, constraints, filters
tests/api.test.js HTTP API — all endpoints, status codes, error cases
tests/helpers.test.js UI helper functions — esc() XSS contract, fmtDate()

Versioning

Catalyst uses semantic versioning. package.json is the single source of truth for the version number.

Change Bump Example
Bug fix patch 1.0.01.0.1
New feature, backward compatible minor 1.0.01.1.0
Breaking change major 1.0.02.0.0

Cutting a release

# 1. Bump version in package.json, then:
git add package.json
git commit -m "chore: release v1.1.0"
git tag v1.1.0
git push && git push --tags

Pushing a tag triggers the full CI pipeline: test → build → release.

  • Docker image tagged :1.1.0, :1.1, and :latest in the Gitea registry
  • A Gitea release is created at v1.1.0
Description
A lightweight instance registry for tracking self-hosted infrastructure
Readme 700 KiB
2026-03-28 21:01:27 -04:00
Languages
JavaScript 80%
CSS 13.4%
HTML 6.3%
Dockerfile 0.3%