Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
|
|
|
|
|
|
// Operator status dashboard — the data behind pb_public/status.html.
|
|
|
|
|
//
|
|
|
|
|
// GET /api/status (superuser) → one JSON snapshot
|
|
|
|
|
// GET /api/status/logs?app=<uuid>&lines=N (superuser) → a service's container logs
|
|
|
|
|
//
|
|
|
|
|
// WHY THIS EXISTS: answering "is Amber healthy?" meant opening Coolify's UI,
|
|
|
|
|
// clicking through per-app pages for status, clicking again for logs, then
|
|
|
|
|
// querying PocketBase separately for what the family is actually being served.
|
|
|
|
|
// This puts the five questions that get asked when something breaks on one screen:
|
|
|
|
|
// is it up, did the last deploy succeed, what did it say, are clients erroring,
|
|
|
|
|
// and is the newest release the one I think it is.
|
|
|
|
|
//
|
|
|
|
|
// **Superuser-gated, not user-gated.** There is no "operator" flag on `users` and
|
|
|
|
|
// inventing one would need a migration; the owner already has superuser
|
|
|
|
|
// credentials for /_/, so that is the honest gate. `nsfwEnabled` was considered
|
|
|
|
|
// and rejected — it is a content flag, and using it as an admin flag would mean
|
|
|
|
|
// any adult-enabled family member could read infrastructure logs.
|
|
|
|
|
//
|
|
|
|
|
// **This is the first hook in the project to make an outbound HTTP request.**
|
|
|
|
|
// Everything before it read only PocketBase's own database (see the note in
|
|
|
|
|
// amber-app/CLAUDE.md about the JSVM having no outbound-HTTP precedent). Two
|
|
|
|
|
// consequences, both handled below: every external call is wrapped so a dead
|
|
|
|
|
// dependency degrades one panel instead of failing the whole response, and every
|
|
|
|
|
// call carries a short timeout so a hung upstream cannot pin a PocketBase worker.
|
|
|
|
|
//
|
|
|
|
|
// **The Coolify token never reaches the browser.** That is the entire reason this
|
|
|
|
|
// endpoint exists rather than the page calling Coolify directly.
|
|
|
|
|
//
|
2026-08-05 19:28:28 +00:00
|
|
|
// The variable is COOLIFY_**READ**_TOKEN, and the name is the point: this dashboard
|
|
|
|
|
// only ever performs GETs, so it must never be handed the write-capable token that
|
|
|
|
|
// `coolify.sh` uses for deploys. A full token here would mean a PocketBase
|
|
|
|
|
// vulnerability could deploy or stop every app on the PaaS.
|
|
|
|
|
//
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
// NOTE: PocketBase runs each routerAdd handler in its own isolated JSVM, so
|
|
|
|
|
// anything a handler needs must be declared INSIDE it — file-scope helpers throw
|
|
|
|
|
// ReferenceError at request time. Nothing is hoisted here; keep it that way.
|
|
|
|
|
|
2026-08-05 19:23:54 +00:00
|
|
|
// Pretty URL, following the same pattern as /settings → /settings.html.
|
|
|
|
|
//
|
|
|
|
|
// Without it, `/status` does NOT 404 — PocketBase's `indexFallback` (on by
|
|
|
|
|
// default) quietly serves index.html for any unmatched static path, so the URL
|
|
|
|
|
// looked like it worked and returned the family landing page instead. The docs
|
|
|
|
|
// were written with the pretty URL, so the docs were wrong until this existed.
|
|
|
|
|
routerAdd("GET", "/status", (e) => e.redirect(302, "/status.html"))
|
|
|
|
|
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
routerAdd("GET", "/api/status", (e) => {
|
|
|
|
|
// ---- helpers, all handler-local (isolated JSVM) ------------------------
|
|
|
|
|
const safe = (label, fn) => {
|
|
|
|
|
try {
|
|
|
|
|
return fn()
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return { error: String(err && err.message ? err.message : err), _label: label }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchJson = (url, headers, timeoutSec) => {
|
|
|
|
|
const res = $http.send({
|
|
|
|
|
url: url,
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: headers || {},
|
|
|
|
|
timeout: timeoutSec || 6,
|
|
|
|
|
})
|
|
|
|
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
|
|
|
throw new Error("HTTP " + res.statusCode)
|
|
|
|
|
}
|
|
|
|
|
return res.json
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const env = (k) => {
|
|
|
|
|
const v = $os.getenv(k)
|
|
|
|
|
return v && v.length ? v : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- what the family is being served (local DB, always available) ------
|
|
|
|
|
const releases = safe("releases", () => {
|
Stop choosing an update variant, and drop the 18+ controls
Amber ships one build. The manifest hook returned `adult` or `clean`
according to the account's nsfwEnabled flag; it now returns `clean` to
everyone.
The `variant` field itself stays in the response, and that is the
important part: every Amber already installed compares it against its
own and refuses a mismatch SILENTLY - no error, no prompt, the update
simply never appears. Removing the field, or sending anything else,
would strand every one of those installs with nothing to see.
whoami stops returning nsfwEnabled. It was there in case some future
amber-api surface wanted to scope results by it; none was built, and an
account flag nobody reads is what a token-introspection endpoint should
not be handing out. amber-api's copy goes with it - it was parsed into
the user object and never once branched on.
The dashboard loses the 18+ account tile, the variant column, the
per-flavour error table, the 18+ pill, the per-user enable/disable
button and the new-account checkbox. A control that sets a flag nothing
reads is worse than no control.
Releases are now read as newest-per-platform filtered to variant='clean',
rather than newest-per-platform-and-variant. Leftover 18+ rows are still
in the collection and would otherwise have been reported as what the
family is being served.
Those rows and the collection rule that hides them are deliberately left
alone: the rule is what keeps them unreachable, and deleting published
artifacts is not something to do as a side effect of a cleanup.
check-flavor.py is gone. It proved which of two builds an artifact was,
by a marker compiled into the Dart snapshot, because a clean-named
Windows installer once carried the 18+ payload. With one build there is
nothing to tell apart.
2026-08-30 02:09:10 +00:00
|
|
|
// Newest build per platform. Three rows in practice; a 200-row window is
|
|
|
|
|
// plenty and bounds the query regardless of history size.
|
|
|
|
|
//
|
|
|
|
|
// Filtered to variant='clean' rather than reading whatever is newest: rows
|
|
|
|
|
// from the 18+ flavour are still in this collection, and a leftover one
|
|
|
|
|
// could otherwise be reported as what the family is being served.
|
|
|
|
|
const rows = $app.findRecordsByFilter(
|
|
|
|
|
"releases", "variant = 'clean'", "-buildNumber", 200, 0)
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
const newest = {}
|
|
|
|
|
for (const r of rows) {
|
Stop choosing an update variant, and drop the 18+ controls
Amber ships one build. The manifest hook returned `adult` or `clean`
according to the account's nsfwEnabled flag; it now returns `clean` to
everyone.
The `variant` field itself stays in the response, and that is the
important part: every Amber already installed compares it against its
own and refuses a mismatch SILENTLY - no error, no prompt, the update
simply never appears. Removing the field, or sending anything else,
would strand every one of those installs with nothing to see.
whoami stops returning nsfwEnabled. It was there in case some future
amber-api surface wanted to scope results by it; none was built, and an
account flag nobody reads is what a token-introspection endpoint should
not be handing out. amber-api's copy goes with it - it was parsed into
the user object and never once branched on.
The dashboard loses the 18+ account tile, the variant column, the
per-flavour error table, the 18+ pill, the per-user enable/disable
button and the new-account checkbox. A control that sets a flag nothing
reads is worse than no control.
Releases are now read as newest-per-platform filtered to variant='clean',
rather than newest-per-platform-and-variant. Leftover 18+ rows are still
in the collection and would otherwise have been reported as what the
family is being served.
Those rows and the collection rule that hides them are deliberately left
alone: the rule is what keeps them unreachable, and deleting published
artifacts is not something to do as a side effect of a cleanup.
check-flavor.py is gone. It proved which of two builds an artifact was,
by a marker compiled into the Dart snapshot, because a clean-named
Windows installer once carried the 18+ payload. With one build there is
nothing to tell apart.
2026-08-30 02:09:10 +00:00
|
|
|
const key = r.getString("platform")
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
if (newest[key]) continue
|
|
|
|
|
newest[key] = {
|
|
|
|
|
platform: r.getString("platform"),
|
|
|
|
|
version: r.getString("version"),
|
|
|
|
|
build: r.getInt("buildNumber"),
|
|
|
|
|
size: r.getInt("size"),
|
|
|
|
|
sha8: r.getString("sha256").substring(0, 8),
|
|
|
|
|
published: r.getDateTime("created").string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Object.keys(newest).sort().map((k) => newest[k])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ---- are clients erroring (local DB) ----------------------------------
|
|
|
|
|
const clients = safe("clients", () => {
|
|
|
|
|
const since = new DateTime()
|
|
|
|
|
// 24h back. dateTime arithmetic in the JSVM is awkward, so compare on a
|
|
|
|
|
// string built from the epoch — client_logs.created is an autodate.
|
|
|
|
|
const cutoff = new DateTime(
|
|
|
|
|
new Date(Date.now() - 24 * 3600 * 1000).toISOString().replace("T", " ").substring(0, 19)
|
|
|
|
|
)
|
|
|
|
|
const rows = $app.findRecordsByFilter(
|
|
|
|
|
"client_logs",
|
|
|
|
|
"created >= {:cutoff}",
|
|
|
|
|
"-created",
|
|
|
|
|
2000,
|
|
|
|
|
0,
|
|
|
|
|
{ cutoff: cutoff }
|
|
|
|
|
)
|
|
|
|
|
const byEvent = {}
|
|
|
|
|
let errors = 0
|
|
|
|
|
let stutter = 0
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
const ev = r.getString("event") || "?"
|
|
|
|
|
byEvent[ev] = (byEvent[ev] || 0) + 1
|
|
|
|
|
const kind = (r.getString("kind") || "").toLowerCase()
|
|
|
|
|
if (kind === "error" || ev === "uncaught" || ev === "player_error") errors++
|
|
|
|
|
// A viewer saying it stuttered while the counters saw nothing is the
|
|
|
|
|
// documented blind spot worth surfacing, not burying in a total.
|
|
|
|
|
const meta = r.getString("meta") || ""
|
|
|
|
|
if (meta.indexOf('"rating":"stutter"') >= 0) stutter++
|
|
|
|
|
}
|
|
|
|
|
return { window: "24h", total: rows.length, errors: errors, stutterReports: stutter, byEvent: byEvent }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const accounts = safe("accounts", () => {
|
|
|
|
|
const rows = $app.findRecordsByFilter("users", "id != ''", "", 500, 0)
|
Stop choosing an update variant, and drop the 18+ controls
Amber ships one build. The manifest hook returned `adult` or `clean`
according to the account's nsfwEnabled flag; it now returns `clean` to
everyone.
The `variant` field itself stays in the response, and that is the
important part: every Amber already installed compares it against its
own and refuses a mismatch SILENTLY - no error, no prompt, the update
simply never appears. Removing the field, or sending anything else,
would strand every one of those installs with nothing to see.
whoami stops returning nsfwEnabled. It was there in case some future
amber-api surface wanted to scope results by it; none was built, and an
account flag nobody reads is what a token-introspection endpoint should
not be handing out. amber-api's copy goes with it - it was parsed into
the user object and never once branched on.
The dashboard loses the 18+ account tile, the variant column, the
per-flavour error table, the 18+ pill, the per-user enable/disable
button and the new-account checkbox. A control that sets a flag nothing
reads is worse than no control.
Releases are now read as newest-per-platform filtered to variant='clean',
rather than newest-per-platform-and-variant. Leftover 18+ rows are still
in the collection and would otherwise have been reported as what the
family is being served.
Those rows and the collection rule that hides them are deliberately left
alone: the rule is what keeps them unreachable, and deleting published
artifacts is not something to do as a side effect of a cleanup.
check-flavor.py is gone. It proved which of two builds an artifact was,
by a marker compiled into the Dart snapshot, because a clean-named
Windows installer once carried the 18+ payload. With one build there is
nothing to tell apart.
2026-08-30 02:09:10 +00:00
|
|
|
return { total: rows.length }
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ---- containers (Coolify) --------------------------------------------
|
|
|
|
|
// Degrades to configured:false rather than failing the response, so the panels
|
|
|
|
|
// above still render on a box where these env vars were never set.
|
|
|
|
|
const coolify = safe("coolify", () => {
|
|
|
|
|
const base = env("COOLIFY_URL")
|
2026-08-05 19:28:28 +00:00
|
|
|
const token = env("COOLIFY_READ_TOKEN")
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
if (!base || !token) {
|
|
|
|
|
return {
|
|
|
|
|
configured: false,
|
2026-08-05 19:28:28 +00:00
|
|
|
why: "COOLIFY_URL / COOLIFY_READ_TOKEN are not set on this container — add them " +
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
"in Coolify → amber-backend → Environment Variables and redeploy.",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const apps = fetchJson(base.replace(/\/+$/, "") + "/api/v1/applications",
|
|
|
|
|
{ Authorization: "Bearer " + token }, 8)
|
|
|
|
|
// Only Amber's own services; the same instance also hosts unrelated apps.
|
|
|
|
|
const mine = [
|
|
|
|
|
"amber-backend", "amber-api", "stremio_prehrajto", "Stremio_Torbox",
|
|
|
|
|
"adult-stremio-addon", "myanime-pair-server", "stripchat-proxy",
|
|
|
|
|
]
|
|
|
|
|
const out = []
|
|
|
|
|
for (const a of apps) {
|
|
|
|
|
const name = a.name || ""
|
|
|
|
|
let keep = false
|
|
|
|
|
for (const m of mine) if (name.indexOf(m) === 0) keep = true
|
|
|
|
|
if (!keep) continue
|
|
|
|
|
out.push({
|
|
|
|
|
name: name,
|
|
|
|
|
uuid: a.uuid,
|
|
|
|
|
status: a.status || "?",
|
|
|
|
|
fqdn: a.fqdn || null,
|
|
|
|
|
// sync.petruzalekr.cz is kSyncServerBase — Amber-critical despite the
|
|
|
|
|
// legacy repo name. Flagged so nobody decommissions it as old myAnime.
|
|
|
|
|
note: name.indexOf("myanime-pair-server") === 0 ? "kSyncServerBase — Amber-critical" : null,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
out.sort((x, y) => (x.name < y.name ? -1 : 1))
|
|
|
|
|
return { configured: true, apps: out }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ---- amber-api vitals -----------------------------------------------
|
|
|
|
|
const amberApi = safe("amberApi", () => {
|
|
|
|
|
const base = env("AMBER_API_URL")
|
|
|
|
|
const secret = env("AMBER_ADMIN_SECRET")
|
|
|
|
|
if (!base || !secret) {
|
|
|
|
|
return {
|
|
|
|
|
configured: false,
|
|
|
|
|
why: "AMBER_API_URL / AMBER_ADMIN_SECRET are not set on this container. " +
|
|
|
|
|
"ADMIN_SECRET is the operator role (x-amber-admin), NOT one of the " +
|
|
|
|
|
"SERVICE_SECRETS pairs, which grant an addon's identity.",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const stats = fetchJson(base.replace(/\/+$/, "") + "/v1/admin/stats",
|
|
|
|
|
{ "x-amber-admin": secret }, 8)
|
|
|
|
|
return { configured: true, probe: stats.probe, queue: stats.queue,
|
2026-09-07 20:05:42 +00:00
|
|
|
metadata: stats.metadata, health: stats.health,
|
|
|
|
|
// Upstream liveness rides along on the same call rather than a
|
|
|
|
|
// second round trip: it is produced by the same service and read
|
|
|
|
|
// by the same screen.
|
2026-09-07 20:26:28 +00:00
|
|
|
upstreams: stats.upstreams,
|
|
|
|
|
throughput: stats.throughput }
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return e.json(200, {
|
|
|
|
|
generatedAt: new DateTime().string(),
|
|
|
|
|
releases: releases,
|
|
|
|
|
clients: clients,
|
|
|
|
|
accounts: accounts,
|
|
|
|
|
coolify: coolify,
|
|
|
|
|
amberApi: amberApi,
|
|
|
|
|
})
|
|
|
|
|
}, $apis.requireSuperuserAuth())
|
|
|
|
|
|
Dashboard: error history, and which publish owns it
GET /api/status/errors plus a panel: 14 daily bars (records vs errors), then
breakdowns by version, platform and flavour.
**Rate, not raw count, is what attributes errors to a publish.** Versions have
wildly different exposure -- right now 1.1.4+13 has 177 records to 1.1.5+14's 20 --
so raw counts would make whichever build the family used most look worst. Every
row carries total, errors and errors/total, coloured on the rate.
**Flavour is not recorded yet and the panel says so.** TelemetryService sends
appVersion, platform and device, but nothing carries clean-vs-adult. The endpoint
reads a top-level field and then meta.flavor, so the day the app starts
sending it this fills in with no backend change; until then the row reads
'neznámá' with a note explaining why. No app change was made here -- that is left
noted, per the owner.
The window is 14 days because that is the retention ceiling: client_logs.pb.js
trims nightly, so nothing older exists to graph. Every day is pre-seeded so a
missing bar and a zero bar cannot look alike.
Inline SVG rather than a charting library -- this page is served by PocketBase with
no build step, and 14 bars do not justify a dependency. Validated on a clean local
boot: 14 bars, correct axis labels, no crash on empty data.
2026-08-05 19:34:42 +00:00
|
|
|
// Error history, sliced by the things that explain it: which publish, which OS,
|
|
|
|
|
// which flavour.
|
|
|
|
|
//
|
|
|
|
|
// GET /api/status/errors?days=14 (superuser)
|
|
|
|
|
//
|
|
|
|
|
// **Rate, not raw count, is what attributes errors to a publish.** Versions have
|
|
|
|
|
// wildly different exposure — at the time of writing 1.1.4+13 had 177 records to
|
|
|
|
|
// 1.1.5+14's 20 — so raw counts make whichever build the family used most look
|
|
|
|
|
// worst. Every version row therefore carries total, errors and errors/total.
|
|
|
|
|
//
|
|
|
|
|
// **Flavour is not recorded yet.** The app's TelemetryService sends appVersion,
|
|
|
|
|
// platform and device, but nothing carries clean-vs-adult. This reads a top-level
|
|
|
|
|
// `flavor` field first and then `meta.flavor`, so the day the app starts sending
|
|
|
|
|
// it this endpoint needs no change; until then the column honestly reads unknown.
|
|
|
|
|
//
|
|
|
|
|
// The window is capped by retention, not by choice: client_logs.pb.js trims to 14
|
|
|
|
|
// days on a nightly cron, so nothing older exists to graph.
|
|
|
|
|
routerAdd("GET", "/api/status/errors", (e) => {
|
|
|
|
|
let days = parseInt(e.request.url.query().get("days") || "14", 10)
|
|
|
|
|
if (!(days > 0) || days > 14) days = 14 // 14 = the retention ceiling
|
|
|
|
|
|
|
|
|
|
const ROW_CAP = 5000
|
|
|
|
|
const cutoffMs = Date.now() - days * 24 * 3600 * 1000
|
|
|
|
|
const cutoff = new DateTime(
|
|
|
|
|
new Date(cutoffMs).toISOString().replace("T", " ").substring(0, 19)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
let rows = []
|
|
|
|
|
try {
|
|
|
|
|
rows = $app.findRecordsByFilter(
|
|
|
|
|
"client_logs", "created >= {:cutoff}", "-created", ROW_CAP, 0, { cutoff: cutoff }
|
|
|
|
|
)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return e.json(500, { error: String(err && err.message ? err.message : err) })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isError = (r) => {
|
|
|
|
|
const kind = (r.getString("kind") || "").toLowerCase()
|
|
|
|
|
const ev = r.getString("event") || ""
|
|
|
|
|
return kind === "error" || ev === "uncaught" || ev === "player_error" || ev === "exo_error"
|
|
|
|
|
}
|
Stop choosing an update variant, and drop the 18+ controls
Amber ships one build. The manifest hook returned `adult` or `clean`
according to the account's nsfwEnabled flag; it now returns `clean` to
everyone.
The `variant` field itself stays in the response, and that is the
important part: every Amber already installed compares it against its
own and refuses a mismatch SILENTLY - no error, no prompt, the update
simply never appears. Removing the field, or sending anything else,
would strand every one of those installs with nothing to see.
whoami stops returning nsfwEnabled. It was there in case some future
amber-api surface wanted to scope results by it; none was built, and an
account flag nobody reads is what a token-introspection endpoint should
not be handing out. amber-api's copy goes with it - it was parsed into
the user object and never once branched on.
The dashboard loses the 18+ account tile, the variant column, the
per-flavour error table, the 18+ pill, the per-user enable/disable
button and the new-account checkbox. A control that sets a flag nothing
reads is worse than no control.
Releases are now read as newest-per-platform filtered to variant='clean',
rather than newest-per-platform-and-variant. Leftover 18+ rows are still
in the collection and would otherwise have been reported as what the
family is being served.
Those rows and the collection rule that hides them are deliberately left
alone: the rule is what keeps them unreachable, and deleting published
artifacts is not something to do as a side effect of a cleanup.
check-flavor.py is gone. It proved which of two builds an artifact was,
by a marker compiled into the Dart snapshot, because a clean-named
Windows installer once carried the 18+ payload. With one build there is
nothing to tell apart.
2026-08-30 02:09:10 +00:00
|
|
|
const days_ = {}, byVersion = {}, byPlatform = {}, byEvent = {}
|
Dashboard: error history, and which publish owns it
GET /api/status/errors plus a panel: 14 daily bars (records vs errors), then
breakdowns by version, platform and flavour.
**Rate, not raw count, is what attributes errors to a publish.** Versions have
wildly different exposure -- right now 1.1.4+13 has 177 records to 1.1.5+14's 20 --
so raw counts would make whichever build the family used most look worst. Every
row carries total, errors and errors/total, coloured on the rate.
**Flavour is not recorded yet and the panel says so.** TelemetryService sends
appVersion, platform and device, but nothing carries clean-vs-adult. The endpoint
reads a top-level field and then meta.flavor, so the day the app starts
sending it this fills in with no backend change; until then the row reads
'neznámá' with a note explaining why. No app change was made here -- that is left
noted, per the owner.
The window is 14 days because that is the retention ceiling: client_logs.pb.js
trims nightly, so nothing older exists to graph. Every day is pre-seeded so a
missing bar and a zero bar cannot look alike.
Inline SVG rather than a charting library -- this page is served by PocketBase with
no build step, and 14 bars do not justify a dependency. Validated on a clean local
boot: 14 bars, correct axis labels, no crash on empty data.
2026-08-05 19:34:42 +00:00
|
|
|
// Pre-seed every day so the graph has no gaps — a missing bar and a zero bar
|
|
|
|
|
// mean different things and must not look alike.
|
|
|
|
|
for (let i = 0; i < days; i++) {
|
|
|
|
|
const d = new Date(Date.now() - i * 24 * 3600 * 1000).toISOString().substring(0, 10)
|
|
|
|
|
days_[d] = { date: d, total: 0, errors: 0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bump = (bag, key, err) => {
|
|
|
|
|
if (!bag[key]) bag[key] = { key: key, total: 0, errors: 0 }
|
|
|
|
|
bag[key].total++
|
|
|
|
|
if (err) bag[key].errors++
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
const err = isError(r)
|
|
|
|
|
const day = r.getDateTime("created").string().substring(0, 10)
|
|
|
|
|
if (days_[day]) { days_[day].total++; if (err) days_[day].errors++ }
|
|
|
|
|
bump(byVersion, r.getString("appVersion") || "?", err)
|
|
|
|
|
bump(byPlatform, r.getString("platform") || "?", err)
|
|
|
|
|
if (err) {
|
|
|
|
|
const ev = r.getString("event") || "?"
|
|
|
|
|
byEvent[ev] = (byEvent[ev] || 0) + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rate = (o) => Object.assign({}, o, {
|
|
|
|
|
rate: o.total ? Math.round((o.errors / o.total) * 1000) / 10 : 0,
|
|
|
|
|
})
|
|
|
|
|
const list = (bag) => Object.keys(bag).map((k) => rate(bag[k]))
|
|
|
|
|
.sort((a, b) => b.total - a.total)
|
|
|
|
|
|
|
|
|
|
return e.json(200, {
|
|
|
|
|
window: days,
|
|
|
|
|
truncated: rows.length >= ROW_CAP,
|
|
|
|
|
sampled: rows.length,
|
|
|
|
|
// Oldest first, so the graph reads left-to-right as time.
|
|
|
|
|
days: Object.keys(days_).sort().map((k) => days_[k]),
|
|
|
|
|
byVersion: list(byVersion),
|
|
|
|
|
byPlatform: list(byPlatform),
|
|
|
|
|
topErrorEvents: Object.keys(byEvent).sort((a, b) => byEvent[b] - byEvent[a])
|
|
|
|
|
.slice(0, 8).map((k) => ({ key: k, errors: byEvent[k] })),
|
|
|
|
|
})
|
|
|
|
|
}, $apis.requireSuperuserAuth())
|
|
|
|
|
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
// Container logs for one service, proxied so the Coolify token stays server-side.
|
|
|
|
|
//
|
|
|
|
|
// Runtime logs ARE available from the Coolify API — /api/v1/applications/<uuid>/logs
|
|
|
|
|
// returns live container stdout. An earlier design assumed this needed SSH to the
|
|
|
|
|
// VPS; it does not, which is how the cinema-rip bug was found in amber-api's log.
|
|
|
|
|
routerAdd("GET", "/api/status/logs", (e) => {
|
|
|
|
|
const app = e.request.url.query().get("app")
|
|
|
|
|
if (!app || !/^[a-z0-9]{10,40}$/.test(app)) {
|
|
|
|
|
return e.json(400, { error: "app must be a Coolify application uuid" })
|
|
|
|
|
}
|
|
|
|
|
let lines = parseInt(e.request.url.query().get("lines") || "200", 10)
|
|
|
|
|
if (!(lines > 0) || lines > 1000) lines = 200
|
|
|
|
|
|
|
|
|
|
const base = $os.getenv("COOLIFY_URL")
|
2026-08-05 19:28:28 +00:00
|
|
|
const token = $os.getenv("COOLIFY_READ_TOKEN")
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
if (!base || !token) {
|
2026-08-05 19:28:28 +00:00
|
|
|
return e.json(503, { error: "COOLIFY_URL / COOLIFY_READ_TOKEN not set on this container" })
|
Operator status dashboard at /status.html
Answering "is Amber healthy?" meant opening Coolify, clicking per-app pages for
status, clicking again for logs, then querying PocketBase separately for what the
family is actually being served. This puts the five questions asked when something
breaks on one screen: is it up, did the last deploy succeed, what did it say, are
clients erroring, and is the newest release the one I think it is.
pb_hooks/status.pb.js serves GET /api/status and GET /api/status/logs, both
superuser-gated. Superuser rather than a user flag because there is no operator
field on `users` and inventing one needs a migration; nsfwEnabled was considered
and rejected, since using a content flag as an admin flag would let any
adult-enabled family member read infrastructure logs.
**This is the first hook in the project to make an outbound HTTP request** -- every
prior one read only PocketBase's own database. So each external call is wrapped to
degrade one panel instead of failing the response, and carries a short timeout so a
hung upstream cannot pin a worker. The Coolify token never reaches the browser;
that is the entire reason the endpoint exists rather than the page calling Coolify
directly.
Runtime container logs come from the Coolify API, not SSH. An earlier design
assumed otherwise and asked for VPS root; it is not needed.
Validated against the project's own pinned PocketBase 0.39.6 before committing,
because a broken pb_hooks file crashes PB at boot and would take family logins
with it: hooks load with no JS errors, both routes 401 unauthenticated, /api/status
executes every panel, the degradation path reports actionable "not configured"
messages, and with real credentials it enumerates all seven Amber services and
returns live amber-api vitals. The page was driven in a browser -- login, panels,
and a log fetch returning 1812 chars of real container output.
Two harness bugs found on the way, both mine: `--publicDir` does not exist in
0.39.6 (pb_public is resolved beside the executable), and `pkill -f "pocketbase
serve"` matches the pattern inside its own command line and killed the shell.
2026-08-05 19:17:05 +00:00
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = $http.send({
|
|
|
|
|
url: base.replace(/\/+$/, "") + "/api/v1/applications/" + app + "/logs?lines=" + lines,
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: { Authorization: "Bearer " + token },
|
|
|
|
|
timeout: 15,
|
|
|
|
|
})
|
|
|
|
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
|
|
|
return e.json(502, { error: "coolify HTTP " + res.statusCode })
|
|
|
|
|
}
|
|
|
|
|
return e.json(200, { app: app, lines: lines, logs: (res.json && res.json.logs) || "" })
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return e.json(502, { error: String(err && err.message ? err.message : err) })
|
|
|
|
|
}
|
|
|
|
|
}, $apis.requireSuperuserAuth())
|