State/lib/gcal.ts listEvents .md file to compare - side-by-side diff against calendar
calendar
description: "Triggers on prompt mention of 'calendar'."
What it does for you
Pulls your week together so you walk in knowing what matters.
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/calendar/SKILL.md
present
state/lib/calendar.ts
present
state/bin/calendar/
not present
state/skills/calendar/AGENTS.md
present
how it's graded - what counts as a good run 5 criteria · 2 deterministic · 3 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/evals.ndjson - The canonical lib is state/lib/calendar.ts (hand-rolled JWT, scope https://www.googleapis.com/auth/calendar, exports listEvents, createEvent, updateEvent, deleteEvent, eventsByAttendee, checkAvailability, proposeSlots). state/lib/gcal.ts does NOT exist (loader claim was stale 2026-04-29). Neither does state/bin/calendar/dump-week.ts.
- NEVER treat listEvents returning items: [] as "no meetings" — Calendar API returns empty silently when the SA lacks access. Cross-check by hitting https://www.googleapis.com/calendar/v3/users/me/calendarList with the SA token; an empty items there = share-gap, not empty week.
- ALWAYS unescape GOOGLE_SERVICE_ACCOUNT_KEY \n to real newlines before signing — state/lib/calendar.ts:49 does this for you, do not bypass.
- The service account xano-automation@snappy-424813.iam.gserviceaccount.com needs the target calendar shared to it with "See all event details". Robert is the only one who can do this — one-time, in calendar.google.com Settings.
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
SKILL.md- the skill, written out in plain English
calendar
Read-only window into Robert's primary Google Calendar so the weekly Ray-update pod can cite real meetings instead of leaning only on Krisp transcripts (which miss everything Robert attended without Krisp running).
The actor (state/lib/gcal.ts) lists events. The auditor is the dump script's shape gate plus an independent diagnostic call to listCalendars() that proves the SA actually has access. If the SA sees zero calendars, the dump logs a P1 friction and downgrades the eval - that is the share-gap signal.
There is a second, older calendar lib at state/lib/calendar.ts that hand-rolls the JWT and uses the broad read+write scope. Prefer state/lib/gcal.ts for new work - same pattern as state/lib/sheets.ts and state/lib/gdocs.ts, scope is narrower (calendar.readonly), client is googleapis.
Steps
getCalendarClient()- JWT-authenticated{ calendar }client, cached per process.listEvents({ since, until, calendarId? })- Schema$Event[] for the window.calendarIddefaults to envGOOGLE_CALENDAR_ID, then"primary".listCalendars()- what the SA can actually see. Diagnostic - call this whenlistEventsreturns empty to disambiguate "no share" from "empty week".state/bin/calendar/dump-week.ts [YYYY-Www | YYYY-MM-DD]- write Mon-Fri events tostate/log/calendar/<YYYY-Www>.json. Logs eval and (on share gap) a P1 friction.
Eval
Actor: state/lib/gcal.ts listEvents. Auditor: the dump script's shape gate plus listCalendars() cross-check. They are different surfaces - the eval is not graded by the same call that produced the data.
| Outcome | Score |
|---|---|
| Window has events AND shape OK | 1.0 |
| Window empty AND SA sees zero calendars (known share gap) | 0.5 + P1 friction |
| Window empty BUT SA sees the calendar (genuinely no meetings) | 0.5 |
| Threw before writing the dump | 0.0 |
Gotchas
- The share is the bootstrapping cost. Calendar API does not 403 a SA that lacks access - it returns
{ items: [] }silently. Treat empty as ambiguous and calllistCalendars()to disambiguate. - Calendar API must be enabled in GCP. If you see
accessNotConfigured, enable at https://console.developers.google.com/apis/api/calendar-json.googleapis.com/overview?project=636767781895 and wait 30 seconds. - Newline unescape. Same SA-key gotcha as sheets/gdocs -
state/lib/gcal.tshandles it. Do not bypass. - Two libs exist.
state/lib/calendar.tsis the older hand-rolled JWT version with read+write scope. Usestate/lib/gcal.tsfor new code unless you need write.
The one-time share action
For the Calendar pull to return real data, Robert must share his primary calendar to the service account. One-time, takes 30 seconds:
- Open https://calendar.google.com in the browser signed in as
robert@snappy.ai. - Hover the "My calendars" entry for
robert@snappy.ai(or whichever calendar isGOOGLE_CALENDAR_ID) -> three-dot menu -> "Settings and sharing". - Scroll to "Share with specific people or groups" -> "Add people".
- Paste the value of
GOOGLE_SERVICE_ACCOUNT_EMAILfrom.env.cache(currentlyxano-automation@snappy-424813.iam.gserviceaccount.com). - Permission: "See all event details".
- Save. The SA inherits access immediately - re-run
npx tsx state/bin/calendar/dump-week.tsto verify.
Graduation
Already graduated - script at state/bin/calendar/dump-week.ts writes a stable JSON shape consumed by the weekly Ray-update pod.
Rubric
criteria:
- name: calendar_dump_generated
kind: deterministic
check: "The command `npx tsx state/bin/calendar/dump-week.ts` successfully generates a JSON file in `state/log/calendar/`."
- name: gcal_lib_used
kind: judge
check: "The `state/bin/calendar/dump-week.ts` script uses `state/lib/gcal.ts` for calendar access, not `state/lib/calendar.ts`."
- name: valid_json_output
kind: deterministic
check: "The generated JSON file in `state/log/calendar/` is valid and well-formed according to `jsonlint`."
- name: correct_event_shape
kind: judge
check: "The events in the generated JSON adhere to the expected `Schema$Event` structure as described for `listEvents`."
- name: share_gap_logging_present
kind: judge
check: "If the calendar dump is empty and `listCalendars()` reports zero accessible calendars, a P1 friction is logged."AGENTS.md- what the AI loads when this skill comes up
calendar - loader
Per-turn rules for the calendar skill. Full reference: state/skills/calendar/SKILL.md. Do not skip these.
Critical Rules
- The canonical lib is
state/lib/calendar.ts(hand-rolled JWT, scopehttps://www.googleapis.com/auth/calendar, exportslistEvents,createEvent,updateEvent,deleteEvent,eventsByAttendee,checkAvailability,proposeSlots).state/lib/gcal.tsdoes NOT exist (loader claim was stale 2026-04-29). Neither doesstate/bin/calendar/dump-week.ts. - NEVER treat
listEventsreturningitems: []as "no meetings" - Calendar API returns empty silently when the SA lacks access. Cross-check by hittinghttps://www.googleapis.com/calendar/v3/users/me/calendarListwith the SA token; an emptyitemsthere = share-gap, not empty week. - ALWAYS unescape
GOOGLE_SERVICE_ACCOUNT_KEY\nto real newlines before signing -state/lib/calendar.ts:49does this for you, do not bypass. - The service account
xano-automation@snappy-424813.iam.gserviceaccount.comneeds the target calendar shared to it with "See all event details". Robert is the only one who can do this - one-time, in calendar.google.com Settings.
Commands
| ui dashboard | state/skills/calendar/resources/ui.openui | |invoke (lib): import from state/lib/calendar.ts - listEvents(days), createEvent, eventsByAttendee, proposeSlots |invoke (CLI): npx tsx state/lib/calendar.ts events 7 (next 7 days), ... today, ... availability 1, ... propose 3 30 |verify (share-gap): hit users/me/calendarList with the SA token; empty array = share missing. See /tmp/list-cals.mjs pattern from 2026-04-29 session. |eval log: state/log/evals.ndjson (skill: "calendar") |output: state/log/calendar/<YYYY-Www>.json
OpenUI Resource
- Skill-owned OpenUI Lang resource:
state/skills/calendar/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
accessNotConfiguredon first call - Calendar API needs enabling at https://console.developers.google.com/apis/api/calendar-json.googleapis.com/overview?project=636767781895- Newline format - bypassing the lib means OpenSSL throws
DECODER routines :: unsupported - Empty
items: []is ambiguous - could be no-share OR empty week. The dump-week script logs a P1 friction when SA sees zero calendars at all, which is the actionable signal for Robert.
Self-Test
An agent reading this should correctly:
- [ ] Use
state/lib/gcal.tsfor new readonly work, not the olderstate/lib/calendar.ts - [ ] Call
listCalendars()as the disambiguation check before reporting "no meetings" - [ ] Surface the share-action requirement in the report when SA sees zero calendars
Self-report
If this loader fell short, append a line:
echo "[$(date -u +%FT%TZ)] calendar: <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)] calendar: <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-calendar/api.ts -- Google Calendar API (direct) for all snappy-* skills.
*
* Auth: Google service account JWT → access token.
* Requires in .env.cache:
* GOOGLE_SERVICE_ACCOUNT_EMAIL (already present)
* GOOGLE_SERVICE_ACCOUNT_KEY (PEM private key, newlines as \n)
* GOOGLE_CALENDAR_ID (optional, defaults to "primary")
*
* To add the key: export from GCP console → Service Accounts → Keys → JSON.
* Copy the "private_key" field value into .env.cache as GOOGLE_SERVICE_ACCOUNT_KEY.
* The calendar must be shared with the service account email.
*
* Usage:
* npx tsx api.ts events # today's events
* npx tsx api.ts events 7 # this week
* npx tsx api.ts availability # free/busy blocks
* npx tsx api.ts create '{"summary":"Call","start_time":"...","end_time":"..."}'
* npx tsx api.ts update '{"event_id":"...","summary":"..."}'
* npx tsx api.ts delete <eventId>
*
* Or import as module:
* import { listEvents, createEvent, updateEvent, deleteEvent, checkAvailability } from "./calendar.ts";
*/
import { env } from "./env.ts";
import { createSign } from "crypto";
import { realpathSync } from "fs";
const GCAL_API = "https://www.googleapis.com/calendar/v3";
const TOKEN_URL = "https://oauth2.googleapis.com/token";
const SCOPE = "https://www.googleapis.com/auth/calendar";
let _accessToken: string | null = null;
let _tokenExpiry = 0;
function calendarId(): string {
return env("GOOGLE_CALENDAR_ID", false) || "primary";
}
function base64url(input: Buffer | string): string {
const buf = typeof input === "string" ? Buffer.from(input) : input;
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function buildJwt(): string {
const email = env("GOOGLE_SERVICE_ACCOUNT_EMAIL");
const key = env("GOOGLE_SERVICE_ACCOUNT_KEY").replace(/\\n/g, "\n");
const now = Math.floor(Date.now() / 1000);
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
const payload = base64url(
JSON.stringify({
iss: email,
scope: SCOPE,
aud: TOKEN_URL,
iat: now,
exp: now + 3600,
}),
);
const sign = createSign("RSA-SHA256");
sign.update(`${header}.${payload}`);
const signature = base64url(sign.sign(key));
return `${header}.${payload}.${signature}`;
}
async function getAccessToken(): Promise<string> {
if (_accessToken && Date.now() / 1000 < _tokenExpiry - 60) {
return _accessToken;
}
const jwt = buildJwt();
const res = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=${jwt}`,
});
const data = await res.json();
if (!res.ok) {
throw new Error(`Google token exchange failed (${res.status}): ${JSON.stringify(data)}`);
}
_accessToken = data.access_token;
_tokenExpiry = Math.floor(Date.now() / 1000) + data.expires_in;
return _accessToken!;
}
async function gcal(method: string, path: string, body?: Record<string, unknown>) {
const token = await getAccessToken();
const res = await fetch(`${GCAL_API}${path}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 204) return {};
const data = await res.json();
if (!res.ok) {
throw new Error(
`Calendar API ${method} ${path} failed (${res.status}): ${JSON.stringify(data)}`,
);
}
return data;
}
// --- Public API ---
export async function listEvents(days = 1) {
const now = new Date();
const timeMin = now.toISOString();
const end = new Date(now);
end.setDate(end.getDate() + days);
const timeMax = end.toISOString();
const params = new URLSearchParams({
timeMin,
timeMax,
singleEvents: "true",
orderBy: "startTime",
maxResults: "100",
});
return gcal("GET", `/calendars/${encodeURIComponent(calendarId())}/events?${params}`);
}
export async function createEvent(data: {
summary: string;
start_time: string;
end_time: string;
description?: string;
location?: string;
attendees?: string[];
}) {
const body: Record<string, unknown> = {
summary: data.summary,
start: { dateTime: data.start_time },
end: { dateTime: data.end_time },
};
if (data.description) body.description = data.description;
if (data.location) body.location = data.location;
if (data.attendees?.length) {
body.attendees = data.attendees.map((email) => ({ email }));
}
return gcal("POST", `/calendars/${encodeURIComponent(calendarId())}/events`, body);
}
export async function updateEvent(eventId: string, updates: Record<string, unknown>) {
const mapped: Record<string, unknown> = {};
if (updates.summary) mapped.summary = updates.summary;
if (updates.description) mapped.description = updates.description;
if (updates.location) mapped.location = updates.location;
if (updates.start_time) mapped.start = { dateTime: updates.start_time };
if (updates.end_time) mapped.end = { dateTime: updates.end_time };
if (Array.isArray(updates.attendees)) {
mapped.attendees = (updates.attendees as string[]).map((email) => ({ email }));
}
return gcal(
"PATCH",
`/calendars/${encodeURIComponent(calendarId())}/events/${encodeURIComponent(eventId)}`,
mapped,
);
}
export async function deleteEvent(eventId: string) {
return gcal(
"DELETE",
`/calendars/${encodeURIComponent(calendarId())}/events/${encodeURIComponent(eventId)}`,
);
}
/**
* Return calendar events where `email` appears in the attendees list, looking
* both backward and forward from today. Sorted by start desc, capped at 20.
*
* This backs snappy-knowledge.resolvePerson()'s `recent_calendar` field. The
* previous listEvents(days) forward-only scan is a hack — use this instead.
*
* Default window: 90 days back, 60 days forward (configurable).
*/
export async function eventsByAttendee(
email: string,
opts: { daysBack?: number; daysForward?: number; limit?: number } = {},
): Promise<any[]> {
const daysBack = opts.daysBack ?? 90;
const daysForward = opts.daysForward ?? 60;
const limit = opts.limit ?? 20;
const now = new Date();
const timeMin = new Date(now);
timeMin.setDate(timeMin.getDate() - daysBack);
const timeMax = new Date(now);
timeMax.setDate(timeMax.getDate() + daysForward);
const params = new URLSearchParams({
timeMin: timeMin.toISOString(),
timeMax: timeMax.toISOString(),
singleEvents: "true",
orderBy: "startTime",
maxResults: "250",
q: email, // server-side free-text hint; we still filter attendee list below
});
const data = await gcal("GET", `/calendars/${encodeURIComponent(calendarId())}/events?${params}`);
const items: any[] = Array.isArray(data?.items) ? data.items : [];
const needle = email.toLowerCase();
const matches = items.filter((ev) => {
const attendees: any[] = Array.isArray(ev?.attendees) ? ev.attendees : [];
return attendees.some((a) => (a?.email || "").toLowerCase() === needle);
});
matches.sort((a, b) => {
const ad = a?.start?.dateTime || a?.start?.date || "";
const bd = b?.start?.dateTime || b?.start?.date || "";
return bd.localeCompare(ad);
});
return matches.slice(0, limit);
}
export async function checkAvailability(days = 1) {
const now = new Date();
const end = new Date(now);
end.setDate(end.getDate() + days);
return gcal("POST", "/freeBusy", {
timeMin: now.toISOString(),
timeMax: end.toISOString(),
items: [{ id: calendarId() }],
});
}
// --- Scheduling (merged from snappy-scheduling) ---
const PREFERRED_WINDOWS = [
{ startHour: 11, endHour: 13 }, // 11 AM - 1 PM
{ startHour: 15, endHour: 17 }, // 3 PM - 5 PM
];
const HARD_END = 18;
const PROTECTED_END = 11; // 9-11 AM deep work
const BUFFER_MINUTES = 15;
/**
* Find available slots in the next N days that respect Robert's preferences.
* Returns proposed 30-min (or custom duration) slots.
*/
export async function proposeSlots(
days = 3,
durationMinutes = 30,
): Promise<Array<{ start: string; end: string; preferred: boolean }>> {
const events = await listEvents(days);
const busy: Array<{ start: number; end: number }> = [];
if (events.items) {
for (const ev of events.items) {
const s = ev.start?.dateTime ? new Date(ev.start.dateTime).getTime() : null;
const e = ev.end?.dateTime ? new Date(ev.end.dateTime).getTime() : null;
if (s && e) {
busy.push({
start: s - BUFFER_MINUTES * 60 * 1000,
end: e + BUFFER_MINUTES * 60 * 1000,
});
}
}
}
const slots: Array<{ start: string; end: string; preferred: boolean }> = [];
const now = new Date();
for (let d = 0; d < days; d++) {
const date = new Date(now);
date.setDate(date.getDate() + d);
const dow = date.getDay();
if (dow === 0 || dow === 6) continue; // skip weekends
if (dow === 5) continue; // skip Friday afternoon per preferences
for (let hour = PROTECTED_END; hour < HARD_END; hour++) {
const slotStart = new Date(date);
slotStart.setHours(hour, 0, 0, 0);
const slotEnd = new Date(slotStart.getTime() + durationMinutes * 60 * 1000);
if (
slotEnd.getHours() > HARD_END ||
(slotEnd.getHours() === HARD_END && slotEnd.getMinutes() > 0)
)
continue;
if (slotStart.getTime() < now.getTime()) continue;
const conflict = busy.some((b) => slotStart.getTime() < b.end && slotEnd.getTime() > b.start);
if (conflict) continue;
const preferred = PREFERRED_WINDOWS.some((w) => hour >= w.startHour && hour < w.endHour);
slots.push({
start: slotStart.toISOString(),
end: slotEnd.toISOString(),
preferred,
});
}
}
slots.sort((a, b) => (b.preferred ? 1 : 0) - (a.preferred ? 1 : 0));
return slots;
}
// --- CLI ---
if (
(() => {
try {
return import.meta.url === `file://${realpathSync(process.argv[1])}`;
} catch {
return false;
}
})()
) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "today": {
const data = await listEvents(1);
console.log(JSON.stringify(data, null, 2));
break;
}
case "events": {
const days = args[0] ? parseInt(args[0], 10) : 1;
const data = await listEvents(days);
console.log(JSON.stringify(data, null, 2));
break;
}
case "by-attendee": {
const [email] = args;
if (!email) {
console.error("Usage: api.ts by-attendee <email>");
process.exit(1);
}
const data = await eventsByAttendee(email);
console.log(JSON.stringify(data, null, 2));
break;
}
case "availability": {
const days = args[0] ? parseInt(args[0], 10) : 1;
const data = await checkAvailability(days);
console.log(JSON.stringify(data, null, 2));
break;
}
case "create": {
if (!args[0]) {
console.error(
'Usage: api.ts create \'{"summary":"...","start_time":"...","end_time":"..."}\'',
);
process.exit(1);
}
const data = await createEvent(JSON.parse(args[0]));
console.log(JSON.stringify(data, null, 2));
break;
}
case "update": {
if (!args[0]) {
console.error('Usage: api.ts update \'{"event_id":"...","summary":"..."}\'');
process.exit(1);
}
const parsed = JSON.parse(args[0]);
const { event_id, ...updates } = parsed;
const data = await updateEvent(event_id, updates);
console.log(JSON.stringify(data, null, 2));
break;
}
case "delete": {
if (!args[0]) {
console.error("Usage: api.ts delete <eventId>");
process.exit(1);
}
await deleteEvent(args[0]);
console.log("deleted");
break;
}
case "propose": {
const days = args[0] ? parseInt(args[0], 10) : 3;
const duration = args[1] ? parseInt(args[1], 10) : 30;
const slots = await proposeSlots(days, duration);
if (slots.length) {
console.log(
`Found ${slots.length} available ${duration}-min slots (next ${days} days):\n`,
);
for (const s of slots.slice(0, 10)) {
const d = new Date(s.start);
const day = d.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
const time = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
console.log(
` ${s.preferred ? "*" : " "} ${day} ${time}${s.preferred ? " (preferred)" : ""}`,
);
}
if (slots.length > 10) console.log(` ... and ${slots.length - 10} more`);
} else {
console.log("No available slots found.");
}
break;
}
default:
console.log(
"Usage: npx tsx api.ts [today|events|availability|create|update|delete|propose] ...",
);
}
})();
}
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-18 07:35Z | - | 0.50 | - | - |
| 2026-04-18 07:34Z | - | 0.50 | - | - |
| 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 | - | - |