@connextar/house 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -1
- package/dist/ai/config-index.d.ts +14 -0
- package/dist/ai/config-index.d.ts.map +1 -0
- package/dist/ai/config-index.js +14 -0
- package/dist/ai/config-index.js.map +1 -0
- package/dist/ai/images.d.ts +14 -0
- package/dist/ai/images.d.ts.map +1 -0
- package/dist/ai/images.js +10 -0
- package/dist/ai/images.js.map +1 -0
- package/dist/ai/index.d.ts +7 -3
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +7 -3
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/provider.d.ts +1 -9
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +0 -5
- package/dist/ai/provider.js.map +1 -1
- package/dist/jobs/counts.d.ts +82 -0
- package/dist/jobs/counts.d.ts.map +1 -0
- package/dist/jobs/counts.js +112 -0
- package/dist/jobs/counts.js.map +1 -0
- package/dist/jobs/cron-auth.d.ts +44 -0
- package/dist/jobs/cron-auth.d.ts.map +1 -0
- package/dist/jobs/cron-auth.js +20 -0
- package/dist/jobs/cron-auth.js.map +1 -0
- package/dist/jobs/health.d.ts +157 -0
- package/dist/jobs/health.d.ts.map +1 -0
- package/dist/jobs/health.js +146 -0
- package/dist/jobs/health.js.map +1 -0
- package/dist/jobs/index.d.ts +26 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +26 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/job-health-list.d.ts +66 -0
- package/dist/jobs/job-health-list.d.ts.map +1 -0
- package/dist/jobs/job-health-list.js +115 -0
- package/dist/jobs/job-health-list.js.map +1 -0
- package/dist/jobs/react-index.d.ts +19 -0
- package/dist/jobs/react-index.d.ts.map +1 -0
- package/dist/jobs/react-index.js +18 -0
- package/dist/jobs/react-index.js.map +1 -0
- package/dist/jobs/registry.d.ts +104 -0
- package/dist/jobs/registry.d.ts.map +1 -0
- package/dist/jobs/registry.js +72 -0
- package/dist/jobs/registry.js.map +1 -0
- package/dist/jobs/scheduler.d.ts +89 -0
- package/dist/jobs/scheduler.d.ts.map +1 -0
- package/dist/jobs/scheduler.js +282 -0
- package/dist/jobs/scheduler.js.map +1 -0
- package/dist/jobs/store.d.ts +94 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +56 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/jobs/windows.d.ts +66 -0
- package/dist/jobs/windows.d.ts.map +1 -0
- package/dist/jobs/windows.js +130 -0
- package/dist/jobs/windows.js.map +1 -0
- package/package.json +13 -1
package/dist/ai/provider.js
CHANGED
|
@@ -58,11 +58,6 @@ export class AiUnusableAnswerError extends AiUnavailableError {
|
|
|
58
58
|
this.usage = usage;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
/** The image types both providers accept inline. */
|
|
62
|
-
export const AI_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
63
|
-
export function isAiImageType(value) {
|
|
64
|
-
return AI_IMAGE_TYPES.includes(value);
|
|
65
|
-
}
|
|
66
61
|
/**
|
|
67
62
|
* An OpenAI reasoning model bills its thinking against the same budget as its
|
|
68
63
|
* answer, so a 900-token ceiling can be spent entirely before the prose starts.
|
package/dist/ai/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,OAAO,EACL,QAAQ,EACR,eAAe,EACf,cAAc,EACd,SAAS,GAOV,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run did, in words a person reads.
|
|
3
|
+
*
|
|
4
|
+
* Both apps stored a job's result as `Record<string, number>` and printed it
|
|
5
|
+
* key by key, so production showed "0 chargesRaised · 0 totalOutstanding" —
|
|
6
|
+
* a variable name where a sentence belonged, and an amount of money printed as
|
|
7
|
+
* if it were a number of things. The fix is not a better printer. It is that a
|
|
8
|
+
* count cannot reach the screen without having been declared: every key has
|
|
9
|
+
* the words that follow its number, and a kind that says how the number is
|
|
10
|
+
* written. An undeclared key is not shown at all, so there is no fallback for
|
|
11
|
+
* a raw name to slip through.
|
|
12
|
+
*
|
|
13
|
+
* No imports: this runs in a server component, a route and a client card alike.
|
|
14
|
+
*/
|
|
15
|
+
export type CountKind = "count" | "money" | "duration";
|
|
16
|
+
/** The words after the number: one form, or a singular and a plural. */
|
|
17
|
+
export type CountLabel = string | {
|
|
18
|
+
one: string;
|
|
19
|
+
other: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* One thing a job reports. The label is what follows the number —
|
|
23
|
+
* "reminders sent", so the line reads "3 reminders sent".
|
|
24
|
+
*
|
|
25
|
+
* Money says its currency **and its unit**, because the two apps already
|
|
26
|
+
* disagree: AltEd's `totalOutstanding` is pounds, summed from a decimal column,
|
|
27
|
+
* while a new job would count pence. A declaration that left the unit to a
|
|
28
|
+
* default would be out by a factor of a hundred for one of them.
|
|
29
|
+
*/
|
|
30
|
+
export type CountDeclaration = {
|
|
31
|
+
label: CountLabel;
|
|
32
|
+
kind?: "count";
|
|
33
|
+
} | {
|
|
34
|
+
label: CountLabel;
|
|
35
|
+
kind: "money";
|
|
36
|
+
currency: string;
|
|
37
|
+
unit: "minor" | "major";
|
|
38
|
+
} | {
|
|
39
|
+
label: CountLabel;
|
|
40
|
+
kind: "duration";
|
|
41
|
+
unit: "ms" | "s";
|
|
42
|
+
};
|
|
43
|
+
export type CountDeclarations = Readonly<Record<string, CountDeclaration>>;
|
|
44
|
+
/** What a job's `run` resolves with: a number for every key it declared. */
|
|
45
|
+
export type CountValues<C extends CountDeclarations> = {
|
|
46
|
+
[K in keyof C]: number;
|
|
47
|
+
};
|
|
48
|
+
/** One count, ready to show. `text` is the whole phrase: "£1,204.50 outstanding". */
|
|
49
|
+
export interface CountLine {
|
|
50
|
+
key: string;
|
|
51
|
+
kind: CountKind;
|
|
52
|
+
value: number;
|
|
53
|
+
text: string;
|
|
54
|
+
}
|
|
55
|
+
export interface CountFormatOptions {
|
|
56
|
+
/** BCP 47 locale for numbers and currency. Default `en-GB`. */
|
|
57
|
+
locale?: string;
|
|
58
|
+
}
|
|
59
|
+
/** An amount of money in its currency's own style. Float noise (12.300000000000001) is rounded away here. */
|
|
60
|
+
export declare function formatMoney(value: number, currency: string, unit: "minor" | "major", locale?: string): string;
|
|
61
|
+
/** How long something took, compactly: "450 ms", "2.5 s", "2 min 5 s", "1 h 3 min", "2 days 4 h". */
|
|
62
|
+
export declare function formatDuration(ms: number, locale?: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* A threshold in the unit a person would say it in: "26 hours", "8 days",
|
|
65
|
+
* "90 minutes". For "no successful run in over 26 hours", where "1 day 2 h"
|
|
66
|
+
* would read as a stopwatch.
|
|
67
|
+
*/
|
|
68
|
+
export declare function describeSpan(ms: number): string;
|
|
69
|
+
/** One declared count as its phrase. */
|
|
70
|
+
export declare function formatCount(value: number, declaration: CountDeclaration, options?: CountFormatOptions): string;
|
|
71
|
+
/**
|
|
72
|
+
* A stored result, read through the job's declarations: in declaration order,
|
|
73
|
+
* each with its phrase. Keys the job no longer declares, and values that are not
|
|
74
|
+
* finite numbers, are left out — a ledger row is JSON someone wrote at another
|
|
75
|
+
* time, so it is repaired rather than trusted.
|
|
76
|
+
*/
|
|
77
|
+
export declare function countLines(declarations: CountDeclarations, stored: unknown, options?: CountFormatOptions): CountLine[];
|
|
78
|
+
/** Only the declared, finite counts — what the dispatcher writes to the ledger. */
|
|
79
|
+
export declare function declaredCounts(declarations: CountDeclarations, returned: unknown): Record<string, number>;
|
|
80
|
+
/** The phrases joined for one line: "3 reminders sent · £1,204.50 outstanding". */
|
|
81
|
+
export declare function countsSummary(lines: readonly CountLine[], separator?: string): string;
|
|
82
|
+
//# sourceMappingURL=counts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counts.d.ts","sourceRoot":"","sources":["../../src/jobs/counts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAAC;AAEvD,wEAAwE;AACxE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;CAAE,GAC/E;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,IAAI,GAAG,GAAG,CAAA;CAAE,CAAC;AAE9D,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAE3E,4EAA4E;AAC5E,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,iBAAiB,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM;CAAE,CAAC;AAElF,qFAAqF;AACrF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AASD,6GAA6G;AAC7G,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,EAAE,MAAM,SAAiB,UAI5G;AAOD,qGAAqG;AACrG,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,SAAiB,GAAG,MAAM,CAc1E;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAM/C;AAED,wCAAwC;AACxC,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,gBAAgB,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAWlH;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,YAAY,EAAE,iBAAiB,EAC/B,MAAM,EAAE,OAAO,EACf,OAAO,GAAE,kBAAuB,GAC/B,SAAS,EAAE,CAQb;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,YAAY,EAAE,iBAAiB,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASzG;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,EAAE,SAAS,SAAQ,GAAG,MAAM,CAEpF"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run did, in words a person reads.
|
|
3
|
+
*
|
|
4
|
+
* Both apps stored a job's result as `Record<string, number>` and printed it
|
|
5
|
+
* key by key, so production showed "0 chargesRaised · 0 totalOutstanding" —
|
|
6
|
+
* a variable name where a sentence belonged, and an amount of money printed as
|
|
7
|
+
* if it were a number of things. The fix is not a better printer. It is that a
|
|
8
|
+
* count cannot reach the screen without having been declared: every key has
|
|
9
|
+
* the words that follow its number, and a kind that says how the number is
|
|
10
|
+
* written. An undeclared key is not shown at all, so there is no fallback for
|
|
11
|
+
* a raw name to slip through.
|
|
12
|
+
*
|
|
13
|
+
* No imports: this runs in a server component, a route and a client card alike.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_LOCALE = "en-GB";
|
|
16
|
+
function labelFor(label, value, locale) {
|
|
17
|
+
if (typeof label === "string")
|
|
18
|
+
return label;
|
|
19
|
+
return new Intl.PluralRules(locale).select(value) === "one" ? label.one : label.other;
|
|
20
|
+
}
|
|
21
|
+
/** An amount of money in its currency's own style. Float noise (12.300000000000001) is rounded away here. */
|
|
22
|
+
export function formatMoney(value, currency, unit, locale = DEFAULT_LOCALE) {
|
|
23
|
+
const format = new Intl.NumberFormat(locale, { style: "currency", currency });
|
|
24
|
+
const digits = format.resolvedOptions().maximumFractionDigits ?? 2;
|
|
25
|
+
return format.format(unit === "minor" ? value / 10 ** digits : value);
|
|
26
|
+
}
|
|
27
|
+
const SECOND = 1000;
|
|
28
|
+
const MINUTE = 60 * SECOND;
|
|
29
|
+
const HOUR = 60 * MINUTE;
|
|
30
|
+
const DAY = 24 * HOUR;
|
|
31
|
+
/** How long something took, compactly: "450 ms", "2.5 s", "2 min 5 s", "1 h 3 min", "2 days 4 h". */
|
|
32
|
+
export function formatDuration(ms, locale = DEFAULT_LOCALE) {
|
|
33
|
+
const n = (value, digits = 0) => new Intl.NumberFormat(locale, { maximumFractionDigits: digits }).format(value);
|
|
34
|
+
const abs = Math.abs(ms);
|
|
35
|
+
if (abs < SECOND)
|
|
36
|
+
return `${n(ms)} ms`;
|
|
37
|
+
if (abs < 10 * SECOND)
|
|
38
|
+
return `${n(ms / SECOND, 1)} s`;
|
|
39
|
+
if (abs < MINUTE)
|
|
40
|
+
return `${n(Math.round(ms / SECOND))} s`;
|
|
41
|
+
const pair = (big, bigUnit, small, smallUnit) => small ? `${n(big)} ${bigUnit} ${n(small)} ${smallUnit}` : `${n(big)} ${bigUnit}`;
|
|
42
|
+
const whole = Math.round(abs / SECOND) * SECOND;
|
|
43
|
+
if (whole < HOUR)
|
|
44
|
+
return pair(Math.floor(whole / MINUTE), "min", Math.round((whole % MINUTE) / SECOND), "s");
|
|
45
|
+
if (whole < DAY)
|
|
46
|
+
return pair(Math.floor(whole / HOUR), "h", Math.round((whole % HOUR) / MINUTE), "min");
|
|
47
|
+
const days = Math.floor(whole / DAY);
|
|
48
|
+
return pair(days, days === 1 ? "day" : "days", Math.round((whole % DAY) / HOUR), "h");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A threshold in the unit a person would say it in: "26 hours", "8 days",
|
|
52
|
+
* "90 minutes". For "no successful run in over 26 hours", where "1 day 2 h"
|
|
53
|
+
* would read as a stopwatch.
|
|
54
|
+
*/
|
|
55
|
+
export function describeSpan(ms) {
|
|
56
|
+
const unit = (value, one, many) => `${value} ${value === 1 ? one : many}`;
|
|
57
|
+
if (ms >= 2 * DAY && ms % DAY === 0)
|
|
58
|
+
return unit(ms / DAY, "day", "days");
|
|
59
|
+
if (ms >= HOUR && ms % HOUR === 0)
|
|
60
|
+
return unit(ms / HOUR, "hour", "hours");
|
|
61
|
+
if (ms >= MINUTE && ms % MINUTE === 0)
|
|
62
|
+
return unit(ms / MINUTE, "minute", "minutes");
|
|
63
|
+
return formatDuration(ms);
|
|
64
|
+
}
|
|
65
|
+
/** One declared count as its phrase. */
|
|
66
|
+
export function formatCount(value, declaration, options = {}) {
|
|
67
|
+
const locale = options.locale ?? DEFAULT_LOCALE;
|
|
68
|
+
const label = labelFor(declaration.label, value, locale);
|
|
69
|
+
switch (declaration.kind) {
|
|
70
|
+
case "money":
|
|
71
|
+
return `${formatMoney(value, declaration.currency, declaration.unit, locale)} ${label}`;
|
|
72
|
+
case "duration":
|
|
73
|
+
return `${formatDuration(declaration.unit === "s" ? value * SECOND : value, locale)} ${label}`;
|
|
74
|
+
default:
|
|
75
|
+
return `${new Intl.NumberFormat(locale).format(value)} ${label}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A stored result, read through the job's declarations: in declaration order,
|
|
80
|
+
* each with its phrase. Keys the job no longer declares, and values that are not
|
|
81
|
+
* finite numbers, are left out — a ledger row is JSON someone wrote at another
|
|
82
|
+
* time, so it is repaired rather than trusted.
|
|
83
|
+
*/
|
|
84
|
+
export function countLines(declarations, stored, options = {}) {
|
|
85
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored))
|
|
86
|
+
return [];
|
|
87
|
+
const values = stored;
|
|
88
|
+
return Object.entries(declarations).flatMap(([key, declaration]) => {
|
|
89
|
+
const value = values[key];
|
|
90
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
91
|
+
return [];
|
|
92
|
+
return [{ key, kind: declaration.kind ?? "count", value, text: formatCount(value, declaration, options) }];
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Only the declared, finite counts — what the dispatcher writes to the ledger. */
|
|
96
|
+
export function declaredCounts(declarations, returned) {
|
|
97
|
+
if (!returned || typeof returned !== "object")
|
|
98
|
+
return {};
|
|
99
|
+
const values = returned;
|
|
100
|
+
const kept = {};
|
|
101
|
+
for (const key of Object.keys(declarations)) {
|
|
102
|
+
const value = values[key];
|
|
103
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
104
|
+
kept[key] = value;
|
|
105
|
+
}
|
|
106
|
+
return kept;
|
|
107
|
+
}
|
|
108
|
+
/** The phrases joined for one line: "3 reminders sent · £1,204.50 outstanding". */
|
|
109
|
+
export function countsSummary(lines, separator = " · ") {
|
|
110
|
+
return lines.map((line) => line.text).join(separator);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=counts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"counts.js","sourceRoot":"","sources":["../../src/jobs/counts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a request may run the scheduled jobs: the shared `CRON_SECRET`, as
|
|
3
|
+
* Vercel's cron sends it (`Authorization: Bearer <secret>`).
|
|
4
|
+
*
|
|
5
|
+
* Both apps wrote this, the same way in substance. The union keeps each one's
|
|
6
|
+
* better half:
|
|
7
|
+
*
|
|
8
|
+
* - **An unset secret refuses everything (503)** rather than running
|
|
9
|
+
* unauthenticated. Both.
|
|
10
|
+
* - **Compared as SHA-256 digests with `timingSafeEqual`** (Quickie). Equal
|
|
11
|
+
* lengths whatever was sent, so the comparison is constant-time and leaks not
|
|
12
|
+
* even the secret's length. AltEd compared the raw bytes after a length check.
|
|
13
|
+
* - **A committed development secret is refused in production** (Quickie). Next
|
|
14
|
+
* loads `.env` in production too, so a host that forgets its own value would
|
|
15
|
+
* otherwise run on the one in the repository.
|
|
16
|
+
* - **No secret in the address unless asked for** (AltEd). `?secret=` lands in
|
|
17
|
+
* every request log on the way; a route only Vercel calls has no use for it.
|
|
18
|
+
* Quickie accepted it everywhere, AltEd on the tick. Off by default here.
|
|
19
|
+
* - **The Bearer scheme is required.** Quickie's check stripped an optional
|
|
20
|
+
* prefix, so a bare secret in the header also passed.
|
|
21
|
+
*
|
|
22
|
+
* Returns a decision, not a `Response`: each app answers in its own envelope.
|
|
23
|
+
*/
|
|
24
|
+
export type CronAuthResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
status: 401 | 503;
|
|
29
|
+
message: string;
|
|
30
|
+
};
|
|
31
|
+
export interface CronAuthOptions {
|
|
32
|
+
/** The secret. Default `process.env.CRON_SECRET`. */
|
|
33
|
+
secret?: string;
|
|
34
|
+
/** Accept `?secret=` from a scheduler that can only call an address. Default `false`. */
|
|
35
|
+
allowQuery?: boolean;
|
|
36
|
+
/** Values that are public — committed to the repository — and so refused in production. */
|
|
37
|
+
publicSecrets?: readonly string[];
|
|
38
|
+
/** Default `process.env.NODE_ENV === "production"`. */
|
|
39
|
+
production?: boolean;
|
|
40
|
+
/** What the refusal calls this endpoint: "Cron", "The harvest". Default "Cron". */
|
|
41
|
+
label?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function checkCronRequest(request: Request, options?: CronAuthOptions): CronAuthResult;
|
|
44
|
+
//# sourceMappingURL=cron-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-auth.d.ts","sourceRoot":"","sources":["../../src/jobs/cron-auth.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,uDAAuD;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB,GAAG,cAAc,CAiBhG"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const digest = (value) => createHash("sha256").update(value).digest();
|
|
3
|
+
export function checkCronRequest(request, options = {}) {
|
|
4
|
+
const label = options.label ?? "Cron";
|
|
5
|
+
const secret = options.secret ?? process.env.CRON_SECRET ?? "";
|
|
6
|
+
if (!secret)
|
|
7
|
+
return { ok: false, status: 503, message: `${label} is not configured (CRON_SECRET unset).` };
|
|
8
|
+
const production = options.production ?? process.env.NODE_ENV === "production";
|
|
9
|
+
if (production && options.publicSecrets?.includes(secret)) {
|
|
10
|
+
return { ok: false, status: 503, message: `${label} is not configured (CRON_SECRET is the development value).` };
|
|
11
|
+
}
|
|
12
|
+
const header = request.headers.get("authorization") ?? "";
|
|
13
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(header)?.[1];
|
|
14
|
+
const provided = bearer ?? (options.allowQuery ? (new URL(request.url).searchParams.get("secret") ?? undefined) : undefined);
|
|
15
|
+
if (!provided || !timingSafeEqual(digest(provided), digest(secret))) {
|
|
16
|
+
return { ok: false, status: 401, message: "Unauthorized" };
|
|
17
|
+
}
|
|
18
|
+
return { ok: true };
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=cron-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron-auth.js","sourceRoot":"","sources":["../../src/jobs/cron-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAwC1D,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;AAE9E,MAAM,UAAU,gBAAgB,CAAC,OAAgB,EAAE,UAA2B,EAAE;IAC9E,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,MAAM,CAAC;IACtC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;IAC/D,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,KAAK,yCAAyC,EAAE,CAAC;IAC3G,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;IAC/E,IAAI,UAAU,IAAI,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,KAAK,4DAA4D,EAAE,CAAC;IACnH,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;IAC1D,MAAM,MAAM,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACpD,MAAM,QAAQ,GACZ,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC9G,IAAI,CAAC,QAAQ,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC;QACpE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;IAC7D,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type CountFormatOptions, type CountLine } from "./counts.js";
|
|
2
|
+
import type { AnyJob, RunNowScope } from "./registry.js";
|
|
3
|
+
import type { JobRunRecord, JobSwitchState } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* A job's health — derived every time it is shown, never stored.
|
|
6
|
+
*
|
|
7
|
+
* A stored status is a second copy of the truth that drifts from the first.
|
|
8
|
+
* Health here is a function of the ledger, the switch and the clock, so the
|
|
9
|
+
* card cannot say "Healthy" about a job whose last run failed an hour ago.
|
|
10
|
+
*
|
|
11
|
+
* Six states, in the order they win:
|
|
12
|
+
*
|
|
13
|
+
* 1. **paused** — switched off. Whatever the runs say, nothing new will start.
|
|
14
|
+
* 2. **never-run** — no run this viewer can see.
|
|
15
|
+
* 3. **running** — the latest run has not finished, and is inside its timeout.
|
|
16
|
+
* 4. **failed** — the latest run failed, or started and never finished. Both
|
|
17
|
+
* apps showed a run still in progress as "Errored", because an unfinished
|
|
18
|
+
* row has `ok = false`; and a run cut off by the platform's time limit stayed
|
|
19
|
+
* that way for ever with no message at all. Past its timeout it is a failure
|
|
20
|
+
* that says so.
|
|
21
|
+
* 5. **stale** — no successful *whole* run for longer than the threshold (a
|
|
22
|
+
* run exactly at the threshold is still healthy). A tenant's own Run now
|
|
23
|
+
* sweeps one tenant, so it proves nothing about the schedule and does not
|
|
24
|
+
* reset the clock — in Quickie it did, so one owner's click made a job look
|
|
25
|
+
* healthy to everyone.
|
|
26
|
+
* 6. **healthy**.
|
|
27
|
+
*/
|
|
28
|
+
export type JobHealthState = "paused" | "never-run" | "running" | "failed" | "stale" | "healthy";
|
|
29
|
+
export type JobHealth = {
|
|
30
|
+
state: "paused";
|
|
31
|
+
} | {
|
|
32
|
+
state: "never-run";
|
|
33
|
+
} | {
|
|
34
|
+
state: "running";
|
|
35
|
+
since: string;
|
|
36
|
+
} | {
|
|
37
|
+
state: "failed";
|
|
38
|
+
at: string;
|
|
39
|
+
error: string | null;
|
|
40
|
+
unfinished: boolean;
|
|
41
|
+
} | {
|
|
42
|
+
state: "stale";
|
|
43
|
+
lastSuccessAt: string | null;
|
|
44
|
+
thresholdMs: number;
|
|
45
|
+
} | {
|
|
46
|
+
state: "healthy";
|
|
47
|
+
lastSuccessAt: string;
|
|
48
|
+
};
|
|
49
|
+
/** The fields of a run that health reads. */
|
|
50
|
+
export interface RunFacts {
|
|
51
|
+
startedAt: Date;
|
|
52
|
+
finishedAt: Date | null;
|
|
53
|
+
ok: boolean;
|
|
54
|
+
error: string | null;
|
|
55
|
+
}
|
|
56
|
+
export interface HealthInput {
|
|
57
|
+
paused: boolean;
|
|
58
|
+
/** The latest run the viewer can see. */
|
|
59
|
+
latest: RunFacts | null;
|
|
60
|
+
/** The latest successful run over every tenant — the one staleness is measured from. */
|
|
61
|
+
lastWholeSuccess: RunFacts | null;
|
|
62
|
+
staleAfterMs: number | null;
|
|
63
|
+
timeoutMs: number;
|
|
64
|
+
now: Date;
|
|
65
|
+
}
|
|
66
|
+
export declare function deriveHealth(input: HealthInput): JobHealth;
|
|
67
|
+
/**
|
|
68
|
+
* Who is looking. A platform viewer (AltEd's administrators, Quickie's
|
|
69
|
+
* operators) may switch jobs and sees every sweep's detail; a tenant viewer
|
|
70
|
+
* (Quickie's owners) sees their own Run now results in full, and of a sweep
|
|
71
|
+
* over every tenant only whether it ran and when.
|
|
72
|
+
*/
|
|
73
|
+
export interface JobAudience {
|
|
74
|
+
platform: boolean;
|
|
75
|
+
tenantId?: string | null;
|
|
76
|
+
}
|
|
77
|
+
/** One run, as the card shows it. Dates are ISO strings, so it crosses from a server component intact. */
|
|
78
|
+
export interface JobRunView {
|
|
79
|
+
startedAt: string;
|
|
80
|
+
finishedAt: string | null;
|
|
81
|
+
ok: boolean;
|
|
82
|
+
trigger: "schedule" | "manual";
|
|
83
|
+
by: string | null;
|
|
84
|
+
/** A tenant's Run now: it swept one tenant, not all. */
|
|
85
|
+
tenantScoped: boolean;
|
|
86
|
+
counts: CountLine[];
|
|
87
|
+
error: string | null;
|
|
88
|
+
/**
|
|
89
|
+
* Counts and error left out: a sweep over every tenant, seen by one tenant.
|
|
90
|
+
* Quickie showed each owner the platform-wide ledger, so "412 emails sent"
|
|
91
|
+
* read as theirs, and an error from another organisation's data was on every
|
|
92
|
+
* owner's settings page.
|
|
93
|
+
*/
|
|
94
|
+
withheld: boolean;
|
|
95
|
+
}
|
|
96
|
+
/** A job as the health screen draws it — plain data, every count already in words. */
|
|
97
|
+
export interface JobView {
|
|
98
|
+
id: string;
|
|
99
|
+
label: string;
|
|
100
|
+
cadence: string;
|
|
101
|
+
schedule: "tick" | "own";
|
|
102
|
+
staleAfterMs: number | null;
|
|
103
|
+
health: JobHealth;
|
|
104
|
+
/** Health with the switch ignored, so the badge is right the moment someone switches it back on. */
|
|
105
|
+
healthIfOn: JobHealth;
|
|
106
|
+
paused: boolean;
|
|
107
|
+
switchedAt: string | null;
|
|
108
|
+
switchedBy: string | null;
|
|
109
|
+
/** This viewer may switch it. */
|
|
110
|
+
canSwitch: boolean;
|
|
111
|
+
/** This viewer may press Run now, and what it would sweep. */
|
|
112
|
+
runNow: RunNowScope | null;
|
|
113
|
+
lastRun: JobRunView | null;
|
|
114
|
+
lastSuccess: JobRunView | null;
|
|
115
|
+
}
|
|
116
|
+
/** What the view needs from the store for one job. */
|
|
117
|
+
export interface JobFacts {
|
|
118
|
+
switch: JobSwitchState | null;
|
|
119
|
+
latest: JobRunRecord | null;
|
|
120
|
+
lastWholeSuccess: JobRunRecord | null;
|
|
121
|
+
lastVisibleSuccess: JobRunRecord | null;
|
|
122
|
+
}
|
|
123
|
+
export interface JobViewOptions extends CountFormatOptions {
|
|
124
|
+
now: Date;
|
|
125
|
+
}
|
|
126
|
+
export declare function effectiveTimeout(job: AnyJob, fallback: number): number;
|
|
127
|
+
/** Whether this viewer may press Run now on this job. */
|
|
128
|
+
export declare function runNowFor(job: AnyJob, audience: JobAudience): RunNowScope | null;
|
|
129
|
+
export declare function buildJobView(job: AnyJob, facts: JobFacts, audience: JobAudience, options: JobViewOptions & {
|
|
130
|
+
timeoutMs: number;
|
|
131
|
+
}): JobView;
|
|
132
|
+
export interface WhenOptions {
|
|
133
|
+
locale?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Whose clock. Default `Europe/London`. Never the runtime's own: the card is
|
|
136
|
+
* drawn on a server in UTC and again in a browser in BST, and two different
|
|
137
|
+
* times in one element is a hydration error.
|
|
138
|
+
*/
|
|
139
|
+
timeZone?: string;
|
|
140
|
+
}
|
|
141
|
+
/** "30 Sept 2026, 14:05". */
|
|
142
|
+
export declare function formatWhen(iso: string, options?: WhenOptions): string;
|
|
143
|
+
/** "29 Sept 2026". */
|
|
144
|
+
export declare function formatDay(iso: string, options?: WhenOptions): string;
|
|
145
|
+
export interface HealthWords {
|
|
146
|
+
label: string;
|
|
147
|
+
/** A sentence under the badge, or `null` when the badge says it all. */
|
|
148
|
+
detail: string | null;
|
|
149
|
+
tone: "good" | "neutral" | "warning" | "bad";
|
|
150
|
+
}
|
|
151
|
+
/** The badge and the sentence under it. */
|
|
152
|
+
export declare function describeHealth(health: JobHealth, options?: WhenOptions): HealthWords;
|
|
153
|
+
/** "Paused 29 Sept 2026 by AltEd Admin", "Turned back on …", or nothing if it was never switched. */
|
|
154
|
+
export declare function describeSwitch(view: Pick<JobView, "paused" | "switchedAt" | "switchedBy">, options?: WhenOptions): string | null;
|
|
155
|
+
/** "Last run 30 Sept 2026, 14:05 · Run now by Ann Lee". */
|
|
156
|
+
export declare function describeRun(run: JobRunView, prefix: string, options?: WhenOptions): string;
|
|
157
|
+
//# sourceMappingURL=health.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/jobs/health.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { countLines, describeSpan } from "./counts.js";
|
|
2
|
+
import { isManualKey } from "./windows.js";
|
|
3
|
+
export function deriveHealth(input) {
|
|
4
|
+
const { latest, lastWholeSuccess, now } = input;
|
|
5
|
+
if (input.paused)
|
|
6
|
+
return { state: "paused" };
|
|
7
|
+
if (!latest && !lastWholeSuccess)
|
|
8
|
+
return { state: "never-run" };
|
|
9
|
+
if (latest && !latest.finishedAt) {
|
|
10
|
+
if (now.getTime() - latest.startedAt.getTime() <= input.timeoutMs) {
|
|
11
|
+
return { state: "running", since: latest.startedAt.toISOString() };
|
|
12
|
+
}
|
|
13
|
+
return { state: "failed", at: latest.startedAt.toISOString(), error: null, unfinished: true };
|
|
14
|
+
}
|
|
15
|
+
if (latest && !latest.ok) {
|
|
16
|
+
return { state: "failed", at: latest.startedAt.toISOString(), error: latest.error, unfinished: false };
|
|
17
|
+
}
|
|
18
|
+
if (input.staleAfterMs !== null &&
|
|
19
|
+
(!lastWholeSuccess || now.getTime() - lastWholeSuccess.startedAt.getTime() > input.staleAfterMs)) {
|
|
20
|
+
return {
|
|
21
|
+
state: "stale",
|
|
22
|
+
lastSuccessAt: lastWholeSuccess?.startedAt.toISOString() ?? null,
|
|
23
|
+
thresholdMs: input.staleAfterMs,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const success = lastWholeSuccess ?? latest;
|
|
27
|
+
return { state: "healthy", lastSuccessAt: success.startedAt.toISOString() };
|
|
28
|
+
}
|
|
29
|
+
export function effectiveTimeout(job, fallback) {
|
|
30
|
+
return job.timeoutMs ?? fallback;
|
|
31
|
+
}
|
|
32
|
+
/** Whether this viewer may press Run now on this job. */
|
|
33
|
+
export function runNowFor(job, audience) {
|
|
34
|
+
if (!job.runNow)
|
|
35
|
+
return null;
|
|
36
|
+
if (job.runNow.scope === "tenant")
|
|
37
|
+
return audience.tenantId ? "tenant" : null;
|
|
38
|
+
return audience.platform ? "platform" : null;
|
|
39
|
+
}
|
|
40
|
+
function runView(job, run, audience, options) {
|
|
41
|
+
const withheld = !audience.platform && run.tenantId === null;
|
|
42
|
+
return {
|
|
43
|
+
startedAt: run.startedAt.toISOString(),
|
|
44
|
+
finishedAt: run.finishedAt?.toISOString() ?? null,
|
|
45
|
+
ok: run.ok,
|
|
46
|
+
trigger: isManualKey(run.windowKey) ? "manual" : "schedule",
|
|
47
|
+
by: run.by,
|
|
48
|
+
tenantScoped: run.tenantId !== null,
|
|
49
|
+
counts: withheld ? [] : countLines(job.counts, run.counts, options),
|
|
50
|
+
error: withheld ? null : run.error,
|
|
51
|
+
withheld,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function buildJobView(job, facts, audience, options) {
|
|
55
|
+
const switchable = job.switchable ?? true;
|
|
56
|
+
const paused = switchable && Boolean(facts.switch?.paused);
|
|
57
|
+
const lastRun = facts.latest ? runView(job, facts.latest, audience, options) : null;
|
|
58
|
+
const input = {
|
|
59
|
+
latest: facts.latest,
|
|
60
|
+
lastWholeSuccess: facts.lastWholeSuccess,
|
|
61
|
+
staleAfterMs: job.staleAfterMs,
|
|
62
|
+
timeoutMs: effectiveTimeout(job, options.timeoutMs),
|
|
63
|
+
now: options.now,
|
|
64
|
+
};
|
|
65
|
+
const healthIfOn = deriveHealth({ ...input, paused: false });
|
|
66
|
+
// A withheld failure keeps its state and loses its words.
|
|
67
|
+
const redact = (health) => health.state === "failed" && lastRun?.withheld ? { ...health, error: null } : health;
|
|
68
|
+
return {
|
|
69
|
+
id: job.id,
|
|
70
|
+
label: job.label,
|
|
71
|
+
cadence: job.cadence,
|
|
72
|
+
schedule: job.schedule ?? "tick",
|
|
73
|
+
staleAfterMs: job.staleAfterMs,
|
|
74
|
+
health: redact(paused ? { state: "paused" } : healthIfOn),
|
|
75
|
+
healthIfOn: redact(healthIfOn),
|
|
76
|
+
paused,
|
|
77
|
+
switchedAt: switchable ? (facts.switch?.at?.toISOString() ?? null) : null,
|
|
78
|
+
switchedBy: switchable ? (facts.switch?.by ?? null) : null,
|
|
79
|
+
canSwitch: switchable && audience.platform,
|
|
80
|
+
runNow: runNowFor(job, audience),
|
|
81
|
+
lastRun,
|
|
82
|
+
lastSuccess: facts.lastVisibleSuccess ? runView(job, facts.lastVisibleSuccess, audience, options) : null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** "30 Sept 2026, 14:05". */
|
|
86
|
+
export function formatWhen(iso, options = {}) {
|
|
87
|
+
return new Intl.DateTimeFormat(options.locale ?? "en-GB", {
|
|
88
|
+
day: "numeric",
|
|
89
|
+
month: "short",
|
|
90
|
+
year: "numeric",
|
|
91
|
+
hour: "2-digit",
|
|
92
|
+
minute: "2-digit",
|
|
93
|
+
timeZone: options.timeZone ?? "Europe/London",
|
|
94
|
+
}).format(new Date(iso));
|
|
95
|
+
}
|
|
96
|
+
/** "29 Sept 2026". */
|
|
97
|
+
export function formatDay(iso, options = {}) {
|
|
98
|
+
return new Intl.DateTimeFormat(options.locale ?? "en-GB", {
|
|
99
|
+
day: "numeric",
|
|
100
|
+
month: "short",
|
|
101
|
+
year: "numeric",
|
|
102
|
+
timeZone: options.timeZone ?? "Europe/London",
|
|
103
|
+
}).format(new Date(iso));
|
|
104
|
+
}
|
|
105
|
+
/** The badge and the sentence under it. */
|
|
106
|
+
export function describeHealth(health, options = {}) {
|
|
107
|
+
switch (health.state) {
|
|
108
|
+
case "paused":
|
|
109
|
+
return { label: "Paused", detail: null, tone: "neutral" };
|
|
110
|
+
case "never-run":
|
|
111
|
+
return { label: "Never run", detail: "No runs recorded yet.", tone: "neutral" };
|
|
112
|
+
case "running":
|
|
113
|
+
return { label: "Running", detail: `Started ${formatWhen(health.since, options)}.`, tone: "neutral" };
|
|
114
|
+
case "failed":
|
|
115
|
+
return {
|
|
116
|
+
label: "Failed",
|
|
117
|
+
detail: health.unfinished
|
|
118
|
+
? `Started ${formatWhen(health.at, options)} and never finished — it was probably cut off.`
|
|
119
|
+
: (health.error ?? "The last run failed."),
|
|
120
|
+
tone: "bad",
|
|
121
|
+
};
|
|
122
|
+
case "stale":
|
|
123
|
+
return {
|
|
124
|
+
label: "Stale",
|
|
125
|
+
detail: health.lastSuccessAt
|
|
126
|
+
? `No successful run in over ${describeSpan(health.thresholdMs)} — the last was ${formatWhen(health.lastSuccessAt, options)}.`
|
|
127
|
+
: `No successful run on its schedule yet; one is expected every ${describeSpan(health.thresholdMs)}.`,
|
|
128
|
+
tone: "warning",
|
|
129
|
+
};
|
|
130
|
+
case "healthy":
|
|
131
|
+
return { label: "Healthy", detail: null, tone: "good" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** "Paused 29 Sept 2026 by AltEd Admin", "Turned back on …", or nothing if it was never switched. */
|
|
135
|
+
export function describeSwitch(view, options = {}) {
|
|
136
|
+
if (!view.switchedAt)
|
|
137
|
+
return view.paused ? "Paused" : null;
|
|
138
|
+
const by = view.switchedBy ? ` by ${view.switchedBy}` : "";
|
|
139
|
+
return `${view.paused ? "Paused" : "Turned back on"} ${formatDay(view.switchedAt, options)}${by}`;
|
|
140
|
+
}
|
|
141
|
+
/** "Last run 30 Sept 2026, 14:05 · Run now by Ann Lee". */
|
|
142
|
+
export function describeRun(run, prefix, options = {}) {
|
|
143
|
+
const when = `${prefix} ${formatWhen(run.startedAt, options)}`;
|
|
144
|
+
return run.trigger === "manual" ? `${when} · Run now${run.by ? ` by ${run.by}` : ""}` : when;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=health.js.map
|