Skip to content

Commit 30ac091

Browse files
ADFA-4838: REST endpoints + wire the job engine into server.ts
- routes.ts: POST /api/:type/download, GET /api/:type/jobs(/:id), POST .../cancel. Stateless calls over the durable engine; clients re-attach by polling the id. - server.ts: mount express.json() + /api router, import the kiwix runner (registers it), and call jobs.reconcileOnBoot() on listen so interrupted jobs resume. - nginx: add a localhost-only /api/ proxy to :4000 (mirrors the /dashboard/ + /socket.io/ locations). Kiwix now works end-to-end over REST. maps/books runners and retiring the socket-owned job paths are the next steps. Typechecks (tsc --noEmit).
1 parent 2a42742 commit 30ac091

3 files changed

Lines changed: 92 additions & 1 deletion

File tree

‎static/dashboard/dash-node-nginx.conf‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ location ^~ /dashboard/ {
1515
proxy_set_header X-Forwarded-Proto $scheme;
1616
}
1717

18+
# Proxy for the REST content API (ADFA-4838, localhost only)
19+
location ^~ /api/ {
20+
allow 127.0.0.1;
21+
allow ::1;
22+
deny all;
23+
24+
error_page 403 =404 /404.html;
25+
26+
proxy_pass http://127.0.0.1:4000/api/;
27+
proxy_set_header Host $http_host;
28+
proxy_set_header X-Real-IP $remote_addr;
29+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
30+
proxy_set_header X-Forwarded-Proto $scheme;
31+
}
32+
1833
# Proxy for WebSockets (socket.io uses it by default)
1934
location ^~ /socket.io/ {
2035
# Basic security (localhost only)

‎static/dashboard/routes.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// routes.ts — ADFA-4838
2+
//
3+
// REST surface over the durable job engine. Short, stateless calls the app (and the
4+
// web UI) use instead of a long-lived socket: start a job, poll its structured status,
5+
// cancel it. The job itself lives in the dashboard process (see sockets/jobs.ts), so
6+
// none of these calls hold state — a client can drop and re-attach by polling the id.
7+
import express, { Router, Request, Response } from 'express';
8+
import { jobs, Job, JobType } from './sockets/jobs';
9+
10+
const VALID_TYPES: JobType[] = ['kiwix', 'maps', 'books'];
11+
function isType(t: string): t is JobType {
12+
return (VALID_TYPES as string[]).includes(t);
13+
}
14+
15+
/** Public shape returned to clients: ids as an array, no raw JSON column. */
16+
function toApi(job: Job) {
17+
let ids: string[] = [];
18+
try { ids = JSON.parse(job.target) as string[]; } catch { ids = []; }
19+
return {
20+
id: job.id,
21+
type: job.type,
22+
ids,
23+
phase: job.phase,
24+
percent: job.percent,
25+
speed: job.speed,
26+
detail: job.detail,
27+
error: job.error,
28+
updated: job.updated,
29+
};
30+
}
31+
32+
export const apiRouter: Router = express.Router();
33+
34+
// Start a content job → 202 { ...job }
35+
apiRouter.post('/:type/download', (req: Request, res: Response): void => {
36+
const type = String(req.params.type);
37+
if (!isType(type)) { res.status(404).json({ error: 'unknown type' }); return; }
38+
const body = req.body as { ids?: unknown };
39+
const ids = Array.isArray(body?.ids) ? body.ids.map((x) => String(x)) : [];
40+
if (ids.length === 0) { res.status(400).json({ error: 'ids required' }); return; }
41+
res.status(202).json(toApi(jobs.create(type, ids)));
42+
});
43+
44+
// Poll one job's structured status.
45+
apiRouter.get('/:type/jobs/:id', (req: Request, res: Response): void => {
46+
const job = jobs.get(String(req.params.id));
47+
if (!job || job.type !== String(req.params.type)) { res.status(404).json({ error: 'not found' }); return; }
48+
res.json(toApi(job));
49+
});
50+
51+
// List a type's jobs (most recent first).
52+
apiRouter.get('/:type/jobs', (req: Request, res: Response): void => {
53+
const type = String(req.params.type);
54+
if (!isType(type)) { res.status(404).json({ error: 'unknown type' }); return; }
55+
res.json(jobs.list(type).map(toApi));
56+
});
57+
58+
// Cancel a running job.
59+
apiRouter.post('/:type/jobs/:id/cancel', (req: Request, res: Response): void => {
60+
const job = jobs.get(String(req.params.id));
61+
if (!job || job.type !== String(req.params.type)) { res.status(404).json({ error: 'not found' }); return; }
62+
jobs.cancel(job.id);
63+
res.json({ ok: true });
64+
});

‎static/dashboard/server.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import { handleKiwixEvents } from './sockets/kiwix.socket';
1010
import { handleHomeEvents } from './sockets/home.socket';
1111
import { handleBooksEvents } from './sockets/books.socket';
1212

13+
// ADFA-4838: durable REST job engine. Importing the runner modules registers them with
14+
// the shared JobManager; the router exposes them over /api.
15+
import { jobs } from './sockets/jobs';
16+
import './sockets/kiwix.exec';
17+
import { apiRouter } from './routes';
18+
1319
const app = express();
1420
const server = http.createServer(app);
1521
const io = new Server(server);
@@ -28,9 +34,13 @@ app.set('view engine', 'ejs');
2834
app.set('views', path.join(__dirname, 'views'));
2935
app.use(express.static(path.join(__dirname, 'public')));
3036

37+
// ADFA-4838: JSON body parsing + the REST content API.
38+
app.use(express.json());
39+
app.use('/api', apiRouter);
40+
3141
// Main route
3242
app.get('/', (req, res) => {
33-
res.render('index');
43+
res.render('index');
3444
});
3545

3646
// Main Socket Connection Handler
@@ -53,6 +63,8 @@ server.listen(PORT, () => {
5363
console.log(`===========================================`);
5464
console.log(`K2Go Dashboard active on port ${PORT}`);
5565
console.log(`===========================================`);
66+
// ADFA-4838: resume any content jobs that were mid-flight before a restart.
67+
try { jobs.reconcileOnBoot(); } catch (e) { console.error('[jobs] reconcile failed', e); }
5668
});
5769

5870
// ==========================================

0 commit comments

Comments
 (0)