-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdomain.js
More file actions
163 lines (150 loc) · 5.52 KB
/
Copy pathdomain.js
File metadata and controls
163 lines (150 loc) · 5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
export const REMINDERS_PATH = '/api/storage/shared/self-reminders.jsonl'
export const APP_SCHEDULES_PATH = '/api/apps/schedules'
const MILLISECOND_UNIX_THRESHOLD = 100_000_000_000
// The shared JSONL contract stores due_at and created_at as Unix seconds.
// Some producers have historically emitted obvious millisecond values; normalize
// those without rejecting otherwise valid records.
export function normalizeUnixSeconds(value) {
if (value == null || value === '') return 0
const n = Number(value)
if (!Number.isFinite(n)) return 0
return Math.abs(n) >= MILLISECOND_UNIX_THRESHOLD ? Math.trunc(n / 1000) : n
}
function normalizeReminder(record) {
return {
...record,
due_at: normalizeUnixSeconds(record.due_at),
created_at: normalizeUnixSeconds(record.created_at),
}
}
// Fold the append-only JSONL: last record per id wins.
export function foldReminders(text) {
const byId = new Map()
for (const line of (text || '').split('\n')) {
const s = line.trim()
if (!s) continue
try {
const r = JSON.parse(s)
if (r && r.id != null) byId.set(r.id, normalizeReminder(r))
} catch { /* tolerate a malformed line, keep the rest */ }
}
return [...byId.values()]
}
// Derived status: a pending task whose due time has passed is surfaced as
// "Needs Attention" rather than silently sitting active.
export function statusOf(task, now) {
if (task.status === 'cancelled') return { key: 'cancelled', label: 'Cancelled', tone: 'muted', rank: 3 }
if (task.status === 'done') return { key: 'done', label: 'Done', tone: 'done', rank: 2 }
const due = normalizeUnixSeconds(task.due_at) * 1000
if (due && due <= now) return { key: 'attention', label: 'Needs Attention', tone: 'attention', rank: 0 }
return { key: 'scheduled', label: 'Scheduled', tone: 'active', rank: 1 }
}
export function sortTasks(tasks, now) {
return (tasks || [])
.map((t) => ({ ...t, _s: statusOf(t, now) }))
.sort((a, b) => (a._s.rank - b._s.rank) || ((a.due_at || Infinity) - (b.due_at || Infinity)))
}
export function summarizeTasks(tasks, now) {
const sorted = sortTasks(tasks, now)
return {
item_count: sorted.length,
attention_count: sorted.filter((t) => t._s.key === 'attention').length,
done_count: sorted.filter((t) => t._s.key === 'done').length,
}
}
export function friendlyLoadError(err, online = true) {
const raw = String(err?.message || err || 'Could not load tasks')
const offline = online === false || /failed to fetch|networkerror|network request failed/i.test(raw)
if (offline) {
return {
title: 'Offline',
message: 'Tasks are unavailable while this app is offline. Reconnect and try again.',
raw,
offline: true,
}
}
if (/^load\s+\d+/.test(raw)) {
return {
title: "Couldn't load tasks",
message: 'Task storage did not respond cleanly. Try again in a moment.',
raw,
offline: false,
}
}
return {
title: "Couldn't load tasks",
message: 'Tasks could not be refreshed. Try again in a moment.',
raw,
offline: false,
}
}
export function friendlyScheduleLoadError(err) {
const raw = String(err?.message || err || 'Could not load schedules')
return {
title: "Couldn't load schedules",
message: 'Scheduled app jobs could not be refreshed. Try again in a moment.',
raw,
offline: /failed to fetch|networkerror|network request failed/i.test(raw),
}
}
export async function readTasks({ fetchImpl, authHeaders, previousTasks, signal, online }) {
try {
const res = await fetchImpl(REMINDERS_PATH, { headers: authHeaders })
// app_ready is emitted by the component once BOTH reads resolve, so it can
// carry schedule_count alongside the task counts.
if (res.status === 404) {
return { tasks: [], error: null, retained: false }
}
if (!res.ok) throw new Error(`load ${res.status}`)
const text = await res.text()
const tasks = foldReminders(text)
return { tasks, error: null, retained: false }
} catch (err) {
signal?.('error', { message: String(err?.message || err), source: 'load' })
const fallback = Array.isArray(previousTasks) ? previousTasks : []
return {
tasks: fallback,
error: friendlyLoadError(err, online),
retained: Array.isArray(previousTasks),
}
}
}
function normalizeSchedule(record) {
if (!record || record.id == null) return null
const cron = typeof record.cron === 'string' ? record.cron.trim() : ''
if (!cron) return null
return {
id: record.id,
name: record.name || 'Untitled app',
slug: record.slug || '',
cron,
job: record.job || 'job.sh',
next_run: record.next_run || null,
}
}
export function sortSchedules(schedules) {
return (schedules || [])
.map(normalizeSchedule)
.filter(Boolean)
.sort((a, b) => String(a.name).localeCompare(String(b.name)) || Number(a.id) - Number(b.id))
}
export async function readSchedules({ fetchImpl, authHeaders, previousSchedules, signal }) {
try {
const res = await fetchImpl(APP_SCHEDULES_PATH, { headers: authHeaders })
if (!res.ok) throw new Error(`load schedules ${res.status}`)
const raw = await res.json()
return {
schedules: sortSchedules(Array.isArray(raw) ? raw : []),
error: null,
retained: false,
}
} catch (err) {
signal?.('error', { message: String(err?.message || err), source: 'schedule_load' })
const fallback = Array.isArray(previousSchedules) ? previousSchedules : []
return {
schedules: fallback,
error: friendlyScheduleLoadError(err),
retained: Array.isArray(previousSchedules),
}
}
}