Skip to content

feat(admin): manage OpenMapX instances through the admin panel - #692

Draft
Medformatik wants to merge 110 commits into
mainfrom
feat/admin-instance-management-parity
Draft

Medformatik wants to merge 110 commits into
mainfrom
feat/admin-instance-management-parity

Conversation

@Medformatik

@Medformatik Medformatik commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

What changed and why

The administration panel previously exposed incomplete host operations and left routine instance management to the CLI. This brings supported administration into the UI, with durable jobs and recovery before expanding coverage.

  • Services: truthful capability and outage states, prepared lifecycle/update workflows, presets, configuration/secrets, platform settings, release management and configurable logs.
  • Data: awaited and cancelable workflows, reversible transit sources, reviewed paired catalog pins, masked feed credentials, readiness inventory and bounded prepared-artifact transfer.
  • Accounts and integrations: effective account policy, invitation/recovery and last-admin safeguards; binding reload, configuration migration and standalone extension adoption/removal.
  • Operations: backup retention/prune, scoped cache previews and complete assisted privacy-case workflows.
  • Reliability: retained original execution identities, partial-failure outcomes, source ownership and atomic extension bookkeeping across retries and process death.

Initial host installation, offline control-plane recovery and destructive whole-stack shutdown remain documented host operations.

Verification

  • All 32 workspace type tasks pass; full policy and generated API/SDK freshness checks pass.
  • Focused API/CLI/web/core/agent checks and real migrated PostgreSQL workflows pass, including native extension-worker death and original-job recovery.
  • Full-suite node/web/mobile, three database groups, Linux user/mount-layout tests and production builds are enforced by the PR’s hosted CI.
  • Production bundles and rendered administration views were inspected locally. External Docker mutation/provider/email delivery acceptance remains environment-specific.

Release and upgrade impact

Product impact: breaking, migration, operator-action

Apply the normal API migrations and upgrade the control services together. Preserve original installation keys and unresolved job records. Private host-control directory prerequisites and reserved community configuration names are documented in the release fragments. Changesets cover the public extension contracts and rebuilt extension CLI.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment on lines +16 to +49
scoped.addHook("onRequest", async (request, reply) => {
const candidate = createHash("sha256")
.update(request.headers.authorization ?? "")
.digest();
const expected = createHash("sha256").update(`Bearer ${options.apiToken}`).digest();
if (!timingSafeEqual(candidate, expected))
return reply.code(401).send({ error: "Artifact stream authentication required" });
if (options.admissionsOpen?.() === false)
return reply.code(503).send({ error: "Artifact transfer admission is draining" });
try {
const capability = verifyArtifactStreamCapability(
options.apiToken,
String(request.headers["x-openmapx-artifact-capability"] ?? ""),
);
const params = request.params as { stageId: string };
if (
capability.stageId !== params.stageId ||
capability.direction !== (request.method === "PUT" ? "upload" : "download")
)
throw Error();
await options.store.getStage({
stageId: capability.stageId,
ownerId: capability.ownerId,
purpose: capability.purpose,
});
} catch (error) {
return reply.code(error instanceof ArtifactTransferError ? error.statusCode : 403).send({
error:
error instanceof ArtifactTransferError
? error.message
: "Artifact stream capability refused",
});
}
});
Comment on lines +71 to +125
async (request, reply) => {
if (
request.headers["content-type"]?.split(";")[0].trim().toLowerCase() !==
"application/octet-stream"
)
return reply
.code(415)
.send({ error: "Artifact upload requires application/octet-stream" });
const capability = verifyArtifactStreamCapability(
options.apiToken,
String(request.headers["x-openmapx-artifact-capability"]),
);
const abort = new AbortController();
const disconnected = () => {
if (!reply.raw.writableFinished) abort.abort();
};
request.raw.on("aborted", disconnected);
reply.raw.on("close", disconnected);
try {
const stage = await options.store.getStage({
stageId: capability.stageId,
ownerId: capability.ownerId,
purpose: capability.purpose,
});
if (stage.bytes > capability.byteCap)
throw new ArtifactTransferError(413, "Artifact stream exceeds capability limit");
const source = request.body as Readable;
const bounded = async function* () {
let bytes = 0;
for await (const chunk of source) {
bytes += chunk.length;
if (bytes > capability.byteCap)
throw new ArtifactTransferError(413, "Artifact stream exceeds capability limit");
yield chunk;
}
};
const contentLength =
request.headers["content-length"] === undefined
? undefined
: Number(request.headers["content-length"]);
const result = await options.store.upload({
...capability,
body: Readable.toWeb(Readable.from(bounded())) as ReadableStream<Uint8Array>,
contentLength,
signal: abort.signal,
});
return reply
.header("Cache-Control", "no-store")
.header("X-Content-Type-Options", "nosniff")
.send(result);
} finally {
request.raw.off("aborted", disconnected);
reply.raw.off("close", disconnected);
}
},
Comment on lines +130 to +161
async (request, reply) => {
const capability = verifyArtifactStreamCapability(
options.apiToken,
String(request.headers["x-openmapx-artifact-capability"]),
);
const abort = new AbortController();
const disconnected = () => {
if (!reply.raw.writableFinished) abort.abort();
};
reply.raw.on("close", disconnected);
try {
await options.store.openDownload(
{ ...capability, signal: abort.signal },
async ({ body, headers, signal }) => {
if (Number(headers.get("Content-Length")) > capability.byteCap)
throw new ArtifactTransferError(413, "Artifact stream exceeds capability limit");
headers.forEach((value, name) => {
reply.header(name, value);
});
reply.hijack();
reply.raw.writeHead(200, Object.fromEntries(headers.entries()));
await pipeline(
Readable.fromWeb(body as import("node:stream/web").ReadableStream<Uint8Array>),
reply.raw,
{ signal },
);
},
);
} finally {
reply.raw.off("close", disconnected);
}
},
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants