@agentdeck/bridge 1.0.5 → 1.0.7

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.
Files changed (128) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/apme/collector.d.ts +10 -0
  3. package/dist/apme/collector.d.ts.map +1 -1
  4. package/dist/apme/collector.js +39 -10
  5. package/dist/apme/collector.js.map +1 -1
  6. package/dist/apme/dashboard-html.d.ts.map +1 -1
  7. package/dist/apme/dashboard-html.js +289 -0
  8. package/dist/apme/dashboard-html.js.map +1 -1
  9. package/dist/apme/graph.d.ts +48 -0
  10. package/dist/apme/graph.d.ts.map +1 -0
  11. package/dist/apme/graph.js +226 -0
  12. package/dist/apme/graph.js.map +1 -0
  13. package/dist/apme/http.d.ts.map +1 -1
  14. package/dist/apme/http.js +52 -0
  15. package/dist/apme/http.js.map +1 -1
  16. package/dist/apme/judge-detect.d.ts.map +1 -1
  17. package/dist/apme/judge-detect.js +26 -2
  18. package/dist/apme/judge-detect.js.map +1 -1
  19. package/dist/apme/store.d.ts +69 -1
  20. package/dist/apme/store.d.ts.map +1 -1
  21. package/dist/apme/store.js +238 -4
  22. package/dist/apme/store.js.map +1 -1
  23. package/dist/apme/types.d.ts +1 -1
  24. package/dist/apme/types.d.ts.map +1 -1
  25. package/dist/apme/types.js.map +1 -1
  26. package/dist/ble-sync-spawn.d.ts +25 -2
  27. package/dist/ble-sync-spawn.d.ts.map +1 -1
  28. package/dist/ble-sync-spawn.js +38 -2
  29. package/dist/ble-sync-spawn.js.map +1 -1
  30. package/dist/ble-sync-status.d.ts +77 -0
  31. package/dist/ble-sync-status.d.ts.map +1 -0
  32. package/dist/ble-sync-status.js +157 -0
  33. package/dist/ble-sync-status.js.map +1 -0
  34. package/dist/bridge-core.d.ts.map +1 -1
  35. package/dist/bridge-core.js +7 -1
  36. package/dist/bridge-core.js.map +1 -1
  37. package/dist/cli.js +0 -0
  38. package/dist/codex-rate-limits-live.d.ts +60 -0
  39. package/dist/codex-rate-limits-live.d.ts.map +1 -0
  40. package/dist/codex-rate-limits-live.js +282 -0
  41. package/dist/codex-rate-limits-live.js.map +1 -0
  42. package/dist/d200h/hid-protocol.d.ts +62 -0
  43. package/dist/d200h/hid-protocol.d.ts.map +1 -0
  44. package/dist/d200h/hid-protocol.js +124 -0
  45. package/dist/d200h/hid-protocol.js.map +1 -0
  46. package/dist/d200h/image-renderer.d.ts +24 -0
  47. package/dist/d200h/image-renderer.d.ts.map +1 -0
  48. package/dist/d200h/image-renderer.js +321 -0
  49. package/dist/d200h/image-renderer.js.map +1 -0
  50. package/dist/daemon-server.d.ts.map +1 -1
  51. package/dist/daemon-server.js +163 -10
  52. package/dist/daemon-server.js.map +1 -1
  53. package/dist/daemon.js +1 -1
  54. package/dist/device-voice-reply.d.ts +1 -10
  55. package/dist/device-voice-reply.d.ts.map +1 -1
  56. package/dist/device-voice-reply.js +4 -33
  57. package/dist/device-voice-reply.js.map +1 -1
  58. package/dist/foundation-models-helper.d.ts.map +1 -1
  59. package/dist/foundation-models-helper.js +59 -22
  60. package/dist/foundation-models-helper.js.map +1 -1
  61. package/dist/hook-migration.d.ts +2 -0
  62. package/dist/hook-migration.d.ts.map +1 -0
  63. package/dist/hook-migration.js +116 -0
  64. package/dist/hook-migration.js.map +1 -0
  65. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +8 -0
  66. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
  67. package/dist/idotmatrix/idotmatrix-daemon-sync.js +19 -3
  68. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  69. package/dist/modules/d200h-module.d.ts +65 -0
  70. package/dist/modules/d200h-module.d.ts.map +1 -0
  71. package/dist/modules/d200h-module.js +495 -0
  72. package/dist/modules/d200h-module.js.map +1 -0
  73. package/dist/modules/d200h-renderer.d.ts +7 -0
  74. package/dist/modules/d200h-renderer.d.ts.map +1 -0
  75. package/dist/modules/d200h-renderer.js +321 -0
  76. package/dist/modules/d200h-renderer.js.map +1 -0
  77. package/dist/modules/idotmatrix-module.d.ts.map +1 -1
  78. package/dist/modules/idotmatrix-module.js +12 -1
  79. package/dist/modules/idotmatrix-module.js.map +1 -1
  80. package/dist/modules/timebox-module.d.ts.map +1 -1
  81. package/dist/modules/timebox-module.js +12 -1
  82. package/dist/modules/timebox-module.js.map +1 -1
  83. package/dist/modules/trmnl-module.d.ts +38 -0
  84. package/dist/modules/trmnl-module.d.ts.map +1 -0
  85. package/dist/modules/trmnl-module.js +111 -0
  86. package/dist/modules/trmnl-module.js.map +1 -0
  87. package/dist/openclaw-session.d.ts +6 -0
  88. package/dist/openclaw-session.d.ts.map +1 -1
  89. package/dist/openclaw-session.js +7 -1
  90. package/dist/openclaw-session.js.map +1 -1
  91. package/dist/timebox/timebox-daemon-sync.d.ts +8 -0
  92. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
  93. package/dist/timebox/timebox-daemon-sync.js +29 -1
  94. package/dist/timebox/timebox-daemon-sync.js.map +1 -1
  95. package/dist/timeline-summarizer.d.ts +12 -2
  96. package/dist/timeline-summarizer.d.ts.map +1 -1
  97. package/dist/timeline-summarizer.js +66 -4
  98. package/dist/timeline-summarizer.js.map +1 -1
  99. package/dist/trmnl/byos-server.d.ts +39 -0
  100. package/dist/trmnl/byos-server.d.ts.map +1 -0
  101. package/dist/trmnl/byos-server.js +301 -0
  102. package/dist/trmnl/byos-server.js.map +1 -0
  103. package/dist/trmnl/frame-cache.d.ts +37 -0
  104. package/dist/trmnl/frame-cache.d.ts.map +1 -0
  105. package/dist/trmnl/frame-cache.js +157 -0
  106. package/dist/trmnl/frame-cache.js.map +1 -0
  107. package/dist/trmnl/image-renderer.d.ts +27 -0
  108. package/dist/trmnl/image-renderer.d.ts.map +1 -0
  109. package/dist/trmnl/image-renderer.js +235 -0
  110. package/dist/trmnl/image-renderer.js.map +1 -0
  111. package/dist/trmnl/trmnl-settings.d.ts +96 -0
  112. package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
  113. package/dist/trmnl/trmnl-settings.js +177 -0
  114. package/dist/trmnl/trmnl-settings.js.map +1 -0
  115. package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
  116. package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
  117. package/dist/trmnl/trmnl-telemetry.js +56 -0
  118. package/dist/trmnl/trmnl-telemetry.js.map +1 -0
  119. package/dist/whisper-server-manager.d.ts +19 -0
  120. package/dist/whisper-server-manager.d.ts.map +1 -0
  121. package/dist/whisper-server-manager.js +171 -0
  122. package/dist/whisper-server-manager.js.map +1 -0
  123. package/fm-helper/Info.plist +27 -0
  124. package/package.json +13 -14
  125. package/scripts/build-fm-helper.mjs +35 -3
  126. package/src/idotmatrix/sync.py +16 -1
  127. package/src/pysync/matrix_sync_common.py +83 -0
  128. package/src/timebox/sync_ble.py +16 -0
