@satanwagen/reviewkit 0.1.0 → 0.1.2

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.
@@ -86,8 +86,10 @@ export function toFinding(item, seq) {
86
86
  };
87
87
  }
88
88
 
89
- /** Emblema status -> ReviewKit ItemStatus. */
90
- export const STATUS_MAP = { accepted: 'accepted', dismissed: 'rejected', fixed: 'applied' };
89
+ /** Emblema status -> ReviewKit ItemStatus. `open` REOPENS a finding (a fix
90
+ was abandoned or resolved by mistake) written into the room, so the
91
+ site pin reverts live. */
92
+ export const STATUS_MAP = { open: 'open', accepted: 'accepted', dismissed: 'rejected', fixed: 'applied' };
91
93
 
92
94
  function statePath(cwd) {
93
95
  return join(cwd, STATE_DIR, STATE_FILE);
@@ -294,11 +296,15 @@ export function cmdServe(cwd) {
294
296
  findings.set(item.id, toFinding(item, seq));
295
297
  queuePush(item);
296
298
  };
299
+ // Tombstone unconditionally: a delete for an id this process never saw (a
300
+ // ghost row left in the Emblema tile by an older/restarted agent) must still
301
+ // be reportable under `deleted`, or the row can never be cleared.
297
302
  const dropItem = (id) => {
298
- if (findings.delete(id)) {
299
- seq += 1;
300
- deletedIds.set(id, seq);
301
- }
303
+ // Already tombstoned and not live: nothing changed — don't churn the
304
+ // cursor (rooms replay their `deleted` list on every reconnect).
305
+ if (!findings.delete(id) && deletedIds.has(id)) return;
306
+ seq += 1;
307
+ deletedIds.set(id, seq);
302
308
  };
303
309
 
304
310
  /* -- realtime push to the Emblema intake (headers per contract v2) -- */
@@ -436,7 +442,7 @@ export function cmdServe(cwd) {
436
442
  /* -- HTTP API -- */
437
443
  const CORS = {
438
444
  'access-control-allow-origin': '*',
439
- 'access-control-allow-methods': 'GET, POST, OPTIONS',
445
+ 'access-control-allow-methods': 'GET, POST, DELETE, OPTIONS',
440
446
  'access-control-allow-headers': 'content-type, authorization, x-reviewkit-key-id',
441
447
  };
442
448
  const send = (res, status, body) => {
@@ -472,6 +478,13 @@ export function cmdServe(cwd) {
472
478
  url: state.url,
473
479
  lastAck: state.lastAck ?? null,
474
480
  findings: findings.size,
481
+ // Additive: realtime-bridge health for status UIs. "off" = no
482
+ // syncToken configured (bridge disabled by design), "connected" =
483
+ // at least one room socket joined, "connecting" = configured but
484
+ // not joined (booting, reconnect backoff, or a bad token).
485
+ realtime: !state.settings.syncToken
486
+ ? { state: 'off', rooms: 0 }
487
+ : { state: sockets.size > 0 ? 'connected' : 'connecting', rooms: sockets.size },
475
488
  });
476
489
  return;
477
490
  }
@@ -546,7 +559,15 @@ export function cmdServe(cwd) {
546
559
  return send(res, 400, { ok: false, error: 'invalid JSON' });
547
560
  }
548
561
  const mapped = STATUS_MAP[status];
549
- if (!mapped) return send(res, 400, { ok: false, error: 'status must be accepted|dismissed|fixed' });
562
+ if (!mapped) {
563
+ // Echo what arrived: the live 400s on `open` were an OLD installed
564
+ // agent, and a bare "must be …" message made that undiagnosable.
565
+ return send(res, 400, {
566
+ ok: false,
567
+ error: `status must be open|accepted|dismissed|fixed (received ${JSON.stringify(status ?? null)})`,
568
+ received: status ?? null,
569
+ });
570
+ }
550
571
  const finding = findings.get(decodeURIComponent(statusMatch[1]));
551
572
  if (!finding) return send(res, 404, { ok: false, error: 'unknown finding' });
552
573
  const ws = sockets.get(finding._route);
@@ -558,6 +579,24 @@ export function cmdServe(cwd) {
558
579
  });
559
580
  return;
560
581
  }
582
+ // DELETE /findings/{id} (POST …/delete for clients that cannot send DELETE).
583
+ // IDEMPOTENT: an unknown id still answers 200 and is tombstoned, so a ghost
584
+ // row left in the tile by an older/restarted agent can always be cleared.
585
+ // No body is read — the id is entirely in the path.
586
+ const deleteMatch =
587
+ (req.method === 'DELETE' && url.pathname.match(/^\/api\/emblema\/findings\/([^/]+)$/)) ||
588
+ (req.method === 'POST' && url.pathname.match(/^\/api\/emblema\/findings\/([^/]+)\/delete$/));
589
+ if (deleteMatch) {
590
+ const id = decodeURIComponent(deleteMatch[1]);
591
+ const finding = findings.get(id);
592
+ // Write-back first, while _route is still known: the site pin must
593
+ // disappear for every reviewer, not just in the tile.
594
+ const ws = finding && sockets.get(finding._route);
595
+ if (ws && ws.readyState === 1) ws.send(JSON.stringify({ t: 'item:delete', id }));
596
+ dropItem(id);
597
+ send(res, 200, { ok: true, id, known: !!finding });
598
+ return;
599
+ }
561
600
  send(res, 404, { ok: false, error: 'not found' });
562
601
  });
563
602
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@satanwagen/reviewkit",
3
- "version": "0.1.0",
4
- "description": "Drop-in visual review & feedback layer for React sites. Reviewers annotate the live page; nothing is ever applied \u2014 changes are recorded as a portable session for a coding agent.",
3
+ "version": "0.1.2",
4
+ "description": "Drop-in visual review & feedback layer for React sites. Reviewers annotate the live page; nothing is ever applied changes are recorded as a portable session for a coding agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -69,4 +69,4 @@
69
69
  "bin": {
70
70
  "reviewkit-emblema": "./cli/emblema-sync.mjs"
71
71
  }
72
- }
72
+ }