Exported functions in state/lib/content.ts. .md file to compare - side-by-side diff against content
content
description: "Triggers on prompt mention of 'content'."
What it does for you
Turns your meetings and ideas into finished content, end to end.
What it produces
A recent result, so you can see the kind of work it returns.
loading…
How to get it
These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
what's inside - the parts that make up a skill 3/4 present
A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.
state/skills/content/SKILL.md
present
state/lib/content.ts
present
state/bin/content/
not present
state/skills/content/AGENTS.md
present
how it's graded - what counts as a good run 4 criteria · 2 deterministic · 2 judge
Each row is one thing a good run has to get right. deterministic means a quick check decides, pass or fail. judge means the AI reads the result and rates it. Grading each piece on its own (instead of one overall score) shows exactly where a run fell short, so the fix is obvious.
how it runs - the shared frame every skill uses 5/5 present
Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.
state/log/pending-eval.ndjson what it has learned - fixes written back in over time sample
When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.
- Loading feedback rows…
how the work flows- who makes it, who checks it
import from `state/lib/content.ts` (e.g. `getContentAtoms`, `listDrafts`, `createDraft`, `tickDraft`)
read-only `listDrafts()` smoke before any `createDraft` / `tickDraft
SKILL.md- the skill, written out in plain English
content
Content production orchestrator for all snappy-* skills.
Ported from kernel snappy-content in Phase 0.5. See state/lib/content.ts for the full API surface.
Steps
getContentAtoms()- seestate/lib/content.tslistDrafts()- seestate/lib/content.tscreateDraft()- seestate/lib/content.tsstatus()- seestate/lib/content.tstickDraft()- seestate/lib/content.tscomputeContentMetric()- seestate/lib/content.tslistContacts()- seestate/lib/content.ts
Eval
Actor: the exported functions in state/lib/content.ts. Auditor: none wired yet - eval is manual (Robert review). File a state/log/pending-eval.ndjson row on each run.
Score convention:
| Outcome | Score |
|---|---|
| Pass on first try | 1.0 |
| Failed first, auto-fix applied, re-check passed | 0.5 |
| Still failing or unrecoverable | 0.0 |
Gotchas
via the Phase 0.5 driver. Only these rewrites were applied: already in state/lib/)
realpathSync(process.argv[1])CLI guard wrapped in try/catch
- See the kernel SKILL.md for the original long-form guidance if you need it
(read-only reference at the kernel path above).
Graduation
This skill is prose. Graduate by defining a deterministic auditor and flipping eval: auto.
Rubric
criteria:
- name: calls_content_functions
kind: judge
check: "The skill calls at least one of the exported functions from 'state/lib/content.ts' based on the skill's input."
- name: handles_required_inputs
kind: deterministic
check: "The skill execution does not error when 'getContentAtoms_input', 'listDrafts_input', and 'createDraft_input' are provided with valid string values."
- name: log_entry_created
kind: deterministic
check: "A new row is appended to 'state/log/pending-eval.ndjson' for each skill execution."
- name: matches_snapshot_output
kind: judge
check: "The output of the skill matches a pre-established snapshot for similar inputs, indicating consistent behavior."AGENTS.md- what the AI loads when this skill comes up
content - loader
Per-turn rules for the content skill. Full reference: state/skills/content/SKILL.md. Do not skip these.
Critical Rules
_(no failures recorded yet - this skill is a Phase 0.5 prose port from kernel snappy-content. No hard-won rules have surfaced yet. Read state/skills/content/SKILL.md and state/lib/content.ts before invoking. The kernel had a stray realpathSync import bug - patched in port.)_
Commands
| ui dashboard | state/skills/content/resources/ui.openui | |invoke: import from state/lib/content.ts (e.g. getContentAtoms, listDrafts, createDraft, tickDraft) |verify: read-only listDrafts() smoke before any createDraft / tickDraft |eval log: state/log/pending-eval.ndjson (manual eval - skill: "content")
OpenUI Resource
- Skill-owned OpenUI Lang resource:
state/skills/content/resources/ui.openui. Read it before rendering or editing this skill's generated component surface. - Treat this resource as a first-class artifact of the skill, not a generic chat response. Improve it when the skill's user-facing output needs to become richer.
- System resources compose OpenUI primitives and inherit SnappyChat tokens. Use
ui_contract: brandedin SKILL.md only for deliberate platform or client visuals.
Known Pitfalls
- Drafts move through
tickDraft()- don't mutate state directly, use the orchestrator fn so chain log captures the transition - Atoms come from
content-mine(which has citation + Robert-signal gates) - if you build a draft from atoms, don't strip thesource_ref
Self-Test
An agent reading this should correctly:
- [ ] Use
state/lib/content.tsfunctions, not direct DB writes - [ ] Call
tickDraft()to advance state, not mutate fields by hand - [ ] Log to
pending-eval.ndjsonuntil an auditor is wired
Self-report
If this loader fell short, append a line:
echo "[$(date -u +%FT%TZ)] content: <what was missing>" >> state/log/loader-feedback.log
<!-- kernel-ok: historical attribution for the PID-loader pattern. -->
Self-correcting loader (PID feedback)
This is the rule that makes loaders learn. Ported from snappy-kernel, where it demonstrably worked: the snappy-dom-cartographer skill mapped Skool admin UI once, every later ship-agent inherited the full map for free.
Before you finish, do two things:
1. Fix gaps (P - proportional). MANDATORY. If this loader didn't cover your case - if you had to read another file, run an undocumented command, work around a wrong selector, or discover a quirk - you MUST attempt an Edit to this AGENTS.md before you log.
- Default action: edit this file. Surgical. Add the missing line, table
row, or code example where it belongs. No restructuring.
- Only
LOGGEDis allowed when: the fix needs >10 lines, spans multiple
files, or requires a structural rewrite. In that case the state/regen/drain.sh queue picks it up asynchronously.
- "I didn't have time" / "it's minor" / "the next agent will figure it out"
are NOT valid reasons. Minor gaps compound - that's the whole point of this loop.
- Concrete example of a valid P-fix: discover
agent-browser screenshot
takes path positionally not as --output <path>. One line in a table. Edit it in. Do not just log it.
- The goal: the next agent never has to leave the loader.
2. Log the result.
echo "[$(date -u +%FT%TZ)] content: <what was missing or fixed> [FIXED|LOGGED]" >> state/log/loader-feedback.log
FIXED= you patched this loader inline (P-fix).LOGGED= too large for inline; the PostToolUse enqueue + Stop-hook drain
will rewrite the loader from scratch on next session-end.
Do not skip this. Every agent run must leave the system better than it found it. The loader is the setpoint; you are the sensor; the gap is the error signal; closing the gap is the correction.
api.ts- the code it can call
#!/usr/bin/env npx tsx
/**
* snappy-content/api.ts -- Content production orchestrator for all snappy-* skills.
*
* Re-exports from knowledge + provides helpers for querying the content engine DB
* (content_atoms on rb-content-engine.fly.dev).
*
* Usage:
* npx tsx api.ts atoms draft # list draft content atoms
* npx tsx api.ts atoms approved # list approved content atoms
* npx tsx api.ts atoms archived # list archived content atoms
*
* Or import as module:
* import { listContacts, getContentAtoms } from "./content.ts";
*/
import { env } from "./env.ts";
import { readdirSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import {
checkTone,
requireCitations,
checkFlow,
} from "./positioning.ts";
import { realpathSync } from "fs";
import {
schedulePost,
patchDraft,
getDraft,
deleteDraft,
listScheduled,
} from "./linkedin.ts";
// --- Re-exports from child skills ---
export { listContacts } from "./knowledge.ts";
// --- Content Engine DB ---
const CONTENT_ENGINE = "https://rb-content-engine.fly.dev/sql";
/**
* Query the content engine DB for content atoms by status.
* Returns rows from content_atoms table.
*/
export async function getContentAtoms(status = "draft") {
const res = await fetch(CONTENT_ENGINE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: "SELECT id, type, draft, speaker, topic, tags, frequency, status, times_used, created_at FROM content_atoms WHERE status = $1 ORDER BY created_at DESC LIMIT 50",
params: [status],
}),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Content engine query failed (${res.status}): ${text}`);
}
return res.json();
}
// --- Content PID loop ---
// Mirrors the classroom loop: disk frontmatter is source of truth, pass-log
// is the append-only decision trace, Typefully is the publishing target.
// See data/README.md for the schema and invariants.
const THIS_DIR = dirname(fileURLToPath(import.meta.url));
const DRAFTS_DIR = join(THIS_DIR, "data", "drafts");
const PASS_LOG = join(THIS_DIR, "data", "pass-log.md");
export type DraftStatus = "drafted" | "scheduled" | "shipped" | "retired";
export type DraftFrontmatter = {
slug: string;
topic: string;
status: DraftStatus;
typefully_id: number;
publish_at: string;
iteration: number;
last_tick_at: string;
last_gate_check: "" | "pass" | "fail";
last_gate_cite: "" | "pass" | "fail";
last_gate_flow: "" | "pass" | "fail";
flow_profile: string;
media_id: string;
cite_mode: "verbatim" | "practitioner";
first_comment: string;
};
export type Draft = {
path: string;
fm: DraftFrontmatter;
body: string;
};
function parseFm(raw: string): { fm: DraftFrontmatter; body: string } {
const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!match) throw new Error("missing frontmatter block");
const fm: Record<string, unknown> = {};
for (const line of match[1].split("\n")) {
const kv = line.match(/^(\w+):\s*(.*)$/);
if (!kv) continue;
let v: unknown = kv[2].trim().replace(/^["']|["']$/g, "");
if (v === "true") v = true;
else if (v === "false") v = false;
else if (typeof v === "string" && /^-?\d+$/.test(v)) v = Number(v);
fm[kv[1]] = v;
}
return { fm: fm as DraftFrontmatter, body: match[2] };
}
function serializeFm(fm: DraftFrontmatter, body: string): string {
const order: (keyof DraftFrontmatter)[] = [
"slug", "topic", "status", "typefully_id", "publish_at",
"iteration", "last_tick_at", "last_gate_check", "last_gate_cite",
"last_gate_flow", "flow_profile", "media_id", "cite_mode", "first_comment",
];
const lines = order.map((k) => {
const v = fm[k];
if (typeof v === "string") return `${k}: "${v}"`;
return `${k}: ${v}`;
});
return `---\n${lines.join("\n")}\n---\n${body.startsWith("\n") ? body : "\n" + body}`;
}
export function listDrafts(): Draft[] {
let files: string[];
try {
files = readdirSync(DRAFTS_DIR).filter((f) => f.endsWith(".md"));
} catch {
return [];
}
return files.map((f) => {
const path = join(DRAFTS_DIR, f);
const raw = readFileSync(path, "utf8");
const { fm, body } = parseFm(raw);
return { path, fm, body };
});
}
function writeDraft(d: Draft): void {
writeFileSync(d.path, serializeFm(d.fm, d.body));
}
export function createDraft(
slug: string,
topic: string,
body: string,
opts: { flowProfile?: string; citeMode?: "verbatim" | "practitioner" } = {},
): Draft {
const fm: DraftFrontmatter = {
slug,
topic,
status: "drafted",
typefully_id: 0,
publish_at: "",
iteration: 0,
last_tick_at: "",
last_gate_check: "",
last_gate_cite: "",
last_gate_flow: "",
flow_profile: opts.flowProfile || "balanced",
media_id: "",
cite_mode: opts.citeMode || "verbatim",
first_comment: "",
};
const path = join(DRAFTS_DIR, `${slug}.md`);
const d: Draft = { path, fm, body };
writeDraft(d);
return d;
}
function logPass(entry: string): void {
appendFileSync(PASS_LOG, "\n" + entry + "\n");
}
/** Read-only status view over the drafts directory. */
export function status() {
const drafts = listDrafts();
const buckets: Record<DraftStatus, Draft[]> = {
drafted: [],
scheduled: [],
shipped: [],
retired: [],
};
for (const d of drafts) {
const s = d.fm.status;
if (buckets[s]) buckets[s].push(d);
}
return {
total: drafts.length,
drafted: buckets.drafted.map((d) => ({ slug: d.fm.slug, topic: d.fm.topic, iteration: d.fm.iteration })),
scheduled: buckets.scheduled.map((d) => ({ slug: d.fm.slug, topic: d.fm.topic, publish_at: d.fm.publish_at, typefully_id: d.fm.typefully_id, iteration: d.fm.iteration })),
shipped: buckets.shipped.map((d) => ({ slug: d.fm.slug, topic: d.fm.topic, publish_at: d.fm.publish_at })),
retired: buckets.retired.map((d) => ({ slug: d.fm.slug, topic: d.fm.topic })),
};
}
function runGates(body: string, profile: string, citeMode: string = "verbatim") {
const tone = checkTone(body);
const cite = citeMode === "practitioner"
? { pass: true, reasons: [] as string[] }
: requireCitations(body);
const flow = checkFlow(body, { profile });
return {
check: tone.pass ? "pass" : "fail",
cite: cite.pass ? "pass" : "fail",
flow: flow.pass ? "pass" : "fail",
reasons: [
...tone.violations.map((r) => `check: ${r}`),
...cite.reasons.map((r) => `cite: ${r}`),
...flow.violations.map((r) => `flow: ${r}`),
],
} as const;
}
/**
* Single PID tick for one draft. Idempotent. Returns a decision record.
*
* Rules:
* - drafted + all gates pass → schedule to Typefully at `when`, write back id+publish_at.
* - scheduled + text changed → patch Typefully in place (bumps iteration).
* - scheduled + gates fail on current body → log gap, do not publish.
* - shipped/retired → no-op.
* - If `dryRun`, runs gates and decides but makes no remote or disk writes.
*/
export async function tickDraft(
slug: string,
opts: { when?: string; dryRun?: boolean } = {},
) {
const drafts = listDrafts();
const d = drafts.find((x) => x.fm.slug === slug);
if (!d) throw new Error(`draft not found: ${slug}`);
const now = new Date().toISOString();
const profile = d.fm.flow_profile || "balanced";
const citeMode = d.fm.cite_mode || "verbatim";
const gates = runGates(d.body, profile, citeMode);
const decision: {
slug: string;
action: "schedule" | "patch" | "noop" | "gap";
gates: typeof gates;
typefully_id?: number;
publish_at?: string;
note: string;
} = { slug, action: "noop", gates, note: "" };
if (d.fm.status === "shipped" || d.fm.status === "retired") {
decision.note = `status=${d.fm.status}; nothing to do`;
return decision;
}
if (gates.check !== "pass" || gates.cite !== "pass" || gates.flow !== "pass") {
decision.action = "gap";
decision.note = `gates failed: ${gates.reasons.join(" | ")}`;
if (!opts.dryRun) {
d.fm.last_tick_at = now;
d.fm.last_gate_check = gates.check;
d.fm.last_gate_cite = gates.cite;
d.fm.last_gate_flow = gates.flow;
writeDraft(d);
logPass(tickEntry(d, decision));
}
return decision;
}
if (d.fm.status === "drafted") {
const when = opts.when
|| d.fm.publish_at
|| new Date(Date.now() + 3 * 24 * 3600 * 1000).toISOString().replace(/\.\d+Z$/, "Z");
decision.action = "schedule";
decision.publish_at = when;
decision.note = `schedule new draft at ${when}`;
if (!opts.dryRun) {
const mediaIds = d.fm.media_id ? [d.fm.media_id] : undefined;
const firstComment = d.fm.first_comment || undefined;
const created = (await schedulePost(stripCiteMarkup(d.body), when, true, mediaIds, firstComment)) as { id: number };
d.fm.status = "scheduled";
d.fm.typefully_id = created.id;
d.fm.publish_at = when;
d.fm.iteration += 1;
d.fm.last_tick_at = now;
d.fm.last_gate_check = "pass";
d.fm.last_gate_cite = "pass";
d.fm.last_gate_flow = "pass";
decision.typefully_id = created.id;
writeDraft(d);
logPass(tickEntry(d, decision));
}
return decision;
}
// status === "scheduled"
const remote = (await getDraft(d.fm.typefully_id)) as any;
const remoteText: string = remote?.platforms?.linkedin?.posts?.[0]?.text ?? "";
const desiredText = stripCiteMarkup(d.body);
if (remoteText === desiredText) {
decision.note = "scheduled text matches local; no patch needed";
if (!opts.dryRun) {
d.fm.last_tick_at = now;
writeDraft(d);
logPass(tickEntry(d, decision));
}
return decision;
}
decision.action = "patch";
decision.typefully_id = d.fm.typefully_id;
decision.note = `patching remote draft ${d.fm.typefully_id} in place`;
if (!opts.dryRun) {
const patchFirstComment = d.fm.first_comment || undefined;
await patchDraft(d.fm.typefully_id, { text: desiredText, firstComment: patchFirstComment });
d.fm.iteration += 1;
d.fm.last_tick_at = now;
writeDraft(d);
logPass(tickEntry(d, decision));
}
return decision;
}
function stripCiteMarkup(body: string): string {
return body
.replace(/\[source:[^\]]*\]/g, "")
.replace(/<\/?connective>/gi, "")
.replace(/[ \t]+\n/g, "\n")
.replace(/[ \t]{2,}/g, " ")
.trim();
}
function tickEntry(d: Draft, decision: { action: string; note: string; gates: { check: string; cite: string; flow: string } }): string {
const now = new Date().toISOString();
return [
`## ${now.slice(0, 10)} content-loop`,
`- **Tick kind**: ${decision.action}`,
`- **Draft**: ${d.fm.slug} (typefully_id=${d.fm.typefully_id || "none"})`,
`- **Gates**: check ${decision.gates.check}, cite ${decision.gates.cite}, flow/${d.fm.flow_profile} ${decision.gates.flow}`,
`- **Decision**: ${decision.note}`,
].join("\n");
}
// --- Metrics (Step 7a) ---
const STAGED_ACTIONS_LOG = `${process.env.HOME}/.claude/logs/staged-actions.ndjson`;
type StagedRun = { ts: string; name: string; action: string };
function readStagedRunsContent(): StagedRun[] {
try {
const lines = readFileSync(STAGED_ACTIONS_LOG, "utf-8").split("\n");
const out: StagedRun[] = [];
for (const line of lines) {
if (!line.trim()) continue;
try {
const j = JSON.parse(line);
if (typeof j?.name === "string" && typeof j?.ts === "string") {
out.push({ ts: j.ts, name: j.name, action: j.action || "" });
}
} catch { /* skip malformed */ }
}
return out;
} catch { return []; }
}
function withinLastDays(tsIso: string, days: number): boolean {
const t = new Date(tsIso).getTime();
if (isNaN(t)) return false;
return t >= Date.now() - days * 86400_000;
}
export function computeContentMetric(name: string): number | null {
const runs = readStagedRunsContent().filter((r) => withinLastDays(r.ts, 7));
switch (name) {
case "polish-per-week":
case "content_polish_runs_per_week":
return runs.filter((r) => r.name === "content-polish").length;
case "polish-apply-rate":
case "content_polish_apply_rate": {
const polish = runs.filter((r) => r.name === "content-polish");
if (!polish.length) return null;
const delivered = polish.filter((r) => r.action === "delivered").length;
return delivered / polish.length;
}
case "atoms-per-week":
case "content_mine_atoms_per_week":
return runs.filter((r) => r.name === "content-mine").length;
default:
return null;
}
}
// --- CLI ---
if ((() => { try { return import.meta.url === `file://${realpathSync(process.argv[1])}`; } catch { return false; } })()) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "metrics": {
const [name, ...rest] = args;
if (!name) {
console.error("Usage: api.ts metrics <name> [--json]");
console.error("Names: polish-per-week, polish-apply-rate, atoms-per-week");
process.exit(1);
}
const value = computeContentMetric(name);
if (rest.includes("--json")) console.log(JSON.stringify({ value }));
else console.log(value == null ? "null" : String(value));
break;
}
case "atoms": {
const status = args[0] || "draft";
const data = await getContentAtoms(status);
if (Array.isArray(data) && data.length) {
for (const atom of data) {
console.log(`[${atom.id}] ${atom.type || "untyped"} -- ${atom.topic || "(no topic)"} (${atom.status})`);
if (atom.draft) console.log(` ${atom.draft.slice(0, 120)}...`);
}
} else {
console.log(JSON.stringify(data, null, 2));
}
break;
}
case "status": {
const s = status();
console.log(`[loop-status] total=${s.total}`);
console.log(` drafted: ${s.drafted.length}`);
for (const d of s.drafted) console.log(` - ${d.slug} (iter=${d.iteration}) — ${d.topic}`);
console.log(` scheduled: ${s.scheduled.length}`);
for (const d of s.scheduled) console.log(` - ${d.slug} (iter=${d.iteration}) id=${d.typefully_id} at=${d.publish_at} — ${d.topic}`);
console.log(` shipped: ${s.shipped.length}`);
for (const d of s.shipped) console.log(` - ${d.slug} at=${d.publish_at} — ${d.topic}`);
console.log(` retired: ${s.retired.length}`);
for (const d of s.retired) console.log(` - ${d.slug} — ${d.topic}`);
break;
}
case "tick": {
let slug: string | undefined;
let when: string | undefined;
let dryRun = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--when") { when = args[++i]; }
else if (args[i] === "--dry-run") { dryRun = true; }
else if (!slug) { slug = args[i]; }
}
if (!slug) { console.error("Usage: api.ts tick <slug> [--when ISO8601Z] [--dry-run]"); process.exit(1); }
const decision = await tickDraft(slug, { when, dryRun });
console.log(JSON.stringify(decision, null, 2));
break;
}
case "tick-all": {
const dryRun = args.includes("--dry-run");
const results = [];
for (const d of listDrafts()) {
if (d.fm.status === "shipped" || d.fm.status === "retired") continue;
results.push(await tickDraft(d.fm.slug, { dryRun }));
}
console.log(JSON.stringify(results, null, 2));
break;
}
case "draft-new": {
let slug: string | undefined;
let topic = "";
let bodyFile: string | undefined;
let profile = "balanced";
let citeMode: "verbatim" | "practitioner" | undefined;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--topic") { topic = args[++i]; }
else if (args[i] === "--body-file") { bodyFile = args[++i]; }
else if (args[i] === "--profile") { profile = args[++i]; }
else if (args[i] === "--cite-mode") { citeMode = args[++i] as "verbatim" | "practitioner"; }
else if (!slug) { slug = args[i]; }
}
if (!slug || !bodyFile) { console.error("Usage: api.ts draft-new <slug> --body-file <path> --topic <topic> [--profile balanced] [--cite-mode verbatim|practitioner]"); process.exit(1); }
const body = readFileSync(bodyFile, "utf8");
const d = createDraft(slug, topic, body, { flowProfile: profile, citeMode });
console.log(JSON.stringify({ slug: d.fm.slug, path: d.path, status: d.fm.status }));
break;
}
default:
console.log("Usage: npx tsx api.ts <atoms|status|tick|tick-all|draft-new> [...args]");
}
})();
}
scripts- helper scripts it can run
prose-only skill - 1 inline code block live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 10 runs
| timestamp | verb | score | primary_issue | artifact |
|---|---|---|---|---|
| 2026-04-25 04:11Z | - | 1.00 | - | - |
| 2026-04-21 15:58Z | - | 1.00 | - | - |
| 2026-04-21 15:56Z | - | 1.00 | - | - |
| 2026-04-21 03:53Z | - | 1.00 | - | - |
| 2026-04-25 04:11Z | - | 1.00 | - | - |
| 2026-04-21 15:58Z | - | 1.00 | - | - |
| 2026-04-21 15:56Z | - | 1.00 | - | - |
| 2026-04-21 03:53Z | - | 1.00 | - | - |
| 2026-04-25 04:11Z | - | 1.00 | - | - |
| 2026-04-21 15:58Z | - | 1.00 | - | - |