@gotcos/glasses-server 6.43.4 → 6.44.1

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.
@@ -0,0 +1,152 @@
1
+ import { Router } from 'express'
2
+ import { queryJobCoordinator } from '../lib/query-job-runtime.js'
3
+ import { runTaskNow } from '../lib/task-dispatcher.js'
4
+ import {
5
+ TaskBridgeError,
6
+ TaskRunError,
7
+ captureTask,
8
+ checkTask,
9
+ listBoard,
10
+ listProjectedTaskRuns,
11
+ loadDispatchCap,
12
+ moveTask,
13
+ saveDispatchCap,
14
+ setTaskRunAt,
15
+ tasksGate,
16
+ workBadgeCount,
17
+ } from '../lib/task-store.js'
18
+
19
+ export const tasksRouter = Router()
20
+
21
+ function sendError(res: import('express').Response, error: unknown): void {
22
+ if (error instanceof TaskRunError) {
23
+ if (error.code === 'task_file_locked') res.setHeader('Retry-After', '2')
24
+ res.status(error.status).json({ error: { code: error.code, message: error.message } })
25
+ return
26
+ }
27
+ if (error instanceof TaskBridgeError) {
28
+ const status = error.code === 'task_file_locked' ? 503
29
+ : error.code === 'task_not_found' || error.code === 'ambiguous_text' ? (error.code === 'ambiguous_text' ? 409 : 404)
30
+ : error.code === 'cos_pipeline_not_configured' ? 503
31
+ : 400
32
+ if (error.code === 'task_file_locked') res.setHeader('Retry-After', '2')
33
+ res.status(status).json({ error: { code: error.code, message: error.message } })
34
+ return
35
+ }
36
+ console.error('[tasks]', error)
37
+ res.status(500).json({ error: { code: 'task_store_failed', message: 'Task store failed.' } })
38
+ }
39
+
40
+ function requireDomain(value: unknown): string {
41
+ if (typeof value !== 'string' || !value) throw new TaskRunError(400, 'invalid_domain', 'domain is required')
42
+ return value
43
+ }
44
+
45
+ tasksRouter.get('/tasks/runs', async (req, res) => {
46
+ if (tasksGate() === 'disabled') {
47
+ return res.status(503).json({ error: { code: 'cos_pipeline_not_configured', message: 'COS pipeline is not configured.' }, gate: 'disabled' })
48
+ }
49
+ const limit = Number.parseInt(String(req.query.limit ?? '20'), 10)
50
+ const runs = await listProjectedTaskRuns(
51
+ jobId => queryJobCoordinator.getSnapshot(jobId).catch(() => undefined),
52
+ Number.isFinite(limit) ? limit : 20,
53
+ )
54
+ res.json({ runs })
55
+ })
56
+
57
+ tasksRouter.get('/tasks/next', async (_req, res) => {
58
+ try {
59
+ const rows = await listBoard('today')
60
+ res.json({ task: rows[0] ?? null })
61
+ } catch (error) {
62
+ sendError(res, error)
63
+ }
64
+ })
65
+
66
+ tasksRouter.post('/tasks/capture', async (req, res) => {
67
+ try {
68
+ const body = req.body ?? {}
69
+ const result = await captureTask({
70
+ domain: requireDomain(body.domain),
71
+ text: String(body.text ?? ''),
72
+ section: String(body.section ?? 'inbox'),
73
+ runAt: typeof body.runAt === 'string' ? body.runAt : undefined,
74
+ captureId: typeof body.captureId === 'string' ? body.captureId : undefined,
75
+ })
76
+ res.status(result.replayed ? 200 : 200).json(result)
77
+ } catch (error) {
78
+ sendError(res, error)
79
+ }
80
+ })
81
+
82
+ tasksRouter.patch('/tasks/by-text', async (req, res) => {
83
+ try {
84
+ const body = req.body ?? {}
85
+ if (typeof body.domain !== 'string' || !body.domain) {
86
+ throw new TaskRunError(400, 'invalid_domain', 'domain is required')
87
+ }
88
+ if (typeof body.text !== 'string' || !body.text) {
89
+ throw new TaskRunError(400, 'text_required', 'text is required')
90
+ }
91
+ await checkTask({
92
+ domain: body.domain,
93
+ text: body.text,
94
+ checked: body.checked !== false,
95
+ })
96
+ res.json({ ok: true })
97
+ } catch (error) {
98
+ sendError(res, error)
99
+ }
100
+ })
101
+
102
+ tasksRouter.patch('/tasks/dispatch-cap', (req, res) => {
103
+ try {
104
+ const cap = Number((req.body ?? {}).capPerDay)
105
+ saveDispatchCap(cap)
106
+ res.json({ capPerDay: loadDispatchCap() })
107
+ } catch (error) {
108
+ sendError(res, error)
109
+ }
110
+ })
111
+
112
+ tasksRouter.get('/tasks', async (req, res) => {
113
+ try {
114
+ const column = typeof req.query.column === 'string' ? req.query.column : undefined
115
+ const rows = await listBoard(column)
116
+ res.json({ tasks: rows, workBadge: workBadgeCount(rows), gate: tasksGate() })
117
+ } catch (error) {
118
+ sendError(res, error)
119
+ }
120
+ })
121
+
122
+ tasksRouter.patch('/tasks/:id', async (req, res) => {
123
+ try {
124
+ const body = req.body ?? {}
125
+ const domain = requireDomain(body.domain)
126
+ if ('runAt' in body) {
127
+ await setTaskRunAt(domain, req.params.id, body.runAt == null || body.runAt === '' ? null : String(body.runAt))
128
+ } else if ('section' in body) {
129
+ await moveTask(domain, req.params.id, String(body.section))
130
+ } else if ('checked' in body) {
131
+ await checkTask({ domain, id: req.params.id, checked: body.checked === true })
132
+ } else {
133
+ throw new TaskRunError(400, 'invalid_patch', 'Expected runAt, section, or checked.')
134
+ }
135
+ res.json({ ok: true })
136
+ } catch (error) {
137
+ sendError(res, error)
138
+ }
139
+ })
140
+
141
+ tasksRouter.post('/tasks/:id/run', async (req, res) => {
142
+ try {
143
+ const domain = requireDomain((req.body ?? {}).domain)
144
+ const result = await runTaskNow(req.params.id, domain)
145
+ res.status(202).json(result)
146
+ } catch (error) {
147
+ if (error instanceof TaskRunError && error.code === 'dispatch_slots_busy') {
148
+ res.setHeader('Retry-After', '30')
149
+ }
150
+ sendError(res, error)
151
+ }
152
+ })