SLOPSHOPPER

lotto-ready

Standby readiness alert: flags when lottery_data.json is behind the draw schedule, with one-click refresh.

newbandcommandtoaststatusprocess
v0.1.0no licenseupdated 2026-10-26ranslu/lotto-oracle/.claude/skills/lotto-ready
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · lotto-ready
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /lotto-check ⎿ lotto-ready: can't read lottery_data.json ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ lotto-ready: 🎰 lotto-ready: can't read lottery_data.json
Source 3 files
hooks/register.tsx 91 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Readiness } from '../types'
5import { check } from './ready'
6
7const DATA = 'lottery_data.json'
8const EVERY = 30 * 60_000
9
10const readiness = atom({ plugin: 'lotto-ready', key: 'readiness' } as const, null)
11const isRefreshing = atom({ plugin: 'lotto-ready', key: 'isRefreshing' } as const, false)
12
13async function runCheck($: EngineInterface, isQuiet = false): Promise<Readiness> {
14  let result: Readiness
15  try {
16    result = check(JSON.parse(await $.fs.read(DATA)), await $.clock.now())
17  } catch (err) {
18    result = { checkedAt: '', behind: [], next: '', error: `can't read ${DATA}` }
19  }
20  const was = await read($, readiness)
21  await update($, readiness, () => result)
22
23  if (result.error) {
24    $.ui.status(`🎰 lotto-ready: ${result.error}`)
25  } else if (result.behind.length) {
26    $.ui.status(`🎰 ⚠ ${result.behind.length} game(s) behind · /lotto-refresh`)
27    const isNew = !was || was.behind.length !== result.behind.length
28    if (isNew && !isQuiet) {
29      $.ui.toast(`Lotto data behind: ${result.behind.map(l => l.game).join(', ')}`)
30    }
31  } else {
32    $.ui.status(`🎰 Lotto ready ✓ · next: ${result.next}`)
33  }
34  return result
35}
36
37async function refresh($: EngineInterface): Promise<string> {
38  if (await read($, isRefreshing)) return 'Refresh already running.'
39  await update($, isRefreshing, () => true)
40  $.ui.status('🎰 refreshing lottery data…')
41  try {
42    const ran = await $.process.run(['python3', 'fetch_lottery.py'], { timeoutMs: 180_000 })
43    const after = await runCheck($, true)
44    if (ran.exitCode !== 0) return `fetch_lottery.py failed (exit ${ran.exitCode}): ${ran.stderr.slice(-300)}`
45    return after.behind.length
46      ? `Fetched, still behind: ${after.behind.map(l => `${l.game} (have ${l.have}, want ${l.want})`).join('; ')}`
47      : `Fetched. All games current ✓ Next: ${after.next}`
48  } finally {
49    await update($, isRefreshing, () => false)
50  }
51}
52
53function describe(r: Readiness): string {
54  if (r.error) return r.error
55  if (!r.behind.length) return `All 5 games current ✓ Next draw: ${r.next}`
56  return `Behind:\n${r.behind.map(l => `- ${l.game}: have ${l.have}, due ${l.want}`).join('\n')}\nRun /lotto-refresh.`
57}
58
59export const register: Register = on => {
60  on('session.start', async ($, e, next) => {
61    await $.command.register({ name: 'lotto-check', description: 'Check lottery data readiness now' })
62    await $.command.register({ name: 'lotto-refresh', description: 'Run fetch_lottery.py and re-check' })
63    void runCheck($)
64    $.clock.every(EVERY, () => void runCheck($))
65    return next(e)
66  })
67
68  on('command.run', { command: 'lotto-check' }, async $ => ({ text: describe(await runCheck($)) }))
69  on('command.run', { command: 'lotto-refresh' }, async $ => ({ text: await refresh($) }))
70
71  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
72    const r = await read($, readiness)
73    if (e.props.hasSurvey || !r || !r.behind.length) return next(e)
74
75    const { Box, Button, Text } = $.ui.resolve(e)
76    const busy = await read($, isRefreshing)
77    return (
78      <Box>
79        <Text color="yellow">
80          🎰 New draws ready: {r.behind.map(l => l.game).join(', ')}{' '}
81        </Text>
82        <Button
83          key="refresh"
84          label={busy ? 'Refreshing…' : 'Refresh now'}
85          onPress={() => void refresh($)}
86        />
87      </Box>
88    )
89  })
90}
91
hooks/ready.ts 46 lines
1import type { Lag, Readiness } from '../types'
2
3// Draw weekdays (0 = Sunday). Draws run ~22:30 ET, so results count as due
4// from 04:00 UTC the following day.
5export const SCHEDULE: Record<string, { label: string; days: number[] }> = {
6  lotto_max: { label: 'Lotto Max', days: [2, 5] },
7  western_max: { label: 'Western Max', days: [2, 5] },
8  lotto_649: { label: 'Lotto 6/49', days: [3, 6] },
9  western_649: { label: 'Western 6/49', days: [3, 6] },
10  daily_grand: { label: 'Daily Grand', days: [1, 4] },
11}
12
13const DAY = 86_400_000
14const DUE_AFTER = 28 * 3_600_000 // draw date midnight UTC + 28h = 04:00 UTC next day
15const WEEKDAY = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
16
17const iso = (ms: number) => new Date(ms).toISOString().slice(0, 10)
18const midnight = (ms: number) => Math.floor(ms / DAY) * DAY
19
20export function latestDue(days: number[], now: number): string {
21  for (let d = midnight(now); ; d -= DAY) {
22    if (days.includes(new Date(d).getUTCDay()) && d + DUE_AFTER <= now) return iso(d)
23  }
24}
25
26export function nextDraw(now: number): string {
27  for (let d = midnight(now); ; d += DAY) {
28    if (d + DUE_AFTER <= now) continue
29    const day = new Date(d).getUTCDay()
30    const games = Object.values(SCHEDULE).filter(g => g.days.includes(day)).map(g => g.label)
31    if (games.length) return `${games.join(' + ')} ${WEEKDAY[day]}`
32  }
33}
34
35type Data = Record<string, { draws?: [string, ...unknown[]][] } | undefined>
36
37export function check(data: Data, now: number): Readiness {
38  const behind: Lag[] = []
39  for (const [key, { label, days }] of Object.entries(SCHEDULE)) {
40    const have = (data[key]?.draws ?? []).reduce((max, [date]) => (date > max ? date : max), '')
41    const want = latestDue(days, now)
42    if (have < want) behind.push({ game: label, have: have || 'none', want })
43  }
44  return { checkedAt: new Date(now).toISOString(), behind, next: nextDraw(now) }
45}
46
types/index.d.ts 9 lines
1export type Lag = { game: string; have: string; want: string }
2export type Readiness = { checkedAt: string; behind: Lag[]; next: string; error?: string }
3
4declare module 'claude-code' {
5  interface PluginState {
6    'lotto-ready': { readiness: Readiness | null; isRefreshing: boolean }
7  }
8}
9