Splits the single workflow into two with distinct responsibilities:
ci.yml — runs tests on push/PR to dev and main. Powers the required
status check for branch protection on both branches.
release.yml — triggers on push to main (merged PR). Reads version from
package.json, asserts the tag doesn't already exist, creates
the git tag, generates patch notes from commits since the
previous tag, builds and pushes the Docker image, and creates
the Gitea release. No more manual git tag or git push --tags.
build.yml deleted — all three of its jobs are covered by the new files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.0 → 1.0.1 |
| New feature, backward compatible | minor | 1.0.0 → 1.1.0 |
| Breaking change | major | 1.0.0 → 2.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:latestin the Gitea registry - A Gitea release is created at
v1.1.0