Files
Primora/apps/cli/src/commands/jobs.ts
T
Tomáš DvořákandDevin d44bbcceda
CI / Backend Tests (push) Canceled after 0s
CI / Frontend Tests (push) Canceled after 0s
CI / Build All Services (push) Canceled after 0s
CI / Docker Build (auth) (push) Canceled after 0s
CI / Docker Build (backend) (push) Canceled after 0s
CI / Docker Build (frontend) (push) Canceled after 0s
CI / Publish edge images (auth) (push) Canceled after 0s
CI / Publish edge images (backend) (push) Canceled after 0s
CI / Publish edge images (frontend) (push) Canceled after 0s
CI / Migration Up/Down (push) Canceled after 0s
CI / Compose E2E Smoke (push) Canceled after 0s
CI / Lint (push) Canceled after 0s
fix: QA hardening across desktop connect, CLI, setup, and PWA update flow (#52)
- desktop: probe-then-persist server URL — scheme-less input tries https
  then http; a failed probe no longer saves a dead URL that would trap
  the next launch in a redirect to a broken address
- frontend: close the create-collection modal on successful submit
  (backdrop stayed mounted and intercepted navigation)
- frontend: reload once on service-worker controllerchange so returning
  users don't run a stale cached shell after a deploy
- frontend: gate dashboard empty states on resource loading to stop the
  "No project selected" / "Create your workspace" flash
- cli: resolve scheduled-job refs by name or id for jobs:run, jobs:runs,
  and jobs:rm, matching bucket/org resolution
- cli: correct stale pk_live_/pk_test_ help text to the real prm_ format;
  align README command spellings and demo seed prefixes
- setup: rewrite all public URL vars (auth, vite, backend) and cookie
  domain for the entered host — https for domains, http for IPs — with
  anchored full-line substitutions so stale ports/paths can't survive
- docs: VITE_* vars never reach the frontend image build; the app
  resolves API/auth URLs same-origin at runtime, so host changes need no
  rebuild — noted HTTPS requirement for PWA/secure-context features

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-03 18:52:53 +02:00

153 lines
4.2 KiB
TypeScript

import type { ScheduledJob, ScheduledJobRun } from "@primora/api-client";
import { AutomationService } from "@primora/api-client";
import { loadConfig } from "../config.js";
import { configureClient, requireAuth, requireProject, resolveJobId } from "../http.js";
import { formatDate, isJson, printJson, printTable, success } from "../out.js";
interface JobOptions {
project?: string;
json?: boolean;
}
function jobRow(job: ScheduledJob): string[] {
return [
job.name,
job.schedule,
job.url,
job.enabled ? "on" : "off",
job.last_status ?? "—",
job.last_run_at ? formatDate(job.last_run_at) : "never",
job.enabled && job.next_run_at ? formatDate(job.next_run_at) : "—",
job.id,
];
}
export async function cmdJobsList(options: JobOptions): Promise<void> {
const cfg = loadConfig();
requireAuth(cfg);
configureClient(cfg);
const projectId = requireProject(cfg, options.project);
const page = await AutomationService.listScheduledJobs({ projectId });
if (isJson(options)) {
printJson(page);
} else {
printTable(
["NAME", "SCHEDULE", "URL", "ENABLED", "LAST", "LAST RUN", "NEXT RUN", "ID"],
page.items.map(jobRow),
);
}
}
export async function cmdJobsCreate(
name: string,
options: JobOptions & {
schedule?: string;
url?: string;
payload?: string;
secret?: string;
disabled?: boolean;
},
): Promise<void> {
const cfg = loadConfig();
requireAuth(cfg);
configureClient(cfg);
if (!options.schedule) throw new Error("pass --schedule, e.g. '*/15 * * * *' or '@every 1h'");
if (!options.url) throw new Error("pass --url <https endpoint>");
let payload: Record<string, unknown> | undefined;
if (options.payload) {
try {
payload = JSON.parse(options.payload) as Record<string, unknown>;
} catch {
throw new Error("--payload must be valid JSON");
}
}
const projectId = requireProject(cfg, options.project);
const job = await AutomationService.createScheduledJob({
projectId,
requestBody: {
name,
schedule: options.schedule,
url: options.url,
secret: options.secret || undefined,
payload,
enabled: !options.disabled,
},
});
if (isJson(options)) {
printJson(job);
return;
}
success(`created ${job.name} (${job.id})`);
if (job.secret) {
process.stderr.write(`signing secret (shown once): ${job.secret}\n`);
}
}
export async function cmdJobsRemove(jobRef: string, options: JobOptions & { yes?: boolean }): Promise<void> {
const cfg = loadConfig();
requireAuth(cfg);
configureClient(cfg);
const projectId = requireProject(cfg, options.project);
const jobId = await resolveJobId(projectId, jobRef);
await AutomationService.deleteScheduledJob({ projectId, jobId });
if (isJson(options)) {
printJson({ deleted: jobId });
} else {
success(`deleted ${jobId}`);
}
}
export async function cmdJobsRun(jobRef: string, options: JobOptions): Promise<void> {
const cfg = loadConfig();
requireAuth(cfg);
configureClient(cfg);
const projectId = requireProject(cfg, options.project);
const jobId = await resolveJobId(projectId, jobRef);
const run = await AutomationService.runScheduledJob({ projectId, jobId });
if (isJson(options)) {
printJson(run);
} else {
success(`run queued (${run.id}) — status ${run.status}`);
}
}
function runRow(run: ScheduledJobRun): string[] {
return [
formatDate(run.started_at),
run.status,
run.triggered_by,
run.status_code != null ? String(run.status_code) : "—",
run.duration_ms != null ? `${run.duration_ms}ms` : "—",
run.error || "—",
];
}
export async function cmdJobsRuns(
jobRef: string,
options: JobOptions & { limit?: string },
): Promise<void> {
const cfg = loadConfig();
requireAuth(cfg);
configureClient(cfg);
const projectId = requireProject(cfg, options.project);
const jobId = await resolveJobId(projectId, jobRef);
const page = await AutomationService.listScheduledJobRuns({
projectId,
jobId,
limit: options.limit ? Number(options.limit) : 50,
});
if (isJson(options)) {
printJson(page);
} else {
printTable(["STARTED", "STATUS", "TRIGGER", "HTTP", "DURATION", "ERROR"], page.items.map(runRow));
}
}