templates
The template catalog and installing one of its templates (hoserva catalog list, hoserva app install): the catalog is listed and each template is shown, with its icon and the privilege summary computed from its Compose content, from the catalog installed on disk; installing resolves the x-hoserva inputs and stores the result as a stack.
Check whether a stack's template has a newer revision
Compares the template revision the stack was installed from (its `stacks` row) with the revision the stack's own source lists now, and never changes the stack, its files or its row: a catalog update is only ever offered, and applying it is the user's action. `update_available` carries `diff`, a unified diff from the stack's stored `docker-compose.yml` to the newer revision's `compose.yaml`, and the source's badge (`sourceKind`, `signed`). A stack that was edited by hand is marked with `manuallyEdited` and its diff is against the edited file, never as if it were the pristine install. The other statuses carry no diff: `up_to_date` (the source lists the installed revision or an older one), `not_from_template` (the stack records no template or no revision number), `source_removed` (the source the stack was installed from is no longer one of the sources) and `template_removed` (the source no longer lists the template). A catalog that cannot be read is an error (503 `catalog_unavailable`), never `up_to_date`. A GPU device a template install added to the stack's Compose file shows in the diff as removed lines. An unknown stack is 404 `stack_not_found`; a name that is not a valid stack name is 400 `invalid_stack_name`.
List the catalog's templates
The templates of the catalog installed on disk, read from its `index.json` and never waiting on the network, with the catalog's `serial` and `generatedAt`, which are the curated catalog's. The curated catalog's entries come first, then those of each user-added source (`listCatalogSources`) in the order the sources were added; a template id a source earlier in that order already lists is never supplied by a later one, so a user-added entry cannot shadow a curated one. Every entry names the `source` it came from (`hoserva` for the curated catalog, a source id for a user-added source) and carries that source's badge as data: `sourceKind` and `signed`. An entry of an unsigned source has `signed` false. Every entry says whether a stack of that template id already exists (`installed`, from the `stacks` table). Entries are in that order. Search, filters and paging are the caller's. `lastCheckedAt` and `lastOutcome` report the most recent catalog check (`refreshCatalog`) since the daemon started, and are absent before any check has run. A catalog that is not installed or whose `index.json` cannot be read is refused with 503 `catalog_unavailable`, never answered with an empty list. With `checkOnOpen` on (`getCatalogSettings`), a call made when the last check is older than 15 minutes, or when none has run since the daemon started, also starts one catalog check in the background, never a second while one is running. The answer is the on-disk copy as it is now; the finished check is announced as a `catalog` event on `/api/v1/events`.
Check for catalog updates now
Runs one conditional request for the latest signed catalog from the catalog host and returns how it ended. It is an explicit user action, so it runs even when automatic refresh is off. An unchanged catalog answers `304` and downloads nothing (`unchanged`). A newer archive replaces the installed catalog only if its signature verifies against the compiled-in catalog key and its serial is strictly higher (`updated`, with the number of new and of updated templates, compared by id and revision against the catalog it replaced). Any other outcome keeps the installed catalog untouched and is `failed`, with a `reason` code and a `message`; a failed verification (`bad_signature`, `not_newer`, `bad_archive`) also raises a `catalog_check_failed` notification, and a network failure does not. A check that fails is still a completed check, answered 200. Calls made while a check is running share that check's request and result. A signature that does not verify makes the check fetch the archive and its signature once more before it reports `bad_signature`, because the two files are cached separately and can briefly disagree while a catalog is being published. Every finished check, whoever started it, is announced as a `catalog` event on `/api/v1/events`. Nothing is fetched from `api.github.com`.
List the catalog sources
The curated catalog (`hoserva`, `kind` `curated`, always first) and every source URL the user added (`kind` `user_added`) in the order they were added. `signed` is true only for a source whose installed archive passed a signature check: always for the curated catalog, and for a user-added source only when it was added with a public key. `serial` is the installed catalog's serial and is absent when it cannot be read. `lastRefreshedAt` is the last time the source's archive was installed or confirmed unchanged, and is absent before the first.
Add a catalog source
Adds a catalog source URL of the user's own: the archive `catalog.tar.zst` is fetched from beneath `url` in the same format as the curated catalog and installed, and only then is the source recorded, so a source that cannot be fetched or verified leaves nothing behind. Hoserva contacts the URL only because the user added it, and only again when the user refreshes it. The URL must be an `https` address with no credentials, query or fragment (400 `invalid_catalog_source` otherwise, before anything is fetched), a redirect must stay on the same host, and the archive is size-capped as the curated one is. With a `publicKey` (PEM, or the base64 of the raw 32-byte Ed25519 key) the archive's detached signature `catalog.tar.zst.sig` is required and must verify against that key, and the source is `signed`. With no `publicKey` the source is accepted unsigned: no signature is requested and every entry it supplies is badged `signed` false. A template of an unsigned source still goes through the same privilege summary and warnings at install. Refused with 409 `catalog_source_exists` when a source with that URL (the curated one included) exists, 502 `catalog_source_unreachable` when the archive could not be fetched, and 422 `catalog_source_rejected` when the archive was fetched but refused (a signature that does not verify, a malformed archive).
Remove a catalog source
Removes one user-added source: its on-disk copy and its record, and nothing of any other source. Stacks already installed from it are not touched: their Compose files and rows stay as they are, and they only report `source_removed` from `getStackTemplateUpdate`. The curated catalog cannot be removed: 409 `catalog_source_curated`. An unknown id is 404 `catalog_source_not_found`. A removal that fails part way leaves a source the next call finishes removing.
Check a catalog source for updates now
Runs one check of the source and returns how it ended, as `refreshCatalog` does for the curated catalog (the curated id runs that same check). A user-added source is only ever fetched when it is added or refreshed here, never by the background interval. A newer archive replaces the source's copy only if it verifies the way the source was added (against its public key, or well-formed for an unsigned source) and its serial is strictly higher. Any other outcome keeps the installed copy and everything recorded about the source, and is `failed` with a `reason` and a `message` (200). A remembered rejection never carries over from one source to another. An unknown id is 404 `catalog_source_not_found`.
Show one catalog template
The template's metadata, its `compose.yaml` text as the catalog holds it, and its privilege summary: what its Compose content asks for beyond an ordinary container, computed with each input's default (a secret, which has none, with a generated-shaped value) and never from anything the template declares. An unknown template is refused with 404 `template_not_found`; an entry that fails the template rules with 422 `template_invalid`, as `previewTemplateInstall` does. The template's source and its badge (`sourceKind`, `signed`) come with it; the privilege summary is the same for an unsigned source's template as for a curated one, because an unsigned template can ask for any privilege.
Get a catalog template's icon
The icon file the template names, with a content type from an allow-list (SVG, PNG, WebP or JPEG) chosen by the file's extension, never by its content. A template that names no icon has none to serve: 404 `template_icon_not_found`, and the web UI shows its placeholder. A file that is not a plain file inside the template's own directory (a symlink, however it points), has another extension, or is larger than 1 MiB is not served: 404 `template_icon_not_found`. An unknown template is 404 `template_not_found`. The response forbids content sniffing and scripts, styles and subresources beyond the image itself, so an SVG cannot run code when it is opened directly. An `<img>` element loads it, which cannot send the `X-Hoserva-Session-Secret` header, so the session cookie alone is accepted here.
Get one of a catalog template's screenshots
The screenshot at that position of the template's `screenshots` list, with a content type from an allow-list (PNG, WebP or JPEG) chosen by the file's extension, never by its content. It is addressed by position, so no file path ever appears in a request. A file that is not a plain file inside the template's own directory (a symlink in the path or as the file, however it points), has another extension, or is larger than 4 MiB is not served, and neither is a position past the end of the list: 404 `template_screenshot_not_found`. An unknown template is 404 `template_not_found`. The response forbids content sniffing and anything but the image itself. An `<img>` element loads it, which cannot send the `X-Hoserva-Session-Secret` header, so the session cookie alone is accepted here.
Preview installing a catalog template
Resolves the template's inputs the way an install would, without creating a stack or writing any file of the template: paths default to the template's own default or, with none, to the existing share of the input's role (`/mnt/user/<role>`) and list the existing shares as suggestions; a port that a container publishes or is configured to publish, running or stopped, that an existing stack's Compose file publishes (resolved with its `.env`, whether or not the stack was started), or that the host listens on resolves to the next free port above it, with the port asked for in `requestedValue`; a timezone with no value takes the host's; a `device` input with role `gpu` lists the host's render devices (`/dev/dri/renderD*`) as suggestions. A secret is never given a value here: it is marked `generated` and is generated by the install. The privilege summary is computed from the Compose content with these values substituted — privileged mode, host networking, the host PID or cgroup namespace, device cgroup rules, added capabilities, disabled or replaced confinement, extra groups, the Docker socket and host paths outside the pool and cache (the cache itself and Docker's data-root on it count as outside) — never from anything the template declares. `compose` is the file an install would write. A path input with no value, no default and no existing share to default to, a port input with neither value nor default, and a `string` input with no value and no default are listed with `required: true` and an `error`, so a form shows every field and `installTemplate` refuses them (a `string` input the template marks `optional` may be left empty and is written to `.env` with an empty value). An input that is not the template's or a value that does not fit its kind is refused with 400 `invalid_template_input`, and so is a setting that does not fit (`networkMode`, `cpus`, `memoryMiB`, `extraParams`); the error's `details.input` names the input or setting it is about. The container settings of the request (`networkMode`, `restart`, `cpus`, `memoryMiB`, `extraParams`) are written into `compose`, and `warnings` lists what they could not carry out; a name that is not a valid stack name with 400 `invalid_stack_name`; an unknown template with 404 `template_not_found`; a catalog entry that fails the template rules with 422 `template_invalid` (an input is written to the stack's `.env` under its name, so one named `PATH`, `HOME`, `XDG_RUNTIME_DIR`, `DOCKER_HOST`, `DOCKER_CONTEXT`, `DOCKER_CONFIG`, `DOCKER_CERT_PATH` or `DOCKER_TLS_VERIFY`, which Docker takes from the daemon's environment, fails them); a GPU the host cannot give to a container (no `render` group) with 409 `gpu_unavailable`; no free port above a conflicting one with 409 `no_free_port`, and the same code, never moving the port, for a host port `extraParams` publishes that is taken; Docker not reachable, which the port check needs, with 503 `docker_unavailable`. The check also reads the ports of every existing stack with `docker compose config`; a stack whose ports cannot be read refuses the request (`stack_action_failed`) instead of the port being assumed free. Reading a stack's ports also regenerates, from the stack's row, any of its `docker-compose.yml`, `.env` or `meta.json` that is missing from its directory; a file that exists is never changed, and a stack removed meanwhile is skipped. `installTemplate` does the same.
Install a catalog template as a stack
Resolves the inputs exactly as `previewTemplateInstall` does, and generates each secret that has no value of its own (48 hexadecimal characters). A port that is taken is moved to the next free port and reported in the result's `requestedValue`, never refused. Taken means what `previewTemplateInstall` lists, including the ports of stacks installed earlier that were never started, so two installs in a row are given different ports; installs run one at a time. Reading the ports of existing stacks regenerates their missing generated files from their rows, as `previewTemplateInstall` describes. A port that something other than Hoserva takes after the install is not checked. Then it creates the stack as `createStack` does: the stack's row records the template's source, id and revision, and `docker-compose.yml` (the template with its `x-hoserva` block kept, plus the `/dev/dri` device and the host's `render` group for a GPU that was chosen), `.env` (every input; secrets only here) and `meta.json` are generated and checked with `docker compose config`. Nothing is started. The result carries the privilege summary, so a template that asks for privileged mode, the Docker socket or any other kind `previewTemplateInstall` lists is reported with its install. The stack errors of `createStack` apply (409 `stack_exists`, 409 `stack_dir_exists`, 400 `invalid_stack`), as do those of `previewTemplateInstall`. An input that needs a value and has none is refused with 400 `invalid_template_input` whose `details.input` names it, and a `networkMode` that names a network that does not exist with 409 `network_missing` (the message holds the `docker network create` command); nothing is written in either case. A request that carries `planDigest` is refused with 409 `template_changed` when the plan this install would write has another digest, because the catalog was refreshed or another source supplied the template since the preview; nothing is created, and a new preview shows what the template is now.