diff --git a/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx b/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx new file mode 100644 index 0000000000..a8b5b1fbb9 --- /dev/null +++ b/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx @@ -0,0 +1,139 @@ +--- +title: Unattended scheduling & failure alerting +description: Run the miner's scheduled commands -- manage poll and discover -- unattended on cron or systemd, and alert reliably when a run fails. +--- + +Operational guidance for running the miner's scheduled commands — `manage poll` and `discover` — +unattended on a timer (cron or systemd), and for alerting when a run fails. These are the two +commands most likely to run on a schedule; everything they need is local and they make no +interactive prompts. + + + Scope: scheduling + failure alerting for `manage poll` / `discover`. For local-state recovery + see the [AMS operations runbook](/docs/ams-operations-runbook); for deployment layout see the + [AMS deployment guide](/docs/ams-deployment). + + +## The exit-code contract (what to alert on) + +Both commands follow the same convention, so any scheduler can detect a failed run from the exit +code: + + + +For scheduled runs, two flags matter: + +- `--no-update-check` (or `LOOPOVER_MINER_NO_UPDATE_CHECK=1`) — skip the npm-registry version + nudge so an unattended run never depends on / prints it. +- `--json` — machine-parseable stdout, so an alert handler can attach the structured output. + +## cron + + + +Two cron facts to get right here: + + + cron emails whatever a job writes to stdout/stderr to `MAILTO` — it does not send a message + "because" the exit code was non-zero. A job that fails *silently* (non-zero exit, no output) + produces no mail, so don't rely on `MAILTO` alone as the failure signal. + + + + `logger` succeeds (exit 0), so `cmd || logger …` makes the whole cron job exit 0 — any + exit-status-based monitoring then sees success. Capture the code first (`status=$?`) and + re-raise it (`exit "$status"`) as shown, so the real failing code survives. + + +## systemd (service + timer) + +A `oneshot` service plus a timer is the more observable option: `systemctl status` / +`journalctl` capture each run, and `OnFailure=` is a first-class alerting hook. + +`loopover-miner-discover.service`: + + + +`loopover-miner-discover.timer`: + + + +Enable with `systemctl enable --now loopover-miner-discover.timer`. + +## Alerting on failure + +Every option keys on the same exit-code contract (`2` = failure). + +**cron.** Append `|| { status=$?; ; exit "$status"; }` (as in the cron example +above) — capture `$?` before the alert command runs and re-raise it, so the failure isn't masked. +Substitute `logger` with a webhook `curl`, a PagerDuty/Slack CLI, etc. (`MAILTO` still mails any +output, but is not a reliable signal for a silent failure — see the cron note above.) + +**systemd.** `OnFailure=loopover-miner-alert@%n.service` runs a templated alert unit on any +non-zero exit. A minimal alert unit: + + + +**Wrapper script.** For any scheduler, wrap the command and preserve its exit code: + + + +Keep `--json` on scheduled runs so the alert handler can forward the structured output; the +human-readable form is for interactive use. diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx index facbf7cc43..6985b1b972 100644 --- a/apps/loopover-ui/src/components/site/docs-nav.tsx +++ b/apps/loopover-ui/src/components/site/docs-nav.tsx @@ -78,6 +78,7 @@ export const docsNav: DocsGroup[] = [ { to: "/docs/ams-deployment", label: "Deployment guide" }, { to: "/docs/ams-operations-runbook", label: "Operations runbook" }, { to: "/docs/ams-observability", label: "Observing your miner" }, + { to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" }, ], }, ], diff --git a/apps/loopover-ui/src/routeTree.gen.ts b/apps/loopover-ui/src/routeTree.gen.ts index c1edde8e3f..ba67b6cf3b 100644 --- a/apps/loopover-ui/src/routeTree.gen.ts +++ b/apps/loopover-ui/src/routeTree.gen.ts @@ -57,6 +57,7 @@ import { Route as DocsGithubAppRouteImport } from './routes/docs.github-app' import { Route as DocsFumadocsSpikeApiReferenceRouteImport } from './routes/docs.fumadocs-spike-api-reference' import { Route as DocsBranchAnalysisRouteImport } from './routes/docs.branch-analysis' import { Route as DocsBetaOnboardingRouteImport } from './routes/docs.beta-onboarding' +import { Route as DocsAmsUnattendedSchedulingRouteImport } from './routes/docs.ams-unattended-scheduling' import { Route as DocsAmsOperationsRunbookRouteImport } from './routes/docs.ams-operations-runbook' import { Route as DocsAmsObservabilityRouteImport } from './routes/docs.ams-observability' import { Route as DocsAmsDeploymentRouteImport } from './routes/docs.ams-deployment' @@ -331,6 +332,12 @@ const DocsBetaOnboardingRoute = DocsBetaOnboardingRouteImport.update({ path: '/beta-onboarding', getParentRoute: () => DocsRoute, } as any) +const DocsAmsUnattendedSchedulingRoute = + DocsAmsUnattendedSchedulingRouteImport.update({ + id: '/ams-unattended-scheduling', + path: '/ams-unattended-scheduling', + getParentRoute: () => DocsRoute, + } as any) const DocsAmsOperationsRunbookRoute = DocsAmsOperationsRunbookRouteImport.update({ id: '/ams-operations-runbook', @@ -457,6 +464,7 @@ export interface FileRoutesByFullPath { '/docs/ams-deployment': typeof DocsAmsDeploymentRoute '/docs/ams-observability': typeof DocsAmsObservabilityRoute '/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute + '/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute '/docs/beta-onboarding': typeof DocsBetaOnboardingRoute '/docs/branch-analysis': typeof DocsBranchAnalysisRoute '/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute @@ -523,6 +531,7 @@ export interface FileRoutesByTo { '/docs/ams-deployment': typeof DocsAmsDeploymentRoute '/docs/ams-observability': typeof DocsAmsObservabilityRoute '/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute + '/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute '/docs/beta-onboarding': typeof DocsBetaOnboardingRoute '/docs/branch-analysis': typeof DocsBranchAnalysisRoute '/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute @@ -593,6 +602,7 @@ export interface FileRoutesById { '/docs/ams-deployment': typeof DocsAmsDeploymentRoute '/docs/ams-observability': typeof DocsAmsObservabilityRoute '/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute + '/docs/ams-unattended-scheduling': typeof DocsAmsUnattendedSchedulingRoute '/docs/beta-onboarding': typeof DocsBetaOnboardingRoute '/docs/branch-analysis': typeof DocsBranchAnalysisRoute '/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute @@ -664,6 +674,7 @@ export interface FileRouteTypes { | '/docs/ams-deployment' | '/docs/ams-observability' | '/docs/ams-operations-runbook' + | '/docs/ams-unattended-scheduling' | '/docs/beta-onboarding' | '/docs/branch-analysis' | '/docs/fumadocs-spike-api-reference' @@ -730,6 +741,7 @@ export interface FileRouteTypes { | '/docs/ams-deployment' | '/docs/ams-observability' | '/docs/ams-operations-runbook' + | '/docs/ams-unattended-scheduling' | '/docs/beta-onboarding' | '/docs/branch-analysis' | '/docs/fumadocs-spike-api-reference' @@ -799,6 +811,7 @@ export interface FileRouteTypes { | '/docs/ams-deployment' | '/docs/ams-observability' | '/docs/ams-operations-runbook' + | '/docs/ams-unattended-scheduling' | '/docs/beta-onboarding' | '/docs/branch-analysis' | '/docs/fumadocs-spike-api-reference' @@ -1192,6 +1205,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsBetaOnboardingRouteImport parentRoute: typeof DocsRoute } + '/docs/ams-unattended-scheduling': { + id: '/docs/ams-unattended-scheduling' + path: '/ams-unattended-scheduling' + fullPath: '/docs/ams-unattended-scheduling' + preLoaderRoute: typeof DocsAmsUnattendedSchedulingRouteImport + parentRoute: typeof DocsRoute + } '/docs/ams-operations-runbook': { id: '/docs/ams-operations-runbook' path: '/ams-operations-runbook' @@ -1381,6 +1401,7 @@ interface DocsRouteChildren { DocsAmsDeploymentRoute: typeof DocsAmsDeploymentRoute DocsAmsObservabilityRoute: typeof DocsAmsObservabilityRoute DocsAmsOperationsRunbookRoute: typeof DocsAmsOperationsRunbookRoute + DocsAmsUnattendedSchedulingRoute: typeof DocsAmsUnattendedSchedulingRoute DocsBetaOnboardingRoute: typeof DocsBetaOnboardingRoute DocsBranchAnalysisRoute: typeof DocsBranchAnalysisRoute DocsFumadocsSpikeApiReferenceRoute: typeof DocsFumadocsSpikeApiReferenceRoute @@ -1424,6 +1445,7 @@ const DocsRouteChildren: DocsRouteChildren = { DocsAmsDeploymentRoute: DocsAmsDeploymentRoute, DocsAmsObservabilityRoute: DocsAmsObservabilityRoute, DocsAmsOperationsRunbookRoute: DocsAmsOperationsRunbookRoute, + DocsAmsUnattendedSchedulingRoute: DocsAmsUnattendedSchedulingRoute, DocsBetaOnboardingRoute: DocsBetaOnboardingRoute, DocsBranchAnalysisRoute: DocsBranchAnalysisRoute, DocsFumadocsSpikeApiReferenceRoute: DocsFumadocsSpikeApiReferenceRoute, diff --git a/apps/loopover-ui/src/routes/docs.ams-unattended-scheduling.tsx b/apps/loopover-ui/src/routes/docs.ams-unattended-scheduling.tsx new file mode 100644 index 0000000000..681e913dbe --- /dev/null +++ b/apps/loopover-ui/src/routes/docs.ams-unattended-scheduling.tsx @@ -0,0 +1,49 @@ +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { Suspense } from "react"; + +import { DocsPage } from "@/components/site/docs-page"; +import { docsClientLoader } from "@/lib/docs-client-loader"; + +// Rendered from content/docs/ams-unattended-scheduling.mdx via fumadocs-mdx's browser entry +// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow +// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment +// for why the loader below resolves only a plain, serializable path string. +export const Route = createFileRoute("/docs/ams-unattended-scheduling")({ + loader: async () => { + const { docsSource } = await import("@/lib/docs-source"); + const page = docsSource.getPage(["ams-unattended-scheduling"]); + if (!page) throw notFound(); + return { path: page.path, title: page.data.title, description: page.data.description }; + }, + head: () => ({ + meta: [ + { title: "Unattended scheduling & failure alerting — LoopOver docs" }, + { + name: "description", + content: + "Run the miner's scheduled commands -- manage poll and discover -- unattended on cron or systemd, and alert reliably when a run fails.", + }, + { property: "og:title", content: "Unattended scheduling & failure alerting — LoopOver docs" }, + { + property: "og:description", + content: + "Run the miner's scheduled commands -- manage poll and discover -- unattended on cron or systemd, and alert reliably when a run fails.", + }, + { property: "og:url", content: "/docs/ams-unattended-scheduling" }, + ], + links: [{ rel: "canonical", href: "/docs/ams-unattended-scheduling" }], + }), + component: AmsUnattendedScheduling, +}); + +function AmsUnattendedScheduling() { + const { path, title, description } = Route.useLoaderData(); + const Content = docsClientLoader.getComponent(path); + return ( + + Loading…

}> + +
+
+ ); +} diff --git a/apps/loopover-ui/src/routes/docs.index.tsx b/apps/loopover-ui/src/routes/docs.index.tsx index 5834681179..3d402be49e 100644 --- a/apps/loopover-ui/src/routes/docs.index.tsx +++ b/apps/loopover-ui/src/routes/docs.index.tsx @@ -76,6 +76,7 @@ const AUDIENCES: Audience[] = [ { to: "/docs/ams-deployment", label: "AMS deployment guide" }, { to: "/docs/ams-operations-runbook", label: "AMS operations runbook" }, { to: "/docs/ams-observability", label: "Observing your miner" }, + { to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" }, { to: "/docs/self-hosting-docs-audit", label: "Self-host docs audit" }, { to: "/docs/maintainer-install-trust", label: "Install & trust guide" }, { to: "/docs/github-app", label: "GitHub App configuration" }, diff --git a/packages/loopover-miner/docs/unattended-scheduling.md b/packages/loopover-miner/docs/unattended-scheduling.md index cf9f9ca513..589551bb52 100644 --- a/packages/loopover-miner/docs/unattended-scheduling.md +++ b/packages/loopover-miner/docs/unattended-scheduling.md @@ -1,5 +1,10 @@ # loopover-miner — unattended scheduling & failure alerting +> Also published on the docs website: [Unattended scheduling & failure +> alerting](https://loopover.ai/docs/ams-unattended-scheduling) (same content, rendered with +> search and the rest of the maintainer docs nav). This file remains the canonical source and +> ships inside the published `@loopover/miner` package. + Operational guidance for running the miner's scheduled commands — `manage poll` and `discover` — unattended on a timer (cron or systemd), and for alerting when a run fails. These are the two commands most likely to run on a schedule; everything they need is local and they make no interactive prompts.