@@ -1 +1 @@
1
- {"version":3,"file":"openclaw-session.js","sourceRoot":"","sources":["../src/openclaw-session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAahF;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAA2B,EAC3B,IAA2B;IAE3B,IAAI,CAAC,uBAAuB,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC3F,IAAI,kBAAkB,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAClD,MAAM,QAAQ,GAAoB;QAChC,EAAE,EAAE,kBAAkB;QACtB,IAAI,EAAE,KAAK;QACX,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,UAAU;QAC3C,SAAS,EAAE,UAAU;QACrB,KAAK,EAAE,IAAI;KACZ,CAAC;IACF,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IAC1D,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;QAAE,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACtE,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;IAC5E,OAAO,CAAC,GAAG,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACjC,CAAC"}
1
+ {"version":3,"file":"openclaw-session.js","sourceRoot":"","sources":["../src/openclaw-session.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,kBAAkB,CAAC;AAYtD;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAA2B,EAC3B,IAA2B;IAE3B,IAAI,CAAC,uBAAuB,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC3F,IAAI,kBAAkB,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IAClD,MAAM,QAAQ,GAAoB;QAChC,EAAE,EAAE,mBAAmB;QACvB,IAAI,EAAE,KAAK;QACX,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,UAAU;QAC3C,SAAS,EAAE,UAAU;QACrB,KAAK,EAAE,IAAI;KACZ,CAAC;IACF,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS;QAAE,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IAC1D,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;QAAE,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACtE,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,QAAQ,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;IAC5E,OAAO,CAAC,GAAG,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACjC,CAAC"}
@@ -5,6 +5,7 @@
5
5
  * (bleak/GATT). Node has no built-in BLE support, so we spawn the small Python
6
6
  * writer. This code is terminal-managed CLI daemon only.
7
7
  */
8
+ import { type BleLinkSnapshot } from '../ble-sync-status.js';
8
9
  export declare function startTimeboxSync(httpPort: number): void;
9
10
  /**
10
11
  * Stop all managed Timebox BLE syncs and cancel pending respawns.
@@ -16,4 +17,11 @@ export declare function startTimeboxSync(httpPort: number): void;
16
17
  * awaited in parallel (BLE is per-device, so farewells don't serialize).
17
18
  */
18
19
  export declare function stopTimeboxSync(awaitFarewell?: boolean): Promise<void>;
20
+ /**
21
+ * Live link state for the configured Timebox panels, in the `BLEMatrixHealth`
22
+ * wire shape. Called by `TimeboxModule.statusSnapshot()` so `/health` reports
23
+ * whether the panel is actually being driven — the configured-device list alone
24
+ * left every dashboard drawing a streaming panel as disconnected.
25
+ */
26
+ export declare function timeboxLinkSnapshot(configuredDeviceCount: number): BleLinkSnapshot;
19
27
  //# sourceMappingURL=timebox-daemon-sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"timebox-daemon-sync.d.ts","sourceRoot":"","sources":["../../src/timebox/timebox-daemon-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA4BH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAyBvD;AAiDD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwB1E"}
1
+ {"version":3,"file":"timebox-daemon-sync.d.ts","sourceRoot":"","sources":["../../src/timebox/timebox-daemon-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,uBAAuB,CAAC;AA6B/B,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CA8BvD;AAqDD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB1E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,qBAAqB,EAAE,MAAM,GAAG,eAAe,CAWlF"}
@@ -7,8 +7,11 @@
7
7
  */
8
8
  import { deviceId, loadTimeboxDevices } from './timebox-settings.js';
9
9
  import { createSyncCycleSquelch, spawnPythonSync, terminateSyncChild } from '../ble-sync-spawn.js';
10
+ import { createBleLinkTracker, mergeBleLinkSnapshots, } from '../ble-sync-status.js';
10
11
  import { getBleRuntimeStatus } from '../python-ble-runtime.js';
11
12
  const entries = new Map();
13
+ /** Set when no child could be spawned at all (missing BLE runtime). */
14
+ let unavailableReason = null;
12
15
  const BASE_BACKOFF_MS = 5_000;
13
16
  const MAX_BACKOFF_MS = 60_000;
14
17
  const HEALTHY_UPTIME_MS = 30_000;
@@ -22,8 +25,12 @@ export function startTimeboxSync(httpPort) {
22
25
  const runtime = getBleRuntimeStatus();
23
26
  if (!runtime.ready || !runtime.python) {
24
27
  log(`sync unavailable (${runtime.reason}); run \`agentdeck ble setup\``);
28
+ // Register the reason so /health can say *why* the panel is dark instead of
29
+ // reporting a bare `connected:false` with no explanation.
30
+ unavailableReason = `BLE sync unavailable: ${runtime.reason}`;
25
31
  return;
26
32
  }
33
+ unavailableReason = null;
27
34
  for (const device of devices) {
28
35
  const id = deviceId(device);
29
36
  if (!id || entries.has(id))
@@ -36,6 +43,7 @@ export function startTimeboxSync(httpPort) {
36
43
  consecutiveFailures: 0,
37
44
  startedAt: 0,
38
45
  squelch: createSyncCycleSquelch(log),
46
+ link: createBleLinkTracker(),
39
47
  };
40
48
  entries.set(id, entry);
41
49
  spawnSync(entry, runtime.python, runtime.paths.scripts.timeboxSync, httpPort);
@@ -51,9 +59,12 @@ function spawnSync(entry, venvPython, syncScript, httpPort) {
51
59
  const args = [syncScript, '--address', device.address, '--url', url, '--brightness', String(brightness)];
52
60
  entry.squelch.logStart(`Starting BLE sync for ${device.name ?? 'Timebox Mini'} (${id}, bridge ${url}, brightness ${brightness}%)`);
53
61
  entry.startedAt = Date.now();
62
+ entry.link.noteSpawn();
54
63
  // stdout/stderr are captured into small rings so clean exits and crashes both
55
64
  // leave enough context without flooding the daemon log while running.
56
- const { proc, stderrTail, outputTail } = spawnPythonSync(venvPython, args);
65
+ const { proc, stderrTail, outputTail } = spawnPythonSync(venvPython, args, {
66
+ onStatus: (payload) => entry.link.applyStatusLine(payload),
67
+ });
57
68
  entry.child = proc;
58
69
  proc.on('error', (err) => {
59
70
  log(`sync failed to spawn for ${id}: ${err.message}`);
@@ -74,6 +85,7 @@ function spawnSync(entry, venvPython, syncScript, httpPort) {
74
85
  const delay = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * entry.consecutiveFailures);
75
86
  const tail = stderrTail() || outputTail();
76
87
  const why = tail ? `; output: ${tail}` : '';
88
+ entry.link.noteExit(tail || `sync exited (code=${code} signal=${signal})`, Date.now() + delay);
77
89
  entry.squelch.logExit(code, signal, uptimeMs, tail, `sync for ${id} exited (code=${code} signal=${signal})${why}; respawning in ${Math.round(delay / 1000)}s`);
78
90
  entry.respawnTimer = setTimeout(() => spawnSync(entry, venvPython, syncScript, httpPort), delay);
79
91
  if (entry.respawnTimer.unref)
@@ -114,7 +126,23 @@ export async function stopTimeboxSync(awaitFarewell = false) {
114
126
  }
115
127
  }
116
128
  entries.clear();
129
+ unavailableReason = null;
117
130
  if (procs.length)
118
131
  await Promise.all(procs.map((p) => terminateSyncChild(p)));
119
132
  }
133
+ /**
134
+ * Live link state for the configured Timebox panels, in the `BLEMatrixHealth`
135
+ * wire shape. Called by `TimeboxModule.statusSnapshot()` so `/health` reports
136
+ * whether the panel is actually being driven — the configured-device list alone
137
+ * left every dashboard drawing a streaming panel as disconnected.
138
+ */
139
+ export function timeboxLinkSnapshot(configuredDeviceCount) {
140
+ const merged = mergeBleLinkSnapshots([...entries.values()].map((e) => e.link.snapshot(configuredDeviceCount)), configuredDeviceCount);
141
+ // A configured device with no entry at all means the supervisor never got to
142
+ // spawn anything — surface that instead of an unexplained `connected:false`.
143
+ if (unavailableReason && configuredDeviceCount > 0 && !merged.connected) {
144
+ return { ...merged, statusReason: unavailableReason };
145
+ }
146
+ return merged;
147
+ }
120
148
  //# sourceMappingURL=timebox-daemon-sync.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"timebox-daemon-sync.js","sourceRoot":"","sources":["../../src/timebox/timebox-daemon-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAsB,MAAM,uBAAuB,CAAC;AACzF,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,kBAAkB,EAAyB,MAAM,sBAAsB,CAAC;AAC1H,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAa/D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAqB,CAAC;AAE7C,MAAM,eAAe,GAAG,KAAK,CAAC;AAC9B,MAAM,cAAc,GAAG,MAAM,CAAC;AAC9B,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAEjC,SAAS,GAAG,CAAC,GAAW;IACtB,OAAO,CAAC,KAAK,CAAC,yBAAyB,GAAG,EAAE,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IACrC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEjC,MAAM,OAAO,GAAG,mBAAmB,EAAE,CAAC;IACtC,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACtC,GAAG,CAAC,qBAAqB,OAAO,CAAC,MAAM,gCAAgC,CAAC,CAAC;QACzE,OAAO;IACT,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC5B,IAAI,CAAC,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,SAAS;QACrC,MAAM,KAAK,GAAc;YACvB,MAAM;YACN,KAAK,EAAE,IAAI;YACX,QAAQ,EAAE,KAAK;YACf,YAAY,EAAE,IAAI;YAClB,mBAAmB,EAAE,CAAC;YACtB,SAAS,EAAE,CAAC;YACZ,OAAO,EAAE,sBAAsB,CAAC,GAAG,CAAC;SACrC,CAAC;QACF,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACvB,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAChF,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,KAAgB,EAAE,UAAkB,EAAE,UAAkB,EAAE,QAAgB;IAC3F,IAAI,KAAK,CAAC,QAAQ;QAAE,OAAO;IAC3B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,GAAG,GAAG,oBAAoB,QAAQ,EAAE,CAAC;IAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAEzG,KAAK,CAAC,OAAO,CAAC,QAAQ,CACpB,yBAAyB,MAAM,CAAC,IAAI,IAAI,cAAc,KAAK,EAAE,YAAY,GAAG,gBAAgB,UAAU,IAAI,CAC3G,CAAC;IACF,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAE7B,8EAA8E;IAC9E,sEAAsE;IACtE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC3E,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;IAEnB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;QAC9B,GAAG,CAAC,4BAA4B,EAAE,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAmB,EAAE,MAA6B,EAAE,EAAE;QACrE,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI;YAAE,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;QAC7C,IAAI,KAAK,CAAC,QAAQ;YAAE,OAAO;QAC3B,wEAAwE;QACxE,qEAAqE;QACrE,0EAA0E;QAC1E,gDAAgD;QAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC;QAC9C,IAAI,IAAI,KAAK,CAAC,IAAI,QAAQ,GAAG,iBAAiB;YAAE,KAAK,CAAC,mBAAmB,GAAG,CAAC,CAAC;QAC9E,KAAK,CAAC,mBAAmB,IAAI,CAAC,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,eAAe,GAAG,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACpF,MAAM,IAAI,GAAG,UAAU,EAAE,IAAI,UAAU,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,KAAK,CAAC,OAAO,CAAC,OAAO,CACnB,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,YAAY,EAAE,iBAAiB,IAAI,WAAW,MAAM,IAAI,GAAG,mBAAmB,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAC1G,CAAC;QACF,KAAK,CAAC,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,KAAK,CAAC,CAAC;QACjG,IAAI,KAAK,CAAC,YAAY,CAAC,KAAK;YAAE,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,aAAa,GAAG,KAAK;IACzD,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACrC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC;QACtB,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACvB,YAAY,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YACjC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;QAC5B,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC;YACzB,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;YACnB,IAAI,aAAa,EAAE,CAAC;gBAClB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC;oBACH,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACvB,CAAC;gBAAC,MAAM,CAAC;oBACP,kBAAkB;gBACpB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,CAAC,KAAK,EAAE,CAAC;IAChB,IAAI,KAAK,CAAC,MAAM;QAAE,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/E,CAAC"}
1
+ {"version":3,"file":"timebox-daemon-sync.js","sourceRoot":"","sources":["../../src/timebox/timebox-daemon-sync.ts"],"names":[],"mappings":"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"}
@@ -2,10 +2,20 @@
2
2
  * Timeline summarizer — uses local LLM to create concise 1-line summaries
3
3
  * of OpenClaw chat responses for timeline display.
4
4
  *
5
- * Tries: local mlx-serve qwen (port 8800) → Ollama → heuristic fallback.
6
- * Non-blocking — caller should fire-and-forget, update entry when ready.
5
+ * Tries: on-device Foundation Models → local mlx-serve qwen (port 8800) →
6
+ * Ollama → heuristic fallback. Non-blocking — caller should fire-and-forget,
7
+ * update entry when ready.
8
+ *
9
+ * The provider order is the Node mirror of Swift's `TimelineSummarizer`
10
+ * `.auto` chain (FoundationModels → MLX → Ollama → heuristic). It was
11
+ * MLX-first for a long time, which meant a CLI-only user on macOS 26 with
12
+ * Apple Intelligence available still got heuristic rows unless they also ran
13
+ * an MLX server — the same machine summarized differently depending on which
14
+ * daemon happened to be up. Keep the two chains in the same order.
7
15
  */
8
16
  export { extractTopicHint } from '@agentdeck/shared';
17
+ /** Reset the cached provider availability (tests only). */
18
+ export declare function clearSummarizerProviderCacheForTests(): void;
9
19
  /**
10
20
  * Summarize a chat response into a concise 1-line Korean summary.
11
21
  * Returns null if summarization fails (caller should use fallback).
@@ -1 +1 @@
1
- {"version":3,"file":"timeline-summarizer.d.ts","sourceRoot":"","sources":["../src/timeline-summarizer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAiCrD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoE5E"}
1
+ {"version":3,"file":"timeline-summarizer.d.ts","sourceRoot":"","sources":["../src/timeline-summarizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAmCrD,2DAA2D;AAC3D,wBAAgB,oCAAoC,IAAI,IAAI,CAS3D;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyF5E"}
@@ -2,12 +2,21 @@
2
2
  * Timeline summarizer — uses local LLM to create concise 1-line summaries
3
3
  * of OpenClaw chat responses for timeline display.
4
4
  *
5
- * Tries: local mlx-serve qwen (port 8800) → Ollama → heuristic fallback.
6
- * Non-blocking — caller should fire-and-forget, update entry when ready.
5
+ * Tries: on-device Foundation Models → local mlx-serve qwen (port 8800) →
6
+ * Ollama → heuristic fallback. Non-blocking — caller should fire-and-forget,
7
+ * update entry when ready.
8
+ *
9
+ * The provider order is the Node mirror of Swift's `TimelineSummarizer`
10
+ * `.auto` chain (FoundationModels → MLX → Ollama → heuristic). It was
11
+ * MLX-first for a long time, which meant a CLI-only user on macOS 26 with
12
+ * Apple Intelligence available still got heuristic rows unless they also ran
13
+ * an MLX server — the same machine summarized differently depending on which
14
+ * daemon happened to be up. Keep the two chains in the same order.
7
15
  */
8
16
  import { debug, log } from './logger.js';
9
17
  import { SUMMARY_SYSTEM_PROMPT, cleanLLMOutput, mlxChatUrl, resolveMlxModel } from '@agentdeck/shared';
10
18
  import { fetchMlxModels } from './mlx-probe.js';
19
+ import { callFoundationModelsHelper, probeFoundationModelsHelper } from './foundation-models-helper.js';
11
20
  export { extractTopicHint } from '@agentdeck/shared';
12
21
  const MLX_URL = mlxChatUrl();
13
22
  // In-memory cache of the probe's first result, so summarizers don't hit
@@ -32,11 +41,24 @@ async function resolveModelForCall() {
32
41
  const OLLAMA_URL = 'http://localhost:11434/api/chat';
33
42
  const TIMEOUT_MS = 30_000; // 30s — first inference needs model load time
34
43
  const MAX_INPUT_CHARS = 2000;
44
+ let fmAvailable = null;
35
45
  let mlxAvailable = null;
36
46
  let ollamaAvailable = null;
47
+ let fmFailedAt = 0;
37
48
  let mlxFailedAt = 0;
38
49
  let ollamaFailedAt = 0;
39
50
  const RETRY_INTERVAL_MS = 60_000; // retry failed providers after 60s
51
+ /** Reset the cached provider availability (tests only). */
52
+ export function clearSummarizerProviderCacheForTests() {
53
+ fmAvailable = null;
54
+ mlxAvailable = null;
55
+ ollamaAvailable = null;
56
+ fmFailedAt = 0;
57
+ mlxFailedAt = 0;
58
+ ollamaFailedAt = 0;
59
+ probedFirstModel = null;
60
+ probedAt = 0;
61
+ }
40
62
  /**
41
63
  * Summarize a chat response into a concise 1-line Korean summary.
42
64
  * Returns null if summarization fails (caller should use fallback).
@@ -49,7 +71,29 @@ export async function summarizeResponse(text) {
49
71
  : text;
50
72
  let mlxJustFailed = false;
51
73
  let ollamaJustFailed = false;
52
- // Try MLX qwen first (retry after RETRY_INTERVAL_MS)
74
+ // Try on-device Foundation Models first no server for the user to run,
75
+ // and it is the same helper process the APME judge already keeps warm.
76
+ // A miss here is not an error state: off-macOS, on macOS < 26, or with
77
+ // Apple Intelligence disabled the probe simply reports unavailable and the
78
+ // chain moves on. Only MLX+Ollama failing together is worth telling the
79
+ // user about (that pair is what the user opts into by installing them).
80
+ if (fmAvailable !== false || (Date.now() - fmFailedAt > RETRY_INTERVAL_MS)) {
81
+ try {
82
+ const result = await callFoundationModels(input);
83
+ if (result) {
84
+ if (fmAvailable === false)
85
+ debug('summarizer', 'Foundation Models recovered');
86
+ fmAvailable = true;
87
+ return result;
88
+ }
89
+ }
90
+ catch (err) {
91
+ fmAvailable = false;
92
+ fmFailedAt = Date.now();
93
+ debug('summarizer', `Foundation Models not available: ${String(err)}`);
94
+ }
95
+ }
96
+ // Try MLX qwen next (retry after RETRY_INTERVAL_MS)
53
97
  if (mlxAvailable !== false || (Date.now() - mlxFailedAt > RETRY_INTERVAL_MS)) {
54
98
  try {
55
99
  const result = await callMLX(input);
@@ -99,11 +143,29 @@ export async function summarizeResponse(text) {
99
143
  if ((mlxJustFailed && ollamaJustFailed)
100
144
  || (mlxJustFailed && ollamaAvailable === false)
101
145
  || (ollamaJustFailed && mlxAvailable === false)) {
102
- log('[timeline] LLM summary backend offline (MLX:8800 / Ollama:11434).', 'Timeline rows will use heuristic summaries.', 'Install MLX (`mlx_vlm.server`) or Ollama to get LLM-summarized chat_end rows.');
146
+ log(`[timeline] LLM summary backend offline (on-device Foundation Models: ${fmAvailable === false ? 'unavailable' : 'not reached'}, MLX:8800, Ollama:11434).`, 'Timeline rows will use heuristic summaries.', 'Install MLX (`mlx_vlm.server`) or Ollama to get LLM-summarized chat_end rows.');
103
147
  }
104
148
  return null;
105
149
  }
106
150
  // extractTopicHint and cleanLLMOutput moved to @agentdeck/shared/timeline-summarizer
151
+ /** On-device summary via the bundled Swift helper (Apple Intelligence).
152
+ * Throws when the helper is unavailable so the caller falls to MLX; the
153
+ * helper owns its own request timeout, so there is no unbounded await here. */
154
+ async function callFoundationModels(input) {
155
+ // Probe only on the first attempt: the probe is a full health round-trip to
156
+ // the helper, and after that the generate call is its own liveness check.
157
+ // (`fmAvailable` is null exactly once — the caller flips it either way.)
158
+ if (fmAvailable === null) {
159
+ const status = await probeFoundationModelsHelper();
160
+ if (!status.available)
161
+ throw new Error(status.reason ?? 'helper unavailable');
162
+ }
163
+ const text = await callFoundationModelsHelper(input, SUMMARY_SYSTEM_PROMPT);
164
+ const result = cleanLLMOutput(text);
165
+ if (result)
166
+ debug('summarizer', `Foundation Models summary: ${result}`);
167
+ return result;
168
+ }
107
169
  async function callMLX(input) {
108
170
  const controller = new AbortController();
109
171
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
@@ -1 +1 @@
1
- {"version":3,"file":"timeline-summarizer.js","sourceRoot":"","sources":["../src/timeline-summarizer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"timeline-summarizer.js","sourceRoot":"","sources":["../src/timeline-summarizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * TRMNL BYOS (Bring Your Own Server) HTTP handlers, mounted on the daemon hub.
3
+ *
4
+ * Implements the minimal TRMNL device contract so a stock-firmware panel pointed
5
+ * at `http://<daemon-host>:9120` renders the AgentDeck dashboard:
6
+ *
7
+ * GET /api/setup ID:<mac> → { api_key, friendly_id, image_url }
8
+ * GET /api/display ID:<mac> Access-Token:<key> → { image_url, filename, refresh_rate, ... }
9
+ * GET /trmnl/image/<W>x<H>-<hash>.png → 1-bit PNG frame (per resolution)
10
+ * POST /api/log ID:<mac> → 204 (device logs, debug only)
11
+ *
12
+ * The device authenticates only by MAC (ID header); the api_key issued at setup
13
+ * is a soft gate on /api/display. This is LAN-local hardware, so no token is
14
+ * required to fetch the image itself. Any panel auto-enrolls regardless of MAC,
15
+ * and the dashboard is rendered at the device-reported Width/Height so different
16
+ * BYOS panels render correctly. The device's FW-Version/RSSI/Battery/etc. headers
17
+ * are captured as runtime telemetry. See docs: https://docs.trmnl.com/go/diy/byos
18
+ */
19
+ import type { IncomingMessage, ServerResponse } from 'http';
20
+ /** Test helper — clear weak-RSSI transition tracking. */
21
+ export declare function _resetWeakRssiTracking(): void;
22
+ /** GET /api/setup — enroll the device (by MAC) and hand back an api_key. */
23
+ export declare function handleTrmnlSetup(req: IncomingMessage, res: ServerResponse): void;
24
+ /**
25
+ * GET /api/display — return the next image + polling cadence.
26
+ *
27
+ * NOTE: we always reply with HTTP 200; the BYOS `status` field carries the real
28
+ * signal (0 = show image, 202 = call /api/setup). Stock firmware treats a non-200
29
+ * HTTP status as an error and goes back to sleep without rendering, so HTTP 200 is
30
+ * required even in the "needs setup" case.
31
+ */
32
+ export declare function handleTrmnlDisplay(req: IncomingMessage, res: ServerResponse): void;
33
+ /** GET /trmnl/image/<W>x<H>-<hash>.png — serve the 1-bit PNG for that resolution. */
34
+ export declare function handleTrmnlImage(req: IncomingMessage, res: ServerResponse): void;
35
+ /** POST /api/log — the panel's own error report; always logged (rate-limited). */
36
+ export declare function handleTrmnlLog(req: IncomingMessage, res: ServerResponse): void;
37
+ /** True for any path this module owns, so the daemon router can delegate. */
38
+ export declare function isTrmnlImagePath(pathname: string): boolean;
39
+ //# sourceMappingURL=byos-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byos-server.d.ts","sourceRoot":"","sources":["../../src/trmnl/byos-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AA+C5D,yDAAyD;AACzD,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAkGD,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CA8BhF;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CA4ElF;AAED,qFAAqF;AACrF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAoBhF;AA0BD,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CA0B9E;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D"}
@@ -0,0 +1,301 @@
1
+ import { TRMNL_WIDTH, TRMNL_HEIGHT } from '@agentdeck/shared';
2
+ import { ensureDevice, findDeviceByMac, loadTrmnlConfig, normalizeMac, effectiveRefreshRate, effectiveImageTimeout, TRMNL_WEAK_RSSI_DBM, } from './trmnl-settings.js';
3
+ import { getTrmnlFrame, getTrmnlFrameByKey, forceRenderTrmnlFrame, getTrmnlActivity } from './frame-cache.js';
4
+ import { recordTelemetry } from './trmnl-telemetry.js';
5
+ import { debug, logTagged } from '../logger.js';
6
+ const TAG = 'trmnl-byos';
7
+ /** MACs currently flagged weak-signal — tracked so we log a transition, not every poll. */
8
+ const weakRssiMacs = new Set();
9
+ /**
10
+ * Log once when a panel's RSSI crosses into/out of the weak-link threshold. A
11
+ * weak/lossy WiFi link is the dominant real-world cause of the firmware's
12
+ * "WiFi connected / not responding" (WIFI_FAILED) screen, so this gives an
13
+ * operator a correlatable record — always visible (not gated behind --debug),
14
+ * but rate-limited to state transitions instead of spamming every poll.
15
+ */
16
+ function noteRssiHealth(mac, rssi) {
17
+ const key = normalizeMac(mac);
18
+ if (!key)
19
+ return;
20
+ const isWeak = rssi != null && Number.isFinite(rssi) && rssi <= TRMNL_WEAK_RSSI_DBM;
21
+ const wasWeak = weakRssiMacs.has(key);
22
+ if (isWeak && !wasWeak) {
23
+ weakRssiMacs.add(key);
24
+ logTagged(TAG, `weak WiFi signal on ${key}: rssi=${rssi}dBm (<= ${TRMNL_WEAK_RSSI_DBM}dBm threshold) — ` +
25
+ `image_url_timeout widened; panel may show "not responding" if this persists`);
26
+ }
27
+ else if (!isWeak && wasWeak) {
28
+ weakRssiMacs.delete(key);
29
+ logTagged(TAG, `WiFi signal recovered on ${key}: rssi=${rssi ?? 'n/a'}dBm`);
30
+ }
31
+ }
32
+ /** Test helper — clear weak-RSSI transition tracking. */
33
+ export function _resetWeakRssiTracking() {
34
+ weakRssiMacs.clear();
35
+ }
36
+ /**
37
+ * Record a poll and log when the panel comes back after missing its window.
38
+ * A battery e-ink panel polls on the cadence we hand it; a gap of more than
39
+ * 2× the slow cadence (+ grace) means at least one poll round-trip failed at
40
+ * the network layer — exactly the moment the firmware flashes its "TRMNL not
41
+ * responding" screen. Logging the gap on recovery (always visible, one line
42
+ * per incident) gives the operator a correlatable record of every dead window.
43
+ */
44
+ function notePollGap(mac, h, cfg) {
45
+ const now = Date.now();
46
+ const prevLastSeen = recordTelemetry(mac, h, now);
47
+ if (prevLastSeen == null)
48
+ return;
49
+ const gapSec = Math.round((now - prevLastSeen) / 1000);
50
+ const expected = Math.max(cfg.refreshRate, cfg.refreshActive);
51
+ if (gapSec > expected * 2 + 30) {
52
+ logTagged(TAG, `panel ${normalizeMac(mac)} back after ${gapSec}s silence (cadence ${expected}s) — ` +
53
+ `it missed ≥1 poll window; likely showed "not responding" in between ` +
54
+ `(rssi=${h.rssi ?? 'n/a'}dBm battery=${h.batteryVoltage ?? 'n/a'}V)`);
55
+ }
56
+ }
57
+ // Sane device-reported resolution bounds; anything outside falls back to OG size.
58
+ const MIN_DIM = 120;
59
+ const MAX_DIM = 4000;
60
+ function header(req, name) {
61
+ const v = req.headers[name.toLowerCase()];
62
+ return Array.isArray(v) ? (v[0] ?? '') : (v ?? '');
63
+ }
64
+ function toNum(v) {
65
+ if (!v)
66
+ return null;
67
+ const n = Number(v.trim());
68
+ return Number.isFinite(n) ? n : null;
69
+ }
70
+ /** Parse the BYOS telemetry headers a panel sends on /api/setup + /api/display. */
71
+ function parseDeviceHeaders(req) {
72
+ const sane = (n) => (n != null && n >= MIN_DIM && n <= MAX_DIM ? Math.round(n) : null);
73
+ return {
74
+ mac: header(req, 'ID'),
75
+ accessToken: header(req, 'Access-Token'),
76
+ fwVersion: header(req, 'FW-Version'),
77
+ batteryVoltage: toNum(header(req, 'Battery-Voltage')),
78
+ rssi: toNum(header(req, 'RSSI')),
79
+ refreshRate: toNum(header(req, 'Refresh-Rate')),
80
+ width: sane(toNum(header(req, 'Width'))),
81
+ height: sane(toNum(header(req, 'Height'))),
82
+ userAgent: header(req, 'User-Agent'),
83
+ };
84
+ }
85
+ /** Render size for a device: its reported resolution, or the OG default. */
86
+ function renderSize(h) {
87
+ return { width: h.width ?? TRMNL_WIDTH, height: h.height ?? TRMNL_HEIGHT };
88
+ }
89
+ /** Build the image URL, qualified by resolution so the image route serves the
90
+ * right-sized frame. The hash is the cache-buster the firmware re-downloads on. */
91
+ function imageUrl(req, size, hash) {
92
+ return `${imageBase(req)}/trmnl/image/${size.width}x${size.height}-${hash}.png`;
93
+ }
94
+ function imageBase(req) {
95
+ // The device reached us at exactly this host:port — reuse it so the image_url
96
+ // is always resolvable from the device's perspective (no IP guessing).
97
+ return `http://${req.headers.host ?? 'localhost'}`;
98
+ }
99
+ function sendJson(res, status, body) {
100
+ const payload = JSON.stringify(body);
101
+ res.writeHead(status, {
102
+ 'Content-Type': 'application/json',
103
+ 'Cache-Control': 'no-store',
104
+ 'Access-Control-Allow-Origin': '*',
105
+ });
106
+ res.end(payload);
107
+ }
108
+ /** GET /api/setup — enroll the device (by MAC) and hand back an api_key. */
109
+ export function handleTrmnlSetup(req, res) {
110
+ const h = parseDeviceHeaders(req);
111
+ const mac = h.mac;
112
+ if (!mac) {
113
+ sendJson(res, 400, { status: 400, message: 'Missing ID (MAC) header' });
114
+ return;
115
+ }
116
+ const { device, created } = ensureDevice(mac);
117
+ if (!device) {
118
+ // autoRegister is off and this MAC isn't enrolled.
119
+ debug(TAG, `setup rejected for ${normalizeMac(mac)} (autoRegister off, not enrolled)`);
120
+ sendJson(res, 404, { status: 404, message: 'Device not enrolled. Add it to settings.trmnl.devices.' });
121
+ return;
122
+ }
123
+ notePollGap(mac, h, loadTrmnlConfig());
124
+ noteRssiHealth(mac, h.rssi);
125
+ const size = renderSize(h);
126
+ if (created) {
127
+ debug(TAG, `enrolled TRMNL ${device.mac} as ${device.friendlyId} (${size.width}x${size.height})`);
128
+ forceRenderTrmnlFrame(size.width, size.height); // first served image reflects live state
129
+ }
130
+ const frame = getTrmnlFrame(size.width, size.height);
131
+ sendJson(res, 200, {
132
+ status: 200,
133
+ api_key: device.apiKey,
134
+ friendly_id: device.friendlyId,
135
+ image_url: imageUrl(req, size, frame.contentHash),
136
+ filename: frame.contentHash,
137
+ message: 'Welcome to AgentDeck',
138
+ });
139
+ }
140
+ /**
141
+ * GET /api/display — return the next image + polling cadence.
142
+ *
143
+ * NOTE: we always reply with HTTP 200; the BYOS `status` field carries the real
144
+ * signal (0 = show image, 202 = call /api/setup). Stock firmware treats a non-200
145
+ * HTTP status as an error and goes back to sleep without rendering, so HTTP 200 is
146
+ * required even in the "needs setup" case.
147
+ */
148
+ export function handleTrmnlDisplay(req, res) {
149
+ const h = parseDeviceHeaders(req);
150
+ const mac = h.mac;
151
+ const cfg = loadTrmnlConfig();
152
+ const size = renderSize(h);
153
+ if (mac) {
154
+ notePollGap(mac, h, cfg);
155
+ noteRssiHealth(mac, h.rssi);
156
+ }
157
+ let device = mac ? findDeviceByMac(mac) : undefined;
158
+ if (!device) {
159
+ // Real devices often carry an api_key from a prior (cloud) setup and never
160
+ // call /api/setup again — they poll /api/display straight away. If we answer
161
+ // "not registered" (status 202) they just sleep/retry forever and never show
162
+ // a screen. So auto-enroll on first poll (when autoRegister is on) and serve
163
+ // a real dashboard (status 0). Only fall back to 202 when autoRegister is off.
164
+ if (mac && cfg.autoRegister) {
165
+ const r = ensureDevice(mac);
166
+ device = r.device;
167
+ if (r.created) {
168
+ debug(TAG, `auto-enrolled ${normalizeMac(mac)} on display poll (${size.width}x${size.height})`);
169
+ forceRenderTrmnlFrame(size.width, size.height);
170
+ }
171
+ }
172
+ if (!device) {
173
+ // autoRegister off and unenrolled: serve a real, correctly-sized frame (the
174
+ // idle hero / dashboard) rather than a bogus `setup.png` the image route
175
+ // can't match. status 202 still tells the firmware "not fully set up".
176
+ const setupFrame = getTrmnlFrame(size.width, size.height);
177
+ debug(TAG, `display from unenrolled ${normalizeMac(mac)} (autoRegister off) — requesting setup`);
178
+ sendJson(res, 200, {
179
+ status: 202,
180
+ image_url: imageUrl(req, size, setupFrame.contentHash),
181
+ filename: setupFrame.contentHash,
182
+ refresh_rate: cfg.refreshRate,
183
+ image_url_timeout: effectiveImageTimeout(cfg, { rssi: h.rssi }),
184
+ special_function: 'sleep',
185
+ reset_firmware: false,
186
+ update_firmware: false,
187
+ firmware_url: null,
188
+ });
189
+ return;
190
+ }
191
+ }
192
+ // Soft auth only: the MAC (ID header) identifies the device. The api_key it
193
+ // presents may have been issued by a previous/cloud server, so we don't
194
+ // hard-reject on mismatch — this is same-LAN hardware.
195
+ const frame = getTrmnlFrame(size.width, size.height);
196
+ // Adaptive cadence: poll fast while an agent is AWAITING/WORKING, slow when idle.
197
+ const activity = getTrmnlActivity();
198
+ const refreshRate = effectiveRefreshRate(cfg, activity);
199
+ // Widen the image-download window on a weak link so a lossy GET still finishes
200
+ // before the firmware shows "not responding" (WIFI_FAILED).
201
+ const imageTimeout = effectiveImageTimeout(cfg, { rssi: h.rssi });
202
+ debug(TAG, `display ${normalizeMac(mac)} ${size.width}x${size.height} hash=${frame.contentHash} ` +
203
+ `refresh=${refreshRate}s imageTimeout=${imageTimeout}s rssi=${h.rssi ?? 'n/a'} ` +
204
+ `awaiting=${activity.awaiting} working=${activity.working}`);
205
+ sendJson(res, 200, {
206
+ status: 0,
207
+ image_url: imageUrl(req, size, frame.contentHash),
208
+ filename: frame.contentHash,
209
+ // Number (matches the reference BYOS + firmware's uint parse), not a string.
210
+ refresh_rate: refreshRate,
211
+ // Generous, link-aware image-download window so a flaky WiFi link doesn't trip
212
+ // the firmware's "not responding" screen.
213
+ image_url_timeout: imageTimeout,
214
+ special_function: 'sleep',
215
+ reset_firmware: false,
216
+ update_firmware: false,
217
+ firmware_url: null,
218
+ });
219
+ }
220
+ /** GET /trmnl/image/<W>x<H>-<hash>.png — serve the 1-bit PNG for that resolution. */
221
+ export function handleTrmnlImage(req, res) {
222
+ // The URL carries the resolution (and a cache-busting hash) so we serve the
223
+ // right-sized frame. We ignore the hash itself — the device always wants the
224
+ // freshest screen for its resolution. Unknown/legacy URLs fall back to the
225
+ // default-size frame.
226
+ const pathname = (req.url || '').split('?')[0];
227
+ const m = /\/trmnl\/image\/(\d+)x(\d+)-[0-9a-f]+\.png$/.exec(pathname);
228
+ let frame;
229
+ if (m) {
230
+ const key = `${m[1]}x${m[2]}`;
231
+ frame = getTrmnlFrameByKey(key) ?? getTrmnlFrame(Number(m[1]), Number(m[2]));
232
+ }
233
+ if (!frame)
234
+ frame = getTrmnlFrame();
235
+ res.writeHead(200, {
236
+ 'Content-Type': frame.contentType,
237
+ 'Content-Length': frame.buffer.length,
238
+ 'Cache-Control': 'no-store',
239
+ 'Access-Control-Allow-Origin': '*',
240
+ });
241
+ res.end(frame.buffer);
242
+ }
243
+ // Device-log throttle: the firmware only POSTs /api/log when something went
244
+ // wrong on its side (failed poll, image timeout, low battery), so these lines
245
+ // ARE the root-cause record for "not responding" incidents. Always log them,
246
+ // but cap the rate per panel so a wedged device can't flood stderr.
247
+ const DEVICE_LOG_INTERVAL_MS = 10_000;
248
+ const lastDeviceLogAt = new Map();
249
+ /** Compress the firmware's log JSON into one greppable line (best effort). */
250
+ function summarizeDeviceLog(body) {
251
+ try {
252
+ const parsed = JSON.parse(body);
253
+ const arr = parsed?.log?.logs_array ?? parsed?.logs?.logs_array ?? parsed?.logs_array;
254
+ if (Array.isArray(arr) && arr.length > 0) {
255
+ return arr
256
+ .map((e) => `[${e?.log_codeline ?? e?.log_sourcefile ?? '?'}] ${e?.log_message ?? JSON.stringify(e)}`)
257
+ .join(' | ')
258
+ .slice(0, 600);
259
+ }
260
+ }
261
+ catch {
262
+ /* not JSON — fall through to raw */
263
+ }
264
+ return body.replace(/\s+/g, ' ').slice(0, 600);
265
+ }
266
+ /** POST /api/log — the panel's own error report; always logged (rate-limited). */
267
+ export function handleTrmnlLog(req, res) {
268
+ let body = '';
269
+ req.on('data', (c) => {
270
+ body += c;
271
+ if (body.length > 64_000)
272
+ req.destroy();
273
+ });
274
+ req.on('end', () => {
275
+ const mac = normalizeMac(header(req, 'ID'));
276
+ const now = Date.now();
277
+ if (now - (lastDeviceLogAt.get(mac) ?? 0) >= DEVICE_LOG_INTERVAL_MS) {
278
+ lastDeviceLogAt.set(mac, now);
279
+ logTagged(TAG, `device log from ${mac}: ${summarizeDeviceLog(body)}`);
280
+ }
281
+ else {
282
+ debug(TAG, `device log from ${mac} (throttled): ${body.slice(0, 500)}`);
283
+ }
284
+ res.writeHead(204, { 'Access-Control-Allow-Origin': '*' });
285
+ res.end();
286
+ });
287
+ req.on('error', () => {
288
+ try {
289
+ res.writeHead(204);
290
+ res.end();
291
+ }
292
+ catch {
293
+ /* ignore */
294
+ }
295
+ });
296
+ }
297
+ /** True for any path this module owns, so the daemon router can delegate. */
298
+ export function isTrmnlImagePath(pathname) {
299
+ return pathname.startsWith('/trmnl/image/');
300
+ }
301
+ //# sourceMappingURL=byos-server.js.map