# Catalyst A lightweight instance registry for tracking self-hosted infrastructure. No backend, no framework — just a browser, a SQLite database compiled to WebAssembly, and a static file server. :) ## Structure ``` index.html Entry point css/app.css Styles js/ config.js Service definitions and seed data db.js Data layer ui.js Rendering, modals, notifications app.js Router ``` ## Data layer All reads and writes go through five functions in `js/db.js`. This is the boundary that would be replaced when wiring Catalyst to a real backend — nothing else in the codebase touches data directly. ### `getInstances(filters?)` Returns an array of instances, sorted by name. All filters are optional. ```js getInstances() getInstances({ search: 'plex' }) getInstances({ state: 'degraded' }) getInstances({ stack: 'production' }) getInstances({ search: 'home', state: 'deployed', stack: 'production' }) ``` `search` matches against `name`, `vmid`, and `stack`. ### `getInstance(vmid)` Returns a single instance by VMID, or `null` if not found. ```js getInstance(137) // → { id, name, vmid, state, stack, ...services, createdAt, updatedAt } ``` ### `getDistinctStacks()` Returns a sorted array of unique stack names present in the registry. Used to populate the stack filter dynamically. ```js getDistinctStacks() // → ['development', 'production'] ``` ### `createInstance(data)` Inserts a new instance. Returns `{ ok: true }` on success or `{ ok: false, error }` on failure (e.g. duplicate VMID). ```js createInstance({ name: 'plex', vmid: 117, state: 'deployed', // 'deployed' | 'testing' | 'degraded' stack: 'production', tailscale_ip: '100.64.0.1', atlas: 1, argus: 1, semaphore: 0, patchmon: 1, tailscale: 1, andromeda: 0, hardware_acceleration: 1, }) ``` ### `updateInstance(id, data)` Updates an existing instance by internal `id`. Accepts the same shape as `createInstance`. Returns `{ ok: true }` or `{ ok: false, error }`. ### `deleteInstance(id)` Deletes an instance by internal `id`. Only instances on the `development` stack can be deleted — this is enforced in the UI before `deleteInstance` is ever called. --- ## Instance shape | Field | Type | Notes | |---|---|---| | `id` | integer | Internal autoincrement ID | | `vmid` | integer | Unique. Used as the public identifier and in URLs (`/instance/137`) | | `name` | string | Display name | | `state` | string | `deployed`, `testing`, or `degraded` | | `stack` | string | `production` or `development` | | `tailscale_ip` | string | Optional | | `atlas` | 0 \| 1 | | | `argus` | 0 \| 1 | | | `semaphore` | 0 \| 1 | | | `patchmon` | 0 \| 1 | | | `tailscale` | 0 \| 1 | | | `andromeda` | 0 \| 1 | | | `hardware_acceleration` | 0 \| 1 | | | `createdAt` | ISO string | Set on insert | | `updatedAt` | ISO string | Updated on every write | --- ## Versioning Catalyst uses [semantic versioning](https://semver.org). The version in `package.json` is the source of truth and must match the release tag. | 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 the version in `package.json`** ```json "version": "1.1.0" ``` **2. Commit, tag, and push** ```bash git add package.json git commit -m "chore: release v1.1.0" git tag v1.1.0 git push && git push --tags ``` Pushing the tag triggers the full pipeline: tests → build → release. - The image is tagged `:1.1.0`, `:1.1`, and `:latest` in the Gitea registry - A Gitea release is created at `v1.1.0` with the image reference in the release notes Pushes to `main` without a tag still run tests and build a `:latest` image — no release is created.