zero-http 0.2.0 → 0.2.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.
- package/README.md +314 -115
- package/documentation/full-server.js +82 -1
- package/documentation/public/data/api.json +154 -33
- package/documentation/public/data/examples.json +35 -11
- package/documentation/public/data/options.json +14 -8
- package/documentation/public/index.html +109 -17
- package/documentation/public/scripts/data-sections.js +4 -4
- package/documentation/public/scripts/helpers.js +4 -4
- package/documentation/public/scripts/playground.js +201 -0
- package/documentation/public/scripts/ui.js +6 -6
- package/documentation/public/scripts/uploads.js +9 -9
- package/documentation/public/styles.css +12 -12
- package/documentation/public/vendor/icons/compress.svg +27 -0
- package/documentation/public/vendor/icons/https.svg +23 -0
- package/documentation/public/vendor/icons/router.svg +29 -0
- package/documentation/public/vendor/icons/sse.svg +25 -0
- package/documentation/public/vendor/icons/websocket.svg +21 -0
- package/index.js +21 -4
- package/lib/app.js +145 -15
- package/lib/body/json.js +3 -0
- package/lib/body/multipart.js +2 -0
- package/lib/body/raw.js +3 -0
- package/lib/body/text.js +3 -0
- package/lib/body/urlencoded.js +3 -0
- package/lib/{fetch.js → fetch/index.js} +30 -1
- package/lib/http/index.js +9 -0
- package/lib/{request.js → http/request.js} +7 -1
- package/lib/{response.js → http/response.js} +70 -1
- package/lib/middleware/compress.js +194 -0
- package/lib/middleware/index.js +12 -0
- package/lib/router/index.js +278 -0
- package/lib/sse/index.js +8 -0
- package/lib/sse/stream.js +322 -0
- package/lib/ws/connection.js +440 -0
- package/lib/ws/handshake.js +122 -0
- package/lib/ws/index.js +12 -0
- package/package.json +1 -1
- package/lib/router.js +0 -87
- /package/lib/{cors.js → middleware/cors.js} +0 -0
- /package/lib/{logger.js → middleware/logger.js} +0 -0
- /package/lib/{rateLimit.js → middleware/rateLimit.js} +0 -0
- /package/lib/{static.js → middleware/static.js} +0 -0
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
7
7
|
<title>zero-http — demo & reference</title>
|
|
8
8
|
<meta name="description"
|
|
9
|
-
content="zero-http is a zero-dependency, Express-like HTTP server for Node.js with built-in parsers, static serving, and upload handling." />
|
|
9
|
+
content="zero-http is a zero-dependency, Express-like HTTP/HTTPS server for Node.js with built-in WebSocket, Server-Sent Events, body parsers, response compression, static serving, and upload handling." />
|
|
10
10
|
<meta name="keywords"
|
|
11
|
-
content="nodejs,http,server,express,expressjs,middleware,body-parser,multipart,uploads,static files,fetch,http client" />
|
|
11
|
+
content="nodejs,http,https,server,express,expressjs,middleware,body-parser,multipart,uploads,static files,fetch,http client,websocket,sse,server-sent events,compression,router" />
|
|
12
12
|
<meta name="author" content="Anthony Wiedman" />
|
|
13
13
|
|
|
14
14
|
<link rel="stylesheet" href="/styles.css" />
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
<link rel="stylesheet" href="/vendor/prism-toolbar.css" />
|
|
17
17
|
<link rel="stylesheet" href="/prism-overrides.css" />
|
|
18
18
|
|
|
19
|
-
<script src="/vendor/prism.min.js"
|
|
20
|
-
<script src="/vendor/prism-json.min.js"
|
|
21
|
-
<script src="/vendor/prism-javascript.min.js"
|
|
22
|
-
<script src="/vendor/prism-copy-to-clipboard.min.js"
|
|
23
|
-
<script src="/vendor/prism-toolbar.min.js"
|
|
19
|
+
<script src="/vendor/prism.min.js" defer></script>
|
|
20
|
+
<script src="/vendor/prism-json.min.js" defer></script>
|
|
21
|
+
<script src="/vendor/prism-javascript.min.js" defer></script>
|
|
22
|
+
<script src="/vendor/prism-copy-to-clipboard.min.js" defer></script>
|
|
23
|
+
<script src="/vendor/prism-toolbar.min.js" defer></script>
|
|
24
24
|
|
|
25
25
|
<script src="/scripts/helpers.js" defer></script>
|
|
26
26
|
<script src="/scripts/uploads.js" defer></script>
|
|
@@ -89,6 +89,8 @@
|
|
|
89
89
|
<li class="toc-sub-item"><a href="#upload-testing">Upload multipart data</a></li>
|
|
90
90
|
<li class="toc-sub-item"><a href="#parser-tests">Parser tests</a></li>
|
|
91
91
|
<li class="toc-sub-item"><a href="#proxy-test">Proxy test</a></li>
|
|
92
|
+
<li class="toc-sub-item"><a href="#ws-chat">WebSocket chat</a></li>
|
|
93
|
+
<li class="toc-sub-item"><a href="#sse-viewer">SSE viewer</a></li>
|
|
92
94
|
</ul>
|
|
93
95
|
</li>
|
|
94
96
|
</ul>
|
|
@@ -112,40 +114,80 @@
|
|
|
112
114
|
<img class="icon-svg" src="/vendor/icons/zero.svg" alt="" aria-hidden="true"
|
|
113
115
|
width="48" height="48" />
|
|
114
116
|
</div>
|
|
115
|
-
<h5>Zero-dependency</h5>
|
|
116
|
-
<p>Small, Express-like API without external deps.</p>
|
|
117
|
+
<div class="feature-text"><h5>Zero-dependency</h5>
|
|
118
|
+
<p>Small, Express-like API without external deps.</p></div>
|
|
117
119
|
</div>
|
|
118
120
|
<div class="feature-card">
|
|
119
121
|
<div class="feature-icon">
|
|
120
122
|
<img class="icon-svg" src="/vendor/icons/plug.svg" alt="" aria-hidden="true"
|
|
121
123
|
width="48" height="48" />
|
|
122
124
|
</div>
|
|
123
|
-
<h5>Pluggable parsers</h5>
|
|
124
|
-
<p>json(), urlencoded(), text(), raw() built-in.</p>
|
|
125
|
+
<div class="feature-text"><h5>Pluggable parsers</h5>
|
|
126
|
+
<p>json(), urlencoded(), text(), raw() built-in.</p></div>
|
|
125
127
|
</div>
|
|
126
128
|
<div class="feature-card">
|
|
127
129
|
<div class="feature-icon">
|
|
128
130
|
<img class="icon-svg" src="/vendor/icons/stream.svg" alt="" aria-hidden="true"
|
|
129
131
|
width="48" height="48" />
|
|
130
132
|
</div>
|
|
131
|
-
<h5>Streaming uploads</h5>
|
|
132
|
-
<p>Multipart streaming to disk for large files.</p>
|
|
133
|
+
<div class="feature-text"><h5>Streaming uploads</h5>
|
|
134
|
+
<p>Multipart streaming to disk for large files.</p></div>
|
|
133
135
|
</div>
|
|
134
136
|
<div class="feature-card">
|
|
135
137
|
<div class="feature-icon">
|
|
136
138
|
<img class="icon-svg" src="/vendor/icons/static.svg" alt="" aria-hidden="true"
|
|
137
139
|
width="48" height="48" />
|
|
138
140
|
</div>
|
|
139
|
-
<h5>Static serving</h5>
|
|
140
|
-
<p>
|
|
141
|
+
<div class="feature-text"><h5>Static serving</h5>
|
|
142
|
+
<p>Content-Type handling and cache options.</p></div>
|
|
141
143
|
</div>
|
|
142
144
|
<div class="feature-card">
|
|
143
145
|
<div class="feature-icon">
|
|
144
146
|
<img class="icon-svg" src="/vendor/icons/fetch.svg" alt="" aria-hidden="true"
|
|
145
147
|
width="48" height="48" />
|
|
146
148
|
</div>
|
|
147
|
-
<h5>Built-in fetch</h5>
|
|
148
|
-
<p>
|
|
149
|
+
<div class="feature-text"><h5>Built-in fetch</h5>
|
|
150
|
+
<p>Server-side fetch with TLS passthrough.</p></div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="feature-card">
|
|
153
|
+
<div class="feature-icon">
|
|
154
|
+
<img class="icon-svg" src="/vendor/icons/websocket.svg" alt="" aria-hidden="true"
|
|
155
|
+
width="48" height="48" />
|
|
156
|
+
</div>
|
|
157
|
+
<div class="feature-text"><h5>WebSocket</h5>
|
|
158
|
+
<p>RFC 6455 with ping/pong & sub-protocols.</p></div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="feature-card">
|
|
161
|
+
<div class="feature-icon">
|
|
162
|
+
<img class="icon-svg" src="/vendor/icons/sse.svg" alt="" aria-hidden="true"
|
|
163
|
+
width="48" height="48" />
|
|
164
|
+
</div>
|
|
165
|
+
<div class="feature-text"><h5>Server-Sent Events</h5>
|
|
166
|
+
<p>Auto-IDs, named events, keep-alive.</p></div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="feature-card">
|
|
169
|
+
<div class="feature-icon">
|
|
170
|
+
<img class="icon-svg" src="/vendor/icons/compress.svg" alt="" aria-hidden="true"
|
|
171
|
+
width="48" height="48" />
|
|
172
|
+
</div>
|
|
173
|
+
<div class="feature-text"><h5>Compression</h5>
|
|
174
|
+
<p>Gzip/deflate with threshold & filter.</p></div>
|
|
175
|
+
</div>
|
|
176
|
+
<div class="feature-card">
|
|
177
|
+
<div class="feature-icon">
|
|
178
|
+
<img class="icon-svg" src="/vendor/icons/https.svg" alt="" aria-hidden="true"
|
|
179
|
+
width="48" height="48" />
|
|
180
|
+
</div>
|
|
181
|
+
<div class="feature-text"><h5>HTTPS & TLS</h5>
|
|
182
|
+
<p>Native HTTPS, req.secure, secure routing.</p></div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="feature-card">
|
|
185
|
+
<div class="feature-icon">
|
|
186
|
+
<img class="icon-svg" src="/vendor/icons/router.svg" alt="" aria-hidden="true"
|
|
187
|
+
width="48" height="48" />
|
|
188
|
+
</div>
|
|
189
|
+
<div class="feature-text"><h5>Modular Router</h5>
|
|
190
|
+
<p>Sub-apps, chaining, introspection.</p></div>
|
|
149
191
|
</div>
|
|
150
192
|
</div>
|
|
151
193
|
</div>
|
|
@@ -401,6 +443,56 @@ node test/test.js
|
|
|
401
443
|
</div>
|
|
402
444
|
</div>
|
|
403
445
|
|
|
446
|
+
<div class="card play-card constrained">
|
|
447
|
+
<div class="card-head">
|
|
448
|
+
<h3 id="ws-chat">WebSocket Chat</h3>
|
|
449
|
+
<div class="small muted">Connect to the built-in WebSocket server and send messages in real time.</div>
|
|
450
|
+
</div>
|
|
451
|
+
<div class="card-body">
|
|
452
|
+
<div class="playgrid" style="grid-template-columns:1fr">
|
|
453
|
+
<div class="play" id="wsChat">
|
|
454
|
+
<div style="display:flex;gap:8px;align-items:end;margin-bottom:8px">
|
|
455
|
+
<label style="flex:1">Name
|
|
456
|
+
<input id="wsName" class="textInput" placeholder="anon" value="" />
|
|
457
|
+
</label>
|
|
458
|
+
<button id="wsConnectBtn" class="btn primary" type="button">Connect</button>
|
|
459
|
+
<button id="wsDisconnectBtn" class="btn warn" type="button" disabled>Disconnect</button>
|
|
460
|
+
</div>
|
|
461
|
+
<div id="wsMessages" class="result mono" style="min-height:120px;max-height:260px;overflow-y:auto;margin-bottom:8px;white-space:pre-wrap"></div>
|
|
462
|
+
<div style="display:flex;gap:8px">
|
|
463
|
+
<input id="wsMsgInput" class="textInput" placeholder="Type a message…" disabled style="flex:1" />
|
|
464
|
+
<button id="wsSendBtn" class="btn" type="button" disabled>Send</button>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<div class="card play-card constrained">
|
|
472
|
+
<div class="card-head">
|
|
473
|
+
<h3 id="sse-viewer">SSE Event Viewer</h3>
|
|
474
|
+
<div class="small muted">Subscribe to the Server-Sent Events endpoint and view incoming events live. Use the broadcast form to push a custom event to all subscribers.</div>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="card-body">
|
|
477
|
+
<div class="playgrid" style="grid-template-columns:1fr">
|
|
478
|
+
<div class="play" id="sseViewer">
|
|
479
|
+
<div style="display:flex;gap:8px;align-items:end;margin-bottom:8px">
|
|
480
|
+
<button id="sseConnectBtn" class="btn primary" type="button">Connect</button>
|
|
481
|
+
<button id="sseDisconnectBtn" class="btn warn" type="button" disabled>Disconnect</button>
|
|
482
|
+
<span id="sseStatus" class="small muted" style="margin-left:8px">Disconnected</span>
|
|
483
|
+
</div>
|
|
484
|
+
<div id="sseMessages" class="result mono" style="min-height:120px;max-height:260px;overflow-y:auto;margin-bottom:8px;white-space:pre-wrap"></div>
|
|
485
|
+
<div style="display:flex;gap:8px;align-items:end">
|
|
486
|
+
<label style="flex:1">Broadcast message (JSON)
|
|
487
|
+
<textarea id="sseBroadcastInput" class="textInput" rows="2">{"hello":"world"}</textarea>
|
|
488
|
+
</label>
|
|
489
|
+
<button id="sseBroadcastBtn" class="btn" type="button">Broadcast</button>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
404
496
|
<div class="card footnote" style="max-width:1300px;margin:18px auto;">
|
|
405
497
|
<p class="muted">This demo doubles as a readable API reference and playground — try uploading files,
|
|
406
498
|
inspect the returned JSON, and use the playground to test parsers.</p>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* highlightAllPre)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
/*
|
|
15
|
+
/* -- TOC Helpers ------------------------------------------------------------- */
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Find a top-level `<li>` in the sidebar TOC whose link matches the given
|
|
@@ -56,7 +56,7 @@ function populateTocSub(href, items)
|
|
|
56
56
|
parentLi.appendChild(sub);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/*
|
|
59
|
+
/* -- API Reference ----------------------------------------------------------- */
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Render a list of API items into the `#api-items` container and Prism-highlight
|
|
@@ -196,7 +196,7 @@ async function loadApiReference()
|
|
|
196
196
|
} catch (e) { }
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/*
|
|
199
|
+
/* -- Options Table ----------------------------------------------------------- */
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
202
|
* Fetch the options JSON and render a `<table>` into `#options-items`.
|
|
@@ -236,7 +236,7 @@ async function loadOptions()
|
|
|
236
236
|
} catch (e) { console.error('loadOptions error', e); }
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
/*
|
|
239
|
+
/* -- Code Examples ----------------------------------------------------------- */
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
242
|
* Fetch the examples JSON, render each as a collapsible accordion, and
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* documentation scripts. Loaded first so every other module can rely on these.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/*
|
|
7
|
+
/* -- DOM Selectors ----------------------------------------------------------- */
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Query a single element by CSS selector.
|
|
@@ -34,7 +34,7 @@ function on(el, evt, cb)
|
|
|
34
34
|
el.addEventListener(evt, cb);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/*
|
|
37
|
+
/* -- String Utilities -------------------------------------------------------- */
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Escape HTML special characters for safe insertion into the DOM.
|
|
@@ -76,7 +76,7 @@ function formatBytes(n)
|
|
|
76
76
|
return (n / Math.pow(k, i)).toFixed(i ? 1 : 0) + ' ' + sizes[i];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/*
|
|
79
|
+
/* -- JSON / Code Rendering --------------------------------------------------- */
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Build a highlighted `<pre>` block containing pretty-printed JSON.
|
|
@@ -100,7 +100,7 @@ function showJsonResult(container, obj)
|
|
|
100
100
|
try { highlightAllPre(); } catch (e) { }
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/*
|
|
103
|
+
/* -- Prism / Code-Block Helpers ---------------------------------------------- */
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Trigger Prism syntax highlighting on all `<pre class="code">` blocks, or
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* playground.js
|
|
3
3
|
* Echo playground forms — JSON, URL-encoded, and plain-text body parsers.
|
|
4
|
+
* WebSocket chat client and SSE event viewer.
|
|
4
5
|
*
|
|
5
6
|
* Depends on: helpers.js (provides $, on, escapeHtml, showJsonResult,
|
|
6
7
|
* highlightAllPre)
|
|
@@ -68,4 +69,204 @@ function initPlayground()
|
|
|
68
69
|
playResult.innerHTML = `<pre class="code"><code>${escapeHtml(text)}</code></pre>`;
|
|
69
70
|
try { highlightAllPre(); } catch (e) { }
|
|
70
71
|
});
|
|
72
|
+
|
|
73
|
+
/* --- WebSocket Chat --- */
|
|
74
|
+
initWsChat();
|
|
75
|
+
|
|
76
|
+
/* --- SSE Viewer --- */
|
|
77
|
+
initSseViewer();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ------------------------------------------------------------------ */
|
|
81
|
+
/* WebSocket Chat */
|
|
82
|
+
/* ------------------------------------------------------------------ */
|
|
83
|
+
function initWsChat()
|
|
84
|
+
{
|
|
85
|
+
let ws = null;
|
|
86
|
+
const connectBtn = $('#wsConnectBtn');
|
|
87
|
+
const disconnectBtn = $('#wsDisconnectBtn');
|
|
88
|
+
const nameInput = $('#wsName');
|
|
89
|
+
const msgInput = $('#wsMsgInput');
|
|
90
|
+
const sendBtn = $('#wsSendBtn');
|
|
91
|
+
const messages = $('#wsMessages');
|
|
92
|
+
|
|
93
|
+
if (!connectBtn) return; // guard if elements not in DOM
|
|
94
|
+
|
|
95
|
+
function appendMsg(html)
|
|
96
|
+
{
|
|
97
|
+
const div = document.createElement('div');
|
|
98
|
+
div.innerHTML = html;
|
|
99
|
+
messages.appendChild(div);
|
|
100
|
+
messages.scrollTop = messages.scrollHeight;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setConnected(connected)
|
|
104
|
+
{
|
|
105
|
+
connectBtn.disabled = connected;
|
|
106
|
+
disconnectBtn.disabled = !connected;
|
|
107
|
+
msgInput.disabled = !connected;
|
|
108
|
+
sendBtn.disabled = !connected;
|
|
109
|
+
nameInput.disabled = connected;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
on(connectBtn, 'click', () =>
|
|
113
|
+
{
|
|
114
|
+
const name = encodeURIComponent(nameInput.value || 'anon');
|
|
115
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
116
|
+
ws = new WebSocket(`${proto}//${location.host}/ws/chat?name=${name}`);
|
|
117
|
+
|
|
118
|
+
ws.onopen = () =>
|
|
119
|
+
{
|
|
120
|
+
setConnected(true);
|
|
121
|
+
appendMsg('<span style="color:#6f6">● Connected</span>');
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
ws.onmessage = (e) =>
|
|
125
|
+
{
|
|
126
|
+
try
|
|
127
|
+
{
|
|
128
|
+
const msg = JSON.parse(e.data);
|
|
129
|
+
if (msg.type === 'system')
|
|
130
|
+
{
|
|
131
|
+
appendMsg(`<span style="color:#aaa">» ${escapeHtml(msg.text)}</span>`);
|
|
132
|
+
} else
|
|
133
|
+
{
|
|
134
|
+
appendMsg(`<strong>${escapeHtml(msg.name)}</strong>: ${escapeHtml(msg.text)}`);
|
|
135
|
+
}
|
|
136
|
+
} catch (_)
|
|
137
|
+
{
|
|
138
|
+
appendMsg(escapeHtml(e.data));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
ws.onclose = () =>
|
|
143
|
+
{
|
|
144
|
+
setConnected(false);
|
|
145
|
+
appendMsg('<span style="color:#f66">● Disconnected</span>');
|
|
146
|
+
ws = null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
ws.onerror = () =>
|
|
150
|
+
{
|
|
151
|
+
appendMsg('<span style="color:#f66">● Connection error</span>');
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
on(disconnectBtn, 'click', () =>
|
|
156
|
+
{
|
|
157
|
+
if (ws) ws.close();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
on(sendBtn, 'click', () =>
|
|
161
|
+
{
|
|
162
|
+
if (ws && ws.readyState === WebSocket.OPEN && msgInput.value.trim())
|
|
163
|
+
{
|
|
164
|
+
ws.send(msgInput.value.trim());
|
|
165
|
+
msgInput.value = '';
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
on(msgInput, 'keydown', (e) =>
|
|
170
|
+
{
|
|
171
|
+
if (e.key === 'Enter')
|
|
172
|
+
{
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
sendBtn.click();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ------------------------------------------------------------------ */
|
|
180
|
+
/* SSE Event Viewer */
|
|
181
|
+
/* ------------------------------------------------------------------ */
|
|
182
|
+
function initSseViewer()
|
|
183
|
+
{
|
|
184
|
+
let es = null;
|
|
185
|
+
const connectBtn = $('#sseConnectBtn');
|
|
186
|
+
const disconnectBtn = $('#sseDisconnectBtn');
|
|
187
|
+
const status = $('#sseStatus');
|
|
188
|
+
const messages = $('#sseMessages');
|
|
189
|
+
const broadcastBtn = $('#sseBroadcastBtn');
|
|
190
|
+
const broadcastInput = $('#sseBroadcastInput');
|
|
191
|
+
|
|
192
|
+
if (!connectBtn) return; // guard
|
|
193
|
+
|
|
194
|
+
function appendMsg(html)
|
|
195
|
+
{
|
|
196
|
+
const div = document.createElement('div');
|
|
197
|
+
div.innerHTML = html;
|
|
198
|
+
messages.appendChild(div);
|
|
199
|
+
messages.scrollTop = messages.scrollHeight;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setConnected(connected)
|
|
203
|
+
{
|
|
204
|
+
connectBtn.disabled = connected;
|
|
205
|
+
disconnectBtn.disabled = !connected;
|
|
206
|
+
status.textContent = connected ? 'Connected' : 'Disconnected';
|
|
207
|
+
status.style.color = connected ? '#6f6' : '';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
on(connectBtn, 'click', () =>
|
|
211
|
+
{
|
|
212
|
+
es = new EventSource('/sse/events');
|
|
213
|
+
|
|
214
|
+
es.onopen = () =>
|
|
215
|
+
{
|
|
216
|
+
setConnected(true);
|
|
217
|
+
appendMsg('<span style="color:#6f6">● SSE connected</span>');
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
es.onmessage = (e) =>
|
|
221
|
+
{
|
|
222
|
+
const ts = new Date().toLocaleTimeString();
|
|
223
|
+
appendMsg(`<span style="color:#aaa">[${ts}]</span> ${escapeHtml(e.data)}`);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
es.addEventListener('broadcast', (e) =>
|
|
227
|
+
{
|
|
228
|
+
const ts = new Date().toLocaleTimeString();
|
|
229
|
+
appendMsg(`<span style="color:#ff0">[${ts} broadcast]</span> ${escapeHtml(e.data)}`);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
es.onerror = () =>
|
|
233
|
+
{
|
|
234
|
+
if (es.readyState === EventSource.CLOSED)
|
|
235
|
+
{
|
|
236
|
+
setConnected(false);
|
|
237
|
+
appendMsg('<span style="color:#f66">● SSE closed</span>');
|
|
238
|
+
es = null;
|
|
239
|
+
} else
|
|
240
|
+
{
|
|
241
|
+
appendMsg('<span style="color:#fa0">● SSE reconnecting…</span>');
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
on(disconnectBtn, 'click', () =>
|
|
247
|
+
{
|
|
248
|
+
if (es) { es.close(); es = null; }
|
|
249
|
+
setConnected(false);
|
|
250
|
+
appendMsg('<span style="color:#f66">● Disconnected</span>');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
on(broadcastBtn, 'click', async () =>
|
|
254
|
+
{
|
|
255
|
+
const raw = broadcastInput.value || '{}';
|
|
256
|
+
let body;
|
|
257
|
+
try { body = JSON.parse(raw); }
|
|
258
|
+
catch (err)
|
|
259
|
+
{
|
|
260
|
+
appendMsg(`<span style="color:#f66">Invalid JSON: ${escapeHtml(err.message)}</span>`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const r = await fetch('/sse/broadcast', {
|
|
265
|
+
method: 'POST',
|
|
266
|
+
headers: { 'Content-Type': 'application/json' },
|
|
267
|
+
body: JSON.stringify(body),
|
|
268
|
+
});
|
|
269
|
+
const j = await r.json();
|
|
270
|
+
appendMsg(`<span style="color:#aaa">» Broadcast sent to ${j.sent} client(s)</span>`);
|
|
271
|
+
});
|
|
71
272
|
}
|
|
@@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', () =>
|
|
|
14
14
|
initTocToolbar();
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
/*
|
|
17
|
+
/* -- Feature Tabs ------------------------------------------------------------ */
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Wire the feature / server-model tab buttons so clicking one activates
|
|
@@ -47,7 +47,7 @@ function initFeatureTabs()
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/*
|
|
50
|
+
/* -- TOC Sidebar Toggle ------------------------------------------------------ */
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Wire the hamburger button to toggle the sidebar on both desktop (persistent)
|
|
@@ -106,7 +106,7 @@ function initTocSidebar()
|
|
|
106
106
|
window.addEventListener('resize', syncAria);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/*
|
|
109
|
+
/* -- TOC Smooth-Scroll Navigation -------------------------------------------- */
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
112
|
* When clicking a TOC link that points to a `#hash`, auto-open any ancestor
|
|
@@ -166,7 +166,7 @@ function initTocNavigation()
|
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/*
|
|
169
|
+
/* -- TOC Toolbar (scroll-to-top & expand/collapse all) ----------------------- */
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* Wire the icon-bar buttons at the top of the sidebar:
|
|
@@ -179,7 +179,7 @@ function initTocToolbar()
|
|
|
179
179
|
const toggleBtn = document.getElementById('toc-toggle-acc');
|
|
180
180
|
if (!topBtn && !toggleBtn) return;
|
|
181
181
|
|
|
182
|
-
/*
|
|
182
|
+
/* -- Scroll to top -------------------------------------------------- */
|
|
183
183
|
const brandBtn = document.getElementById('brand-top');
|
|
184
184
|
|
|
185
185
|
[topBtn, brandBtn].forEach(el =>
|
|
@@ -192,7 +192,7 @@ function initTocToolbar()
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
/*
|
|
195
|
+
/* -- Expand / Collapse all accordions ------------------------------- */
|
|
196
196
|
if (toggleBtn)
|
|
197
197
|
{
|
|
198
198
|
let expanded = false;
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
* showJsonResult, highlightAllPre)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
/*
|
|
10
|
+
/* -- Pagination State -------------------------------------------------------- */
|
|
11
11
|
|
|
12
12
|
let currentPage = 1;
|
|
13
13
|
let currentSort = 'mtime';
|
|
14
14
|
let currentOrder = 'desc';
|
|
15
15
|
const pageSize = 4;
|
|
16
16
|
|
|
17
|
-
/*
|
|
17
|
+
/* -- Combined Uploads + Trash JSON ------------------------------------------- */
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Fetch the combined uploads + trash listing and render it as JSON into the
|
|
@@ -30,7 +30,7 @@ async function loadUploadsCombined()
|
|
|
30
30
|
} catch (e) { }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/*
|
|
33
|
+
/* -- Convenience: DELETE + Show Result --------------------------------------- */
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Issue a DELETE request and display the JSON response.
|
|
@@ -46,7 +46,7 @@ async function deleteAndShow(path, container)
|
|
|
46
46
|
try { await loadUploadsCombined(); } catch (e) { showJsonResult(container, j); }
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/*
|
|
49
|
+
/* -- Trash Row Factory ------------------------------------------------------- */
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Build a trash-row DOM node with Restore and Delete Permanently buttons.
|
|
@@ -90,7 +90,7 @@ function createTrashRow(name)
|
|
|
90
90
|
return row;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/*
|
|
93
|
+
/* -- Trash List -------------------------------------------------------------- */
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Fetch the trash listing from the server and render rows into `#trashList`.
|
|
@@ -125,7 +125,7 @@ function addTrashRow(name)
|
|
|
125
125
|
else trashList.appendChild(row);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/*
|
|
128
|
+
/* -- Undo Toast -------------------------------------------------------------- */
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Display a transient undo panel when a file is moved to trash.
|
|
@@ -161,7 +161,7 @@ function showUndo(name)
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/*
|
|
164
|
+
/* -- Upload Card Factory ----------------------------------------------------- */
|
|
165
165
|
|
|
166
166
|
/** Inline SVG placeholder for non-image files. */
|
|
167
167
|
const PLACEHOLDER_SVG = 'data:image/svg+xml;utf8,' + encodeURIComponent(
|
|
@@ -241,7 +241,7 @@ function createUploadCard(f, uploadResult)
|
|
|
241
241
|
return card;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
/*
|
|
244
|
+
/* -- Paginated Upload List --------------------------------------------------- */
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
247
|
* Fetch the paginated upload list and render file cards into `#uploadsList`.
|
|
@@ -310,7 +310,7 @@ async function loadUploadsList()
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
/*
|
|
313
|
+
/* -- Wire Upload Form & Bulk Actions ----------------------------------------- */
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
316
|
* Initialise the upload form (XHR with progress), pagination controls, and
|
|
@@ -188,24 +188,24 @@ details.acc[open] > summary:before{transform:rotate(90deg)}
|
|
|
188
188
|
.tabs-content{display:block}
|
|
189
189
|
.tab-panel{display:none;padding:6px 2px;transition:opacity .18s ease}
|
|
190
190
|
.tab-panel.active{display:block}
|
|
191
|
-
.feature-grid{display:grid;grid-template-columns:repeat(
|
|
192
|
-
.feature-card{background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);padding:
|
|
193
|
-
.feature-card:hover{transform:translateY(-
|
|
194
|
-
.feature-icon{width:
|
|
195
|
-
.feature-icon svg{width:
|
|
196
|
-
.feature-icon img.icon-svg{width:
|
|
197
|
-
|
|
198
|
-
.feature-
|
|
199
|
-
|
|
200
|
-
.feature-
|
|
201
|
-
.feature-card p{margin:0;color:var(--muted);font-size:14px}
|
|
191
|
+
.feature-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px}
|
|
192
|
+
.feature-card{background:linear-gradient(180deg,rgba(255,255,255,0.02),transparent);padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.03);display:flex;flex-direction:row;align-items:center;gap:10px;transition:transform .12s ease,box-shadow .12s ease}
|
|
193
|
+
.feature-card:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,0.5)}
|
|
194
|
+
.feature-icon{width:40px;height:40px;min-width:40px;border-radius:10px;background:linear-gradient(180deg, rgba(88,101,242,0.12), rgba(31,142,253,0.06));display:grid;place-items:center;font-size:16px;color:var(--accent);}
|
|
195
|
+
.feature-icon svg{width:26px;height:26px;display:block;fill:var(--accent);}
|
|
196
|
+
.feature-icon img.icon-svg{width:30px;height:30px;display:block;object-fit:contain;margin:0 auto}
|
|
197
|
+
.feature-icon img.icon-svg[src$="/static.svg"], .feature-icon img.icon-svg[src$="static.svg"]{transform:translateY(1px);display:block}
|
|
198
|
+
.feature-card h5{margin:0;font-size:13px;line-height:1.2}
|
|
199
|
+
.feature-card p{margin:0;color:var(--muted);font-size:12px;line-height:1.3}
|
|
200
|
+
.feature-text{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
202
201
|
.behavior-list{display:flex;flex-direction:column;gap:10px}
|
|
203
202
|
.behavior-step{display:flex;gap:12px;align-items:flex-start;padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.03);background:linear-gradient(180deg,rgba(255,255,255,0.01),transparent)}
|
|
204
203
|
.step-num{width:44px;height:44px;border-radius:10px;background:var(--accent);display:flex;align-items:center;justify-content:center;font-weight:800;color:#fff;box-shadow:0 8px 18px rgba(31,142,253,0.08)}
|
|
205
204
|
.step-body h5{margin:0 0 6px 0}
|
|
206
205
|
.step-body p{margin:0;color:var(--muted)}
|
|
207
206
|
|
|
208
|
-
@media(max-width:900px){.feature-grid{grid-template-columns:1fr}.tabs-nav{flex-wrap:wrap}}
|
|
207
|
+
@media(max-width:900px){.feature-grid{grid-template-columns:repeat(auto-fill,minmax(180px,1fr))}.tabs-nav{flex-wrap:wrap}}
|
|
208
|
+
@media(max-width:500px){.feature-grid{grid-template-columns:1fr}}
|
|
209
209
|
|
|
210
210
|
@media(max-width:900px){.playgrid{grid-template-columns:1fr}.ui-shell{padding:16px}}
|
|
211
211
|
|