Apps API
GET /api/v1/apps
Section titled “GET /api/v1/apps”List Apps
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
host | query | integer | null | no | |
q | query | string | null | no | |
status | query | string | null | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
DELETE /api/v1/apps/{app_id}
Section titled “DELETE /api/v1/apps/{app_id}”Uninstall App
RBAC: admin. Entitlement: apps.uninstall.
Remove an app, either by destroying its CT or by forgetting it.
Doc 01’s apps-only model means one app is exactly one LXC container, so
“uninstall” is “destroy that container”. keep_ct is the escape hatch for
the operator who wants Proxploy out of the way without losing the
workload, and it is the inverse of adopt rather than a softer delete.
Returns 200, not 202, for both outcomes, unlike every other job-returning route in this API. keep_ct: true returns {removed: true, ct_kept: true} synchronously and leaves the container running on PVE. The default (destroy) path returns {job} (job kind app.uninstall), which is async despite the 200.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Request body, application/json, UninstallIn
| Field | Type | Required | Description |
|---|---|---|---|
confirm | string | null | no | |
keep_ct | boolean | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
| 404 | app not found | |
| 409 | confirm_required: confirm must equal the app’s current name |
GET /api/v1/apps/{app_id}
Section titled “GET /api/v1/apps/{app_id}”App Detail
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
PATCH /api/v1/apps/{app_id}
Section titled “PATCH /api/v1/apps/{app_id}”Reconfigure App
RBAC: operator. Entitlement: apps.reconfigure.
Resize a CT and/or edit how Proxploy presents the app.
Resource changes go straight to PVE rather than through a job: an lxc
config write is synchronous there (see guest_config_update), so there is
no task to track and reporting one would be theatre.
Disk size is deliberately not here. Growing a CT’s root volume is a different PVE endpoint and is one-way (PVE cannot shrink), which makes it its own feature with its own confirmation rather than a field on a PATCH.
cores/memory/swap are VM.Config.CPU/Memory, lifecycle privileges, so the
client below asks for “lifecycle” explicitly: this call site defaulted
to whatever client_for_host resolved before per-capability tokens
existed, which worked only because the one token in play was
over-scoped. Found during the sweep (host-token-privileges-step-one-
report.md), same class of gap as Sys.PowerMgmt.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Request body, application/json, ReconfigureIn
| Field | Type | Required | Description |
|---|---|---|---|
cores | integer | null | no | |
memory_mb | integer | null | no | |
name | string | null | no | |
swap_mb | integer | null | no | |
web_path | string | null | no | |
web_port | integer | null | no | |
web_protocol | string | null | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
| 404 | app not found | |
| 409 | app has no host | |
| 422 | validation error, or nothing to change | |
| 502 | pve_error |
POST /api/v1/apps/{app_id}/{action}
Section titled “POST /api/v1/apps/{app_id}/{action}”App Lifecycle
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes | |
action | path | string | yes |
Request body, application/json, LifecycleIn
| Field | Type | Required | Description |
|---|---|---|---|
confirm | string | null | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 202 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/apps/{app_id}/logs
Section titled “GET /api/v1/apps/{app_id}/logs”App Logs
Doc 05: ‘Recent CT log lines (journal tail via pct exec / console channel)’. No such exec/journal channel exists anywhere in this codebase yet; services/lifecycle.py and executor/ only ever run install/update scripts over SSH on the HOST, never a command inside a guest CT, and ProxmoxClient has no pct-exec-equivalent call. Rather than fabricate log lines, this is a real, deliberate 501 so the frontend can render an honest gap (see AppLogs) instead of silently polling a 404 forever.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
POST /api/v1/apps/{app_id}/migrate
Section titled “POST /api/v1/apps/{app_id}/migrate”Migrate App Route
Params handed to the job are ONLY app_id/target_host_id: strategy, target ctid and shared storage all come from a FRESH preflight the handler itself runs, never from this route’s own preflight call below; state (host connectivity, storage, capacity) can change in the gap between this request and the job actually running (Task 15 interfaces note).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Request body (required), application/json, MigrateIn
| Field | Type | Required | Description |
|---|---|---|---|
confirm | string | null | no | |
storage | string | null | no | |
target_host_id | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 202 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
POST /api/v1/apps/{app_id}/migrate/preflight
Section titled “POST /api/v1/apps/{app_id}/migrate/preflight”Migrate Preflight
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Request body (required), application/json, MigratePreflightIn
| Field | Type | Required | Description |
|---|---|---|---|
storage | string | null | no | |
target_host_id | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/apps/{app_id}/network
Section titled “GET /api/v1/apps/{app_id}/network”App Network
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
PUT /api/v1/apps/{app_id}/network/{iface}
Section titled “PUT /api/v1/apps/{app_id}/network/{iface}”App Network Update
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes | |
iface | path | string | yes |
Request body (required), application/json, NicIn
| Field | Type | Required | Description |
|---|---|---|---|
bridge | string | null | no | |
firewall | boolean | null | no | |
gw | string | null | no | |
gw6 | string | null | no | |
ip | string | null | no | |
ip6 | string | null | no | |
link_down | boolean | null | no | |
mtu | integer | null | no | |
rate | number | null | no | |
tag | integer | null | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/apps/{app_id}/ports
Section titled “GET /api/v1/apps/{app_id}/ports”App Ports
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response |
GET /api/v1/apps/{app_id}/script
Section titled “GET /api/v1/apps/{app_id}/script”Get App Script
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
PUT /api/v1/apps/{app_id}/script
Section titled “PUT /api/v1/apps/{app_id}/script”Put App Script
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Request body (required), application/json, ScriptIn
| Field | Type | Required | Description |
|---|---|---|---|
content | string | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
POST /api/v1/apps/{app_id}/script/revert
Section titled “POST /api/v1/apps/{app_id}/script/revert”Revert App Script
Task 5 review found a dead end: put_app_script above always writes
source="edited", and nothing else ever writes source="upstream" except
the install/update job handlers, so once an app’s script is edited,
services/appstore.py::_resolve_update’s edited-script guard blocks
app.update FOREVER, even if the operator pastes the exact upstream text
back (there was no way to re-mark a row “upstream”). This route is that
way back: pin a NEW version to the catalog’s CURRENT install_script,
sourced “upstream”, so pinned_ref reads the catalog sha again and the
guard clears.
Never mutates or deletes the edited row being reverted from: the version history is the record, same rule put_app_script already follows.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/apps/{app_id}/script/versions
Section titled “GET /api/v1/apps/{app_id}/script/versions”List App Script Versions
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/apps/{app_id}/update
Section titled “GET /api/v1/apps/{app_id}/update”Get App Update
What an update would do: which commit to which, and the script diff.
Doc 10 Phase 7 requires the same diff/consent surface install has, so the
diff shown here is the SAME _diff_vs_upstream the Config tab renders:
one implementation, one answer, no chance of the two disagreeing about
what is about to run.
Unlike the Config tab’s GET /script (which always shows drift, including
the rare case where a catalog refresh moves raw.install_script without
the pinned commit changing), this route only surfaces a diff when there is
an update TO show. A caller here is asking “what would POST .../update
do”, and the honest answer when the app is already on the catalog’s
commit is “nothing”, not a diff sourced from unrelated content drift.
An edited newest script (script_source == "edited") is reported as no
update available at all, never a diff: upstream_ref is NULL on that row,
so POST will refuse regardless of the catalog state (see update_app), and
showing a populated diff_vs_upstream/update_available here would
advertise an action POST is about to reject.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
POST /api/v1/apps/{app_id}/update
Section titled “POST /api/v1/apps/{app_id}/update”Update App
Root-consent gated, exactly like install (api/catalog.py::install_catalog_entry): this re-runs a community script as root on the node, and brief §8 says the honest thing is to make the operator say so out loud. Unlike install (admin-only), doc 05 grants this to operator; a lower bar than the catalog table above intentionally accepts, not an oversight to fix here.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Request body (required), application/json, proxploy__api__apps__UpdateIn
| Field | Type | Required | Description |
|---|---|---|---|
consent | boolean | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 202 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/apps/{app_id}/web-url
Section titled “GET /api/v1/apps/{app_id}/web-url”Resolve Web URL
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app_id | path | integer | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
POST /api/v1/apps/adopt
Section titled “POST /api/v1/apps/adopt”Adopt Apps
Bulk-adopt pre-existing/discovered CTs as tracked apps (doc 05, Phase 4).
One commit for the whole batch: a mid-batch ux_apps_host_ctid conflict rolls back everything flushed so far in this request (nothing partially lands), and a single audit row covers the whole batch rather than one per item.
Request body (required), application/json, AdoptIn
| Field | Type | Required | Description |
|---|---|---|---|
items | AdoptItem[] | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/apps/discovered
Section titled “GET /api/v1/apps/discovered”Discovered
Pre-existing CTs not yet adopted (doc 05). Read-only until Phase 4.
Two Hosts can be two nodes of the SAME cluster; cluster_resources()
returns the whole cluster from either one, so every host’s snapshot
lists the same unadopted CT, each carrying node, the CT’s real owning
node (already correct in the payload; see pollers/init.py). Deduped
here by (cluster, ctid): a ctid is only unique WITHIN a cluster, so two
different clusters (or two standalone hosts, see cluster_scope) can
legitimately both have a CT 101 and both must be offered, and
attributed to the Host actually registered at that node, not whichever
host happened to poll it. An already-tracked App’s own poll cycle only
checks its own host_id (mapped_ctids is host-scoped), so a CT adopted on
one host still shows up as discovered in another host’s snapshot of the
SAME cluster; checking every App row here, scoped the same way, is what
keeps it from being offered for adoption twice.
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
POST /api/v1/apps/update-all
Section titled “POST /api/v1/apps/update-all”Update All Apps
One app.update job per stale app (doc 05: “per-app results”).
No new queue machinery: JobBackend.MAX_CONCURRENT already runs four at a time and genuinely queues the rest, and each job carries its own status, transcript and result, which is what “per-app results” means.
skipped is not decoration. A bare “0 jobs started” is indistinguishable
from a broken endpoint, so every app that did not get a job says why.
Reuses _update_state and mirrors POST /{app_id}/update’s own skip
order exactly, so a bulk run and a single-app run never disagree about
why a given app didn’t get a job:
- Edited script first: an edited row’s
upstream_refis NULL, so checking “no pinned script” before “edited” would misreport an edited app as having no upstream at all. Enqueueing anyway would spray a guaranteed-JobFailedjob (services/appstore.py:: _resolve_update refuses to discard local edits), so this is skipped, not enqueued-to-fail. - No catalog entry / no upstream_sha / no pinned script at all.
- Already on the catalog’s current commit.
Request body (required), application/json, proxploy__api__apps__UpdateIn
| Field | Type | Required | Description |
|---|---|---|---|
consent | boolean | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 202 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |