bitwrench 2.0.22 → 2.0.23

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 (88) hide show
  1. package/LICENSE.txt +1 -1
  2. package/README.md +4 -3
  3. package/bin/bwmcp.js +3 -0
  4. package/dist/bitwrench-bccl.cjs.js +1 -1
  5. package/dist/bitwrench-bccl.cjs.min.js +1 -1
  6. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  7. package/dist/bitwrench-bccl.esm.js +1 -1
  8. package/dist/bitwrench-bccl.esm.min.js +1 -1
  9. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  10. package/dist/bitwrench-bccl.umd.js +1 -1
  11. package/dist/bitwrench-bccl.umd.min.js +1 -1
  12. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  13. package/dist/bitwrench-code-edit.cjs.js +1 -1
  14. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  15. package/dist/bitwrench-code-edit.es5.js +1 -1
  16. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  17. package/dist/bitwrench-code-edit.esm.js +1 -1
  18. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  19. package/dist/bitwrench-code-edit.umd.js +1 -1
  20. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  21. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  22. package/dist/bitwrench-debug.js +1 -1
  23. package/dist/bitwrench-debug.min.js +1 -1
  24. package/dist/bitwrench-lean.cjs.js +3 -3
  25. package/dist/bitwrench-lean.cjs.min.js +2 -2
  26. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  27. package/dist/bitwrench-lean.es5.js +3 -3
  28. package/dist/bitwrench-lean.es5.min.js +2 -2
  29. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  30. package/dist/bitwrench-lean.esm.js +3 -3
  31. package/dist/bitwrench-lean.esm.min.js +2 -2
  32. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  33. package/dist/bitwrench-lean.umd.js +3 -3
  34. package/dist/bitwrench-lean.umd.min.js +2 -2
  35. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  36. package/dist/bitwrench-util-css.cjs.js +1 -1
  37. package/dist/bitwrench-util-css.cjs.min.js +1 -1
  38. package/dist/bitwrench-util-css.es5.js +1 -1
  39. package/dist/bitwrench-util-css.es5.min.js +1 -1
  40. package/dist/bitwrench-util-css.esm.js +1 -1
  41. package/dist/bitwrench-util-css.esm.min.js +1 -1
  42. package/dist/bitwrench-util-css.umd.js +1 -1
  43. package/dist/bitwrench-util-css.umd.min.js +1 -1
  44. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  45. package/dist/bitwrench.cjs.js +3 -3
  46. package/dist/bitwrench.cjs.min.js +2 -2
  47. package/dist/bitwrench.cjs.min.js.gz +0 -0
  48. package/dist/bitwrench.css +1 -1
  49. package/dist/bitwrench.es5.js +3 -3
  50. package/dist/bitwrench.es5.min.js +2 -2
  51. package/dist/bitwrench.es5.min.js.gz +0 -0
  52. package/dist/bitwrench.esm.js +3 -3
  53. package/dist/bitwrench.esm.min.js +2 -2
  54. package/dist/bitwrench.esm.min.js.gz +0 -0
  55. package/dist/bitwrench.umd.js +3 -3
  56. package/dist/bitwrench.umd.min.js +2 -2
  57. package/dist/bitwrench.umd.min.js.gz +0 -0
  58. package/dist/builds.json +57 -57
  59. package/dist/bwserve.cjs.js +2 -2
  60. package/dist/bwserve.esm.js +2 -2
  61. package/dist/sri.json +45 -45
  62. package/docs/README.md +76 -0
  63. package/docs/app-patterns.md +264 -0
  64. package/docs/bitwrench-mcp.md +426 -0
  65. package/docs/bitwrench_api.md +2232 -0
  66. package/docs/bw-attach.md +399 -0
  67. package/docs/bwserve.md +841 -0
  68. package/docs/cli.md +307 -0
  69. package/docs/component-cheatsheet.md +144 -0
  70. package/docs/component-library.md +1099 -0
  71. package/docs/framework-translation-table.md +33 -0
  72. package/docs/llm-bitwrench-guide.md +672 -0
  73. package/docs/routing.md +562 -0
  74. package/docs/state-management.md +767 -0
  75. package/docs/taco-format.md +373 -0
  76. package/docs/theming.md +309 -0
  77. package/docs/thinking-in-bitwrench.md +1457 -0
  78. package/docs/tutorial-bwserve.md +297 -0
  79. package/docs/tutorial-embedded.md +314 -0
  80. package/docs/tutorial-website.md +255 -0
  81. package/package.json +11 -3
  82. package/readme.html +5 -4
  83. package/src/mcp/knowledge.js +231 -0
  84. package/src/mcp/live.js +226 -0
  85. package/src/mcp/server.js +216 -0
  86. package/src/mcp/tools.js +369 -0
  87. package/src/mcp/transport.js +55 -0
  88. package/src/version.js +3 -3
