josh 6c04a30c3a
All checks were successful
CI / test (pull_request) Successful in 9m29s
CI / build-dev (pull_request) Has been skipped
fix: skip db boot init in test env to prevent parallel worker lock
Vitest runs test files in parallel workers. Each worker imports server/db.js,
which triggered module-level init(DEFAULT_PATH) unconditionally. Two workers
racing to open the same SQLite file caused "database is locked", followed
by process.exit(1) killing the worker — surfacing as:

  Error: process.exit unexpectedly called with "1"

Fix: guard the boot init block behind NODE_ENV !== 'test'. Vitest sets
NODE_ENV=test automatically. Each worker's beforeEach(() => _resetForTest())
initialises its own :memory: database, so no file coordination is needed.

process.exit(1) is also guarded by the same condition — it must never
fire inside a test runner process.

TDD: two regression tests added to tests/db.test.js documenting the
expected boot behaviour and proving the module loads cleanly in parallel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 11:31:55 -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%