@@ -0,0 +1,297 @@
1
+ # Tutorial: Building a Server App with bwserve
2
+
3
+ This tutorial builds a Streamlit-style dashboard where all state lives on the server. The browser is a thin display — the server pushes UI updates over SSE.
4
+
5
+ ## What you'll build
6
+
7
+ A real-time analytics dashboard:
8
+ - Live counter with increment/decrement buttons
9
+ - Auto-updating metrics (requests/sec, memory, uptime)
10
+ - Event log with scrolling entries
11
+ - All state on the server — refresh the page and you pick up where you left off
12
+
13
+ ## Prerequisites
14
+
15
+ - Node.js 18+
16
+ - `npm install bitwrench` (local or global)
17
+
18
+ ## Step 1: Minimal server
19
+
20
+ Create `server.js`:
21
+
22
+ ```javascript
23
+ import bwserve from 'bitwrench/bwserve';
24
+
25
+ var app = bwserve.create({ port: 7902, title: 'My Dashboard' });
26
+
27
+ app.page('/', function(client) {
28
+ client.render('#app', { t: 'h1', c: 'Hello from the server!' });
29
+ });
30
+
31
+ app.listen();
32
+ ```
33
+
34
+ Run it:
35
+
36
+ ```bash
37
+ node server.js
38
+ # Open http://localhost:7902
39
+ ```
40
+
41
+ The browser shows "Hello from the server!" — rendered by bitwrench on the client from a TACO object sent over SSE.
42
+
43
+ ## Step 2: Add a counter with buttons
44
+
45
+ Replace the page handler:
46
+
47
+ ```javascript
48
+ app.page('/', function(client) {
49
+ var count = 0;
50
+
51
+ // Render initial UI
52
+ client.render('#app', {
53
+ t: 'div', c: [
54
+ { t: 'h1', c: 'Counter' },
55
+ { t: 'div', a: { id: 'count', style: 'font-size: 3rem; text-align: center' }, c: '0' },
56
+ { t: 'div', a: { style: 'text-align: center; margin-top: 1rem' }, c: [
57
+ { t: 'button', a: { 'data-bw-action': 'decrement', class: 'bw-btn bw-btn-secondary' }, c: '-1' },
58
+ { t: 'button', a: { 'data-bw-action': 'increment', class: 'bw-btn bw-btn-primary', style: 'margin-left: 0.5rem' }, c: '+1' },
59
+ { t: 'button', a: { 'data-bw-action': 'reset', class: 'bw-btn', style: 'margin-left: 0.5rem' }, c: 'Reset' }
60
+ ]}
61
+ ]
62
+ });
63
+
64
+ // Handle button clicks
65
+ client.on('increment', function() {
66
+ count++;
67
+ client.patch('count', String(count));
68
+ });
69
+
70
+ client.on('decrement', function() {
71
+ count--;
72
+ client.patch('count', String(count));
73
+ });
74
+
75
+ client.on('reset', function() {
76
+ count = 0;
77
+ client.patch('count', '0');
78
+ });
79
+ });
80
+ ```
81
+
82
+ Key concepts:
83
+ - `data-bw-action="increment"` on the button tells the client to POST `{action: "increment"}` when clicked
84
+ - `client.on('increment', fn)` registers a server-side handler for that action
85
+ - `client.patch('count', '0')` sends an SSE message that updates the element with `id="count"`
86
+
87
+ ## Step 3: Add live metrics
88
+
89
+ Add a metrics section that updates every 2 seconds:
90
+
91
+ ```javascript
92
+ app.page('/', function(client) {
93
+ var count = 0;
94
+ var startTime = Date.now();
95
+ var requestCount = 0;
96
+
97
+ // ... counter UI from Step 2 ...
98
+
99
+ // Add metrics section below the counter
100
+ client.append('#app', {
101
+ t: 'div', a: { style: 'margin-top: 2rem' }, c: [
102
+ { t: 'h2', c: 'Live Metrics' },
103
+ { t: 'div', a: { style: 'display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem' }, c: [
104
+ { t: 'div', a: { class: 'bw-card' }, c: [
105
+ { t: 'div', a: { style: 'color: #666; font-size: 0.85rem' }, c: 'Uptime' },
106
+ { t: 'div', a: { id: 'uptime', style: 'font-size: 1.5rem; font-weight: 700' }, c: '0s' }
107
+ ]},
108
+ { t: 'div', a: { class: 'bw-card' }, c: [
109
+ { t: 'div', a: { style: 'color: #666; font-size: 0.85rem' }, c: 'Memory' },
110
+ { t: 'div', a: { id: 'memory', style: 'font-size: 1.5rem; font-weight: 700' }, c: '--' }
111
+ ]},
112
+ { t: 'div', a: { class: 'bw-card' }, c: [
113
+ { t: 'div', a: { style: 'color: #666; font-size: 0.85rem' }, c: 'Requests' },
114
+ { t: 'div', a: { id: 'requests', style: 'font-size: 1.5rem; font-weight: 700' }, c: '0' }
115
+ ]}
116
+ ]}
117
+ ]
118
+ });
119
+
120
+ // Push updates every 2 seconds
121
+ var interval = setInterval(function() {
122
+ var uptime = Math.floor((Date.now() - startTime) / 1000);
123
+ var mem = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
124
+
125
+ client.batch(
126
+ { type: 'patch', target: 'uptime', content: uptime + 's' },
127
+ { type: 'patch', target: 'memory', content: mem + ' MB' },
128
+ { type: 'patch', target: 'requests', content: String(requestCount) }
129
+ );
130
+ }, 2000);
131
+
132
+ // Track actions as requests
133
+ client.on('increment', function() { count++; requestCount++; client.patch('count', String(count)); });
134
+ client.on('decrement', function() { count--; requestCount++; client.patch('count', String(count)); });
135
+ client.on('reset', function() { count = 0; requestCount++; client.patch('count', '0'); });
136
+
137
+ // Clean up when client disconnects
138
+ client.on('disconnect', function() {
139
+ clearInterval(interval);
140
+ });
141
+ });
142
+ ```
143
+
144
+ `client.batch()` sends all three patches in a single SSE frame — the browser applies them atomically.
145
+
146
+ ## Step 4: Add an event log
147
+
148
+ Append a scrolling log that records every action:
149
+
150
+ ```javascript
151
+ // Add log section
152
+ client.append('#app', {
153
+ t: 'div', a: { style: 'margin-top: 2rem' }, c: [
154
+ { t: 'h2', c: 'Event Log' },
155
+ { t: 'div', a: { id: 'log', style: 'max-height: 200px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 6px; padding: 0.5rem; font-family: monospace; font-size: 0.85rem' }, c: '' }
156
+ ]
157
+ });
158
+
159
+ function logEvent(msg) {
160
+ var time = new Date().toLocaleTimeString();
161
+ client.append('#log', {
162
+ t: 'div', a: { style: 'padding: 2px 0; border-bottom: 1px solid #f3f4f6' },
163
+ c: time + ' — ' + msg
164
+ });
165
+ // Auto-scroll to bottom
166
+ client.call('scrollTo', { target: '#log', behavior: 'smooth' });
167
+ }
168
+
169
+ // In each handler:
170
+ client.on('increment', function() {
171
+ count++; requestCount++;
172
+ client.patch('count', String(count));
173
+ logEvent('increment → ' + count);
174
+ });
175
+ ```
176
+
177
+ `client.call('scrollTo', ...)` invokes a built-in client-side function. Other built-ins: `focus`, `download`, `clipboard`, `redirect`, `log`.
178
+
179
+ ## Step 5: Add a theme
180
+
181
+ ```javascript
182
+ var app = bwserve.create({
183
+ port: 7902,
184
+ title: 'Analytics Dashboard',
185
+ theme: 'ocean' // Built-in preset: ocean, sunset, forest, slate, etc.
186
+ });
187
+ ```
188
+
189
+ The auto-generated client page applies the theme automatically — all bitwrench components (buttons, cards, alerts) use the theme colors.
190
+
191
+ ## Step 6: Multi-page app
192
+
193
+ Add a second page:
194
+
195
+ ```javascript
196
+ app.page('/settings', function(client) {
197
+ client.render('#app', {
198
+ t: 'div', c: [
199
+ { t: 'h1', c: 'Settings' },
200
+ { t: 'a', a: { href: '/' }, c: 'Back to Dashboard' },
201
+ { t: 'p', c: 'Configure your dashboard preferences here.' }
202
+ ]
203
+ });
204
+ });
205
+ ```
206
+
207
+ Each page gets its own handler, its own state, its own client connection. Navigate between them with regular links.
208
+
209
+ ## How it works
210
+
211
+ ```
212
+ Browser Server (Node.js)
213
+ | |
214
+ | GET / |
215
+ | <── HTML shell (bitwrench + SSE) |
216
+ | |
217
+ | GET /events/:id (SSE) |
218
+ | <── {type:'replace', target:'#app', |
219
+ | node: {t:'div', c:[...]}} |
220
+ | |
221
+ | User clicks [+1] button |
222
+ | POST /action/:id |
223
+ | {action:'increment', data:{}} ──> |
224
+ | | count++
225
+ | <── {type:'patch', |
226
+ | target:'count', |
227
+ | content:'1'} |
228
+ | |
229
+ | (every 2s) |
230
+ | <── {type:'batch', ops:[ |
231
+ | {type:'patch',target:'uptime'} |
232
+ | {type:'patch',target:'memory'} |
233
+ | ]} |
234
+ ```
235
+
236
+ ## Protocol messages
237
+
238
+ | Type | Method | What it does |
239
+ |------|--------|-------------|
240
+ | `replace` | `client.render(target, taco)` | Replace element content with TACO |
241
+ | `patch` | `client.patch(id, text, attrs)` | Update text/attributes of element |
242
+ | `append` | `client.append(target, taco)` | Add child element |
243
+ | `remove` | `client.remove(target)` | Remove element from DOM |
244
+ | `batch` | `client.batch(op1, op2, ...)` | Multiple ops in one frame |
245
+ | `message` | `client.message(level, text)` | Show notification |
246
+ | `call` | `client.call(name, ...args)` | Invoke registered or built-in function |
247
+ | `exec` | `client.exec(code)` | Run arbitrary JS (requires `allowExec`) |
248
+ | `register` | `client.register(name, body)` | Send named function to client |
249
+
250
+ ### Screenshots
251
+
252
+ The server can capture what the browser is displaying:
253
+
254
+ ```javascript
255
+ // Enable in server options
256
+ var app = create({ port: 7902, allowScreenshot: true });
257
+
258
+ app.page('/', function(client) {
259
+ client.render('#app', myDashboard);
260
+
261
+ client.on('capture', async function() {
262
+ // Capture the full page as PNG
263
+ var img = await client.screenshot();
264
+ require('fs').writeFileSync('screenshot.png', img.data);
265
+
266
+ // Capture a specific element, resized to max 800px wide
267
+ var thumb = await client.screenshot('#chart', {
268
+ maxWidth: 800,
269
+ format: 'jpeg',
270
+ quality: 0.8
271
+ });
272
+ });
273
+ });
274
+ ```
275
+
276
+ Uses html2canvas (vendored, lazy-loaded). See [screenshot example](../examples/client-server/screenshot-server.js).
277
+
278
+ ## Deployment
279
+
280
+ bwserve apps are regular Node.js servers. Deploy like any other:
281
+
282
+ ```bash
283
+ # systemd, PM2, Docker, etc.
284
+ node server.js
285
+
286
+ # Or with environment variables
287
+ PORT=3000 node server.js
288
+ ```
289
+
290
+ For production, put a reverse proxy (nginx, Caddy) in front for TLS and static asset caching.
291
+
292
+ ## Next steps
293
+
294
+ - [bwserve Reference](bwserve.md) — full API documentation
295
+ - [Component Library](component-library.md) — all 50+ `make*()` components work in bwserve
296
+ - [Tutorial: Embedded](tutorial-embedded.md) — same protocol on ESP32
297
+ - [examples/client-server/](../examples/client-server/) — runnable example
@@ -0,0 +1,314 @@
1
+ # Tutorial: ESP32 IoT Dashboard with Bitwrench
2
+
3
+ This tutorial builds a real-time sensor dashboard served by an ESP32 microcontroller. The device sends data — bitwrench renders the UI in the browser.
4
+
5
+ ## What you'll build
6
+
7
+ - ESP32 serves a bitwrench-powered web page over WiFi
8
+ - Temperature and humidity readings pushed via SSE every 2 seconds
9
+ - LED control button sends commands back to the device
10
+ - Total HTML payload: ~5KB (excluding bitwrench library)
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ ESP32 Browser
16
+ | |
17
+ | GET / |
18
+ | <── index.html (from SPIFFS) |
19
+ | GET /bitwrench.umd.min.js |
20
+ | <── JS (from SPIFFS) |
21
+ | |
22
+ | GET /events (SSE) |
23
+ | <── sensor JSON every 2s |
24
+ | |
25
+ | POST /api/command |
26
+ | {cmd: 'led', val: 'on'} ──> |
27
+ | <── {ok: true} |
28
+ ```
29
+
30
+ The ESP32 never generates HTML. It only sends JSON data. The browser renders everything.
31
+
32
+ ## Prerequisites
33
+
34
+ - ESP32 DevKit (any variant)
35
+ - DHT22 temperature/humidity sensor (GPIO 4)
36
+ - Arduino IDE or PlatformIO
37
+ - Arduino libraries: `ESPAsyncWebServer`, `AsyncTCP`, `DHT sensor library`, `ArduinoJson`
38
+
39
+ ## Step 1: Set up the project
40
+
41
+ ### With bitwrench C headers (recommended)
42
+
43
+ Copy the embedded C headers into your project:
44
+
45
+ ```
46
+ my_project/
47
+ my_project.ino
48
+ bitwrench.h ← from embedded_c/
49
+ bwserve.h ← from embedded_c/
50
+ data/
51
+ index.html ← the dashboard page
52
+ bitwrench.umd.min.js.gz ← gzip -k dist/bitwrench.umd.min.js
53
+ ```
54
+
55
+ ### With PlatformIO
56
+
57
+ ```
58
+ lib/
59
+ bitwrench/
60
+ bitwrench.h
61
+ bwserve.h
62
+ data/
63
+ index.html
64
+ bitwrench.umd.min.js.gz
65
+ ```
66
+
67
+ Store the gzipped file on flash (~40KB instead of ~150KB). ESPAsyncWebServer
68
+ serves `.gz` files transparently -- the browser decompresses automatically.
69
+
70
+ ## Step 2: Write the Arduino sketch
71
+
72
+ ```cpp
73
+ #include <WiFi.h>
74
+ #include <ESPAsyncWebServer.h>
75
+ #include <SPIFFS.h>
76
+ #include <DHT.h>
77
+ #include "bitwrench.h"
78
+ #include "bwserve.h"
79
+
80
+ const char* SSID = "YOUR_WIFI";
81
+ const char* PASS = "YOUR_PASSWORD";
82
+
83
+ #define DHT_PIN 4
84
+ #define LED_PIN 2
85
+
86
+ AsyncWebServer server(80);
87
+ AsyncEventSource events("/events");
88
+ DHT dht(DHT_PIN, DHT22);
89
+
90
+ void setup() {
91
+ Serial.begin(115200);
92
+ pinMode(LED_PIN, OUTPUT);
93
+ dht.begin();
94
+
95
+ // Mount flash filesystem
96
+ SPIFFS.begin(true);
97
+
98
+ // Connect WiFi
99
+ WiFi.begin(SSID, PASS);
100
+ while (WiFi.status() != WL_CONNECTED) delay(500);
101
+ Serial.print("IP: ");
102
+ Serial.println(WiFi.localIP());
103
+
104
+ // Serve static files from SPIFFS
105
+ server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
106
+
107
+ // SSE endpoint
108
+ events.onConnect([](AsyncEventSourceClient* client) {
109
+ Serial.println("Browser connected");
110
+ });
111
+ server.addHandler(&events);
112
+
113
+ // Command endpoint
114
+ server.on("/api/command", HTTP_POST,
115
+ [](AsyncWebServerRequest* req) {},
116
+ NULL,
117
+ [](AsyncWebServerRequest* req, uint8_t* data, size_t len, size_t, size_t) {
118
+ if (strstr((char*)data, "\"led_on\"")) {
119
+ digitalWrite(LED_PIN, HIGH);
120
+ } else if (strstr((char*)data, "\"led_off\"")) {
121
+ digitalWrite(LED_PIN, LOW);
122
+ }
123
+ req->send(200, "application/json", "{\"ok\":true}");
124
+ }
125
+ );
126
+
127
+ server.begin();
128
+ }
129
+
130
+ void loop() {
131
+ static unsigned long lastSend = 0;
132
+ if (millis() - lastSend < 2000) return;
133
+ lastSend = millis();
134
+
135
+ // Read sensors
136
+ float temp = dht.readTemperature();
137
+ float hum = dht.readHumidity();
138
+
139
+ // Send batch update using bwserve macros
140
+ bw_batch_t batch;
141
+ bw_batch_begin(&batch);
142
+
143
+ char m1[256], m2[256], m3[256];
144
+ char ts[16], hs[16], us[16];
145
+
146
+ snprintf(ts, sizeof(ts), "%.1f C", isnan(temp) ? 0.0 : temp);
147
+ BW_PATCH(m1, "val-temp", ts);
148
+ bw_batch_add(&batch, m1);
149
+
150
+ snprintf(hs, sizeof(hs), "%.1f%%", isnan(hum) ? 0.0 : hum);
151
+ BW_PATCH(m2, "val-humidity", hs);
152
+ bw_batch_add(&batch, m2);
153
+
154
+ snprintf(us, sizeof(us), "%lus", millis() / 1000);
155
+ BW_PATCH(m3, "val-uptime", us);
156
+ bw_batch_add(&batch, m3);
157
+
158
+ char out[1024];
159
+ bw_batch_end(out, sizeof(out), &batch);
160
+ events.send(out, NULL, millis());
161
+ }
162
+ ```
163
+
164
+ The `BW_PATCH` and `bw_batch_*` macros from `bwserve.h` compose the protocol messages. They use the r-prefix relaxed JSON format, so no double-quote escaping needed.
165
+
166
+ ## Step 3: Write the dashboard HTML
167
+
168
+ Create `data/index.html`:
169
+
170
+ ```html
171
+ <!DOCTYPE html>
172
+ <html lang="en">
173
+ <head>
174
+ <meta charset="UTF-8">
175
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
176
+ <title>ESP32 Dashboard</title>
177
+ <script src="/bitwrench.umd.min.js"></script>
178
+ <style>
179
+ body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 1rem; }
180
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 0.75rem; max-width: 600px; margin: 1rem auto; }
181
+ .card { background: #1e293b; border-radius: 8px; padding: 1rem; text-align: center; }
182
+ .card h3 { margin: 0 0 0.5rem; font-size: 0.8rem; color: #94a3b8; text-transform: uppercase; }
183
+ .card .val { font-size: 1.5rem; font-weight: 700; color: #38bdf8; }
184
+ h1 { text-align: center; color: #10b981; }
185
+ button { background: #334155; color: #e2e8f0; border: 1px solid #475569; border-radius: 6px; padding: 0.5rem 1.5rem; cursor: pointer; margin: 0.25rem; }
186
+ </style>
187
+ </head>
188
+ <body>
189
+ <h1>ESP32 Dashboard</h1>
190
+ <div class="grid">
191
+ <div class="card"><h3>Temperature</h3><div class="val" id="val-temp">--</div></div>
192
+ <div class="card"><h3>Humidity</h3><div class="val" id="val-humidity">--</div></div>
193
+ <div class="card"><h3>Uptime</h3><div class="val" id="val-uptime">--</div></div>
194
+ </div>
195
+ <div style="text-align: center; margin-top: 1rem;">
196
+ <button onclick="sendCmd('led_on')">LED On</button>
197
+ <button onclick="sendCmd('led_off')">LED Off</button>
198
+ </div>
199
+
200
+ <script>
201
+ // Connect to SSE stream
202
+ var es = new EventSource('/events');
203
+ es.onmessage = function(e) {
204
+ var raw = e.data;
205
+ // Handle r-prefix relaxed JSON from ESP32
206
+ if (raw.charAt(0) === 'r') raw = raw.slice(1).replace(/'/g, '"');
207
+ try { var msg = JSON.parse(raw); } catch(x) { return; }
208
+
209
+ if (msg.type === 'batch') {
210
+ msg.ops.forEach(applyOp);
211
+ } else {
212
+ applyOp(msg);
213
+ }
214
+ };
215
+
216
+ function applyOp(op) {
217
+ if (op.type === 'patch') {
218
+ var el = document.getElementById(op.target);
219
+ if (el) el.textContent = op.content;
220
+ }
221
+ }
222
+
223
+ function sendCmd(cmd) {
224
+ fetch('/api/command', {
225
+ method: 'POST',
226
+ headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({ cmd: cmd })
228
+ });
229
+ }
230
+ </script>
231
+ </body>
232
+ </html>
233
+ ```
234
+
235
+ This page is ~2KB. With bitwrench.umd.min.js.gz (~40KB), the total SPIFFS usage is ~42KB out of 1.5MB available. ESPAsyncWebServer serves `.gz` files transparently -- the browser decompresses automatically.
236
+
237
+ ## Step 4: Upload and test
238
+
239
+ 1. Edit `SSID` and `PASS` in the sketch
240
+ 2. Upload the `data/` folder to SPIFFS (Arduino IDE: Tools > ESP32 Sketch Data Upload)
241
+ 3. Upload the sketch
242
+ 4. Open Serial Monitor — note the IP address
243
+ 5. Navigate to `http://<ip-address>/` in your browser
244
+
245
+ ## Try without hardware first
246
+
247
+ The `examples/embedded/cmake-demo/` directory contains a POSIX version that compiles on Linux/macOS:
248
+
249
+ ```bash
250
+ cd examples/embedded/cmake-demo
251
+ mkdir build && cd build
252
+ cmake .. && make
253
+ ./bwserve_demo
254
+ # Open http://localhost:8080
255
+ ```
256
+
257
+ Same protocol, same macros, simulated sensors. Copy the pattern to your real sketch.
258
+
259
+ ## Memory budget
260
+
261
+ | Item | Size |
262
+ |------|------|
263
+ | Dashboard HTML | ~2 KB |
264
+ | bitwrench.umd.min.js.gz | ~40 KB |
265
+ | **Total SPIFFS** | **~42 KB** |
266
+ | ESP32 SPIFFS partition | 1.5 MB |
267
+ | Free heap (runtime) | ~240 KB |
268
+ | SSE frame per update | ~200 bytes |
269
+
270
+ Store gzipped: `gzip -k dist/bitwrench.umd.min.js`. ESPAsyncWebServer
271
+ serves `.gz` files transparently for the matching uncompressed filename.
272
+
273
+ ## The r-prefix relaxed JSON
274
+
275
+ The C macros produce strings like:
276
+ ```
277
+ r{'type':'patch','target':'val-temp','content':'23.5 C'}
278
+ ```
279
+
280
+ The `r` prefix tells the browser parser to convert single quotes to double quotes before `JSON.parse()`. This avoids escaping double quotes in C string literals — a major ergonomic win.
281
+
282
+ **Escaping rule**: Since single quotes delimit strings, apostrophes in values need escaping with `\'`:
283
+
284
+ ```c
285
+ // Static text with apostrophe — escape in the literal:
286
+ BW_PATCH(msg, "room", "Barry\\'s Room");
287
+
288
+ // Dynamic user text — use BW_PATCH_SAFE (auto-escapes):
289
+ char user_text[] = "it's 23.5 C";
290
+ BW_PATCH_SAFE(msg, sizeof(msg), "status", user_text);
291
+ ```
292
+
293
+ This is still far better than standard JSON in C, where every quote needs `\"`.
294
+
295
+ Direction: r-prefix is outbound only (ESP32 to browser). The browser always sends strict JSON back via `fetch()`.
296
+
297
+ ## Other languages
298
+
299
+ The same protocol works from any language:
300
+
301
+ | Platform | Language | Library |
302
+ |----------|----------|---------|
303
+ | ESP32 (Arduino) | C/C++ | `embedded_c/bwserve.h` |
304
+ | ESP32 (esp-idf) | Rust | `embedded_rust/` |
305
+ | ESP32/RPi (MicroPython) | Python | `embedded_python/bwserve.py` |
306
+ | Adafruit boards | CircuitPython | `embedded_python/bwserve.py` |
307
+ | Node.js | JavaScript | `import bwserve from 'bitwrench/bwserve'` |
308
+
309
+ ## Next steps
310
+
311
+ - [bwserve Reference](bwserve.md) — full protocol documentation
312
+ - [embedded_c/ README](../embedded_c/README.md) — C/C++ macro reference
313
+ - [examples/embedded/](../examples/embedded/) — complete example with mock data
314
+ - [Tutorial: bwserve](tutorial-bwserve.md) — same pattern in Node.js