vectorplasticity commited on
Commit
a00f3fc
Β·
verified Β·
1 Parent(s): b690638

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +97 -244
server.js CHANGED
@@ -14,13 +14,12 @@
14
  *
15
  * Deploy:
16
  * 1. Create Space β†’ Docker SDK
17
- * 2. Push this file + Dockerfile.hf
18
- * 3. Set port to 7860 (already default)
19
- * 4. Your Space URL becomes the endpoint!
20
  *
21
- * Read: https://USER-REPO.hf.space/blobs.jsonl
22
- * Write: https://USER-REPO.hf.space/submit
23
- * Web: https://USER-REPO.hf.space/
24
  */
25
 
26
  const http = require('http');
@@ -31,26 +30,19 @@ const urlMod = require('url');
31
  // ─── Configuration ───────────────────────────────────────────────────
32
  const PORT = parseInt(process.env.PORT) || 7860;
33
 
34
- // HuggingFace Spaces mounts a persistent volume at /data
35
- let DATA_DIR = '/data';
36
- let BLOBS_FILE = path.join(DATA_DIR, 'blobs.jsonl');
 
 
37
 
38
  // ─── Initialize ──────────────────────────────────────────────────────
39
- if (!fs.existsSync(DATA_DIR)) {
40
- // Fallback to local ./data if /data doesn't exist (local dev)
41
- DATA_DIR = path.join(__dirname, 'data');
42
- BLOBS_FILE = path.join(DATA_DIR, 'blobs.jsonl');
43
- console.warn('⚠️ /data volume not found β€” using local ./data instead');
44
- }
45
-
46
  if (!fs.existsSync(DATA_DIR)) {
47
  fs.mkdirSync(DATA_DIR, { recursive: true });
48
- console.log(`πŸ“ Created data directory: ${DATA_DIR}`);
49
- } else {
50
- console.log(`πŸ“ Using HuggingFace persistent volume: ${DATA_DIR}`);
51
  }
 
52
 
53
- // ─── Helper: get message count ──────────────────────────────────────
54
  function getStats() {
55
  if (!fs.existsSync(BLOBS_FILE)) {
56
  return { messages: 0, size_bytes: 0, size_human: '0 B' };
@@ -83,176 +75,80 @@ function renderDashboard() {
83
  const idShort = entry.id ? entry.id.substring(0, 16) + '...' : '?';
84
  const time = entry.ts ? new Date(entry.ts).toLocaleString() : '?';
85
  const blobSize = entry.blob ? (entry.blob.length / 1024).toFixed(1) + ' KB' : '?';
86
- return `<tr>
87
- <td>${idShort}</td>
88
- <td>${time}</td>
89
- <td>${blobSize}</td>
90
- </tr>`;
91
  } catch {
92
- return `<tr><td colspan="3" style="color:var(--red)">⚠️ Malformed line</td></tr>`;
93
  }
94
  }).join('');
95
  }
96
 
97
  return `<!DOCTYPE html>
98
  <html lang="en">
99
- <head>
100
- <meta charset="UTF-8">
101
- <meta name="viewport" content="width=device-width,initial-scale=1">
102
  <title>πŸ” SocialCrypt β€” HuggingFace Endpoint</title>
103
  <style>
104
- :root {
105
- --bg: #0d1117; --card: #161b22; --border: #30363d;
106
- --text: #e6edf3; --dim: #8b949e; --accent: #58a6ff;
107
- --green: #3fb950; --orange: #d29922; --red: #f85149;
108
- }
109
- * { box-sizing: border-box; margin: 0; padding: 0; }
110
- body {
111
- background: var(--bg); color: var(--text);
112
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
113
- line-height: 1.6; padding: 20px;
114
- }
115
- .container { max-width: 800px; margin: 0 auto; }
116
- .hero {
117
- text-align: center; padding: 40px 20px;
118
- border-bottom: 1px solid var(--border); margin-bottom: 24px;
119
- }
120
- .hero h1 { font-size: 2.5rem; }
121
- .hero h1 span { color: var(--accent); }
122
- .hero p { color: var(--dim); margin-top: 8px; font-size: 1.1rem; }
123
- .hero .hf-badge {
124
- display: inline-block; margin-top: 12px;
125
- padding: 4px 16px; border-radius: 999px;
126
- background: rgba(255, 152, 0, 0.15); color: #ff9800;
127
- font-size: 0.85rem; font-weight: 600;
128
- }
129
- .card {
130
- background: var(--card); border: 1px solid var(--border);
131
- border-radius: 12px; padding: 24px; margin-bottom: 16px;
132
- }
133
- .card h2 { font-size: 1.2rem; margin-bottom: 16px; }
134
- .stat-grid {
135
- display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
136
- }
137
- .stat-box {
138
- text-align: center; padding: 16px;
139
- background: var(--bg); border: 1px solid var(--border);
140
- border-radius: 8px;
141
- }
142
- .stat-box .num { font-size: 2rem; font-weight: 700; color: var(--accent); }
143
- .stat-box .label { font-size: 0.82rem; color: var(--dim); margin-top: 4px; }
144
- .endpoint-list { list-style: none; }
145
- .endpoint-list li {
146
- display: flex; align-items: center; gap: 8px;
147
- padding: 12px; margin-bottom: 8px;
148
- background: var(--bg); border: 1px solid var(--border);
149
- border-radius: 8px; font-family: 'SF Mono', 'Fira Code', monospace;
150
- font-size: 0.85rem; word-break: break-all;
151
- }
152
- .endpoint-list .method {
153
- display: inline-block; padding: 2px 8px; border-radius: 4px;
154
- font-size: 0.75rem; font-weight: 700; flex-shrink: 0;
155
- }
156
- .method.get { background: rgba(88, 166, 255, 0.15); color: var(--accent); }
157
- .method.post { background: rgba(63, 185, 80, 0.15); color: var(--green); }
158
- table { width: 100%; border-collapse: collapse; }
159
- th {
160
- text-align: left; padding: 10px 12px;
161
- border-bottom: 2px solid var(--border);
162
- color: var(--dim); font-size: 0.82rem; text-transform: uppercase;
163
- letter-spacing: 0.05em;
164
- }
165
- td {
166
- padding: 10px 12px; border-bottom: 1px solid var(--border);
167
- font-size: 0.88rem; font-family: 'SF Mono', monospace;
168
- }
169
- tr:hover td { background: rgba(255,255,255,0.03); }
170
- .footer {
171
- text-align: center; padding: 24px;
172
- color: var(--dim); font-size: 0.85rem;
173
- }
174
- .footer a { color: var(--accent); text-decoration: none; }
175
- .tip {
176
- background: rgba(88, 166, 255, 0.08);
177
- border: 1px solid rgba(88, 166, 255, 0.2);
178
- border-radius: 8px; padding: 16px; margin-top: 12px;
179
- font-size: 0.9rem;
180
- }
181
- .tip code { background: var(--bg); padding: 2px 6px; border-radius: 4px; font-size: 0.82rem; }
182
- @media (max-width: 600px) {
183
- .hero { padding: 24px 12px; }
184
- .hero h1 { font-size: 1.8rem; }
185
- .stat-grid { grid-template-columns: 1fr; }
186
- }
187
- </style>
188
- </head>
189
- <body>
190
- <div class="container">
191
  <div class="hero">
192
  <h1><span>πŸ”</span> SocialCrypt</h1>
193
- <p>Encrypted blob store. Running on HuggingFace Spaces.</p>
194
- <div class="hf-badge">πŸ€— Powered by HuggingFace Spaces</div>
195
  </div>
196
  <div class="card">
197
- <h2>πŸ“Š Live Stats</h2>
198
- <div class="stat-grid">
199
- <div class="stat-box">
200
- <div class="num">${stats.messages}</div>
201
- <div class="label">Messages Stored</div>
202
- </div>
203
- <div class="stat-box">
204
- <div class="num">${stats.size_human}</div>
205
- <div class="label">Total Size</div>
206
- </div>
207
- <div class="stat-box">
208
- <div class="num">/data</div>
209
- <div class="label">Persistent Volume</div>
210
- </div>
211
  </div>
212
  </div>
213
  <div class="card">
214
  <h2>πŸ“‘ Endpoints</h2>
215
- <p style="color:var(--dim);margin-bottom:12px;">Point your SocialCrypt client at these URLs.</p>
216
- <ul class="endpoint-list">
217
- <li><span class="method get">GET</span><span><strong>/blobs.jsonl</strong> β€” Fetch all stored blobs (JSONL format)</span></li>
218
- <li><span class="method post">POST</span><span><strong>/submit</strong> β€” Submit a new blob <code>{"id":"...","blob":"..."}</code></span></li>
219
- <li><span class="method get">GET</span><span><strong>/stats</strong> β€” JSON stats</span></li>
220
- <li><span class="method get">GET</span><span><strong>/health</strong> β€” Health check</span></li>
221
  </ul>
222
- <div class="tip">
223
- πŸ’‘ <strong>Client setup:</strong> In <code>social.html</code>, add these as your endpoint:<br><br>
224
- <strong>Read URL:</strong> <code>https://${process.env.SPACE_ID || 'USER-REPO'}.hf.space/blobs.jsonl</code><br>
225
- <strong>Submit URL:</strong> <code>https://${process.env.SPACE_ID || 'USER-REPO'}.hf.space/submit</code>
226
- </div>
227
  </div>
228
  <div class="card">
229
- <h2>πŸ“¬ Recent Messages ${hasMessages ? `(last ${Math.min(stats.messages, 50)} of ${stats.messages})` : ''}</h2>
230
- ${hasMessages ? `
231
- <div style="overflow-x:auto;">
232
- <table>
233
- <thead><tr><th>Recipient ID</th><th>Timestamp</th><th>Blob Size</th></tr></thead>
234
- <tbody>${messagesHtml}</tbody>
235
- </table>
236
- </div>
237
- <p style="color:var(--dim);font-size:0.8rem;margin-top:8px;">πŸ”’ Blobs are encrypted β€” the server cannot read them.</p>
238
- ` : `<p style="text-align:center;padding:40px;color:var(--dim);">πŸ“­ No messages yet. Share this endpoint!</p>`}
239
- </div>
240
- <div class="card" style="text-align:center;">
241
- <h2>πŸ” Zero-Knowledge Architecture</h2>
242
- <p style="color:var(--dim);font-size:0.9rem;">This server stores <strong>only encrypted blobs</strong>. Messages are encrypted in-browser with AES-256-GCM, and the AES key is wrapped with the recipient's RSA-2048 public key. The server never sees plaintext. Ever.</p>
243
- <p style="color:var(--dim);font-size:0.85rem;margin-top:8px;">Data persists in the <code>/data</code> HuggingFace volume β€” survives restarts and sleeps.</p>
244
- </div>
245
- <div class="footer">
246
- <a href="https://github.com/YOUR_USER/socialcrypt" target="_blank">SocialCrypt</a> β€” Encrypted Social Messaging
247
  </div>
248
- </div>
249
- </body>
250
- </html>`;
251
  }
252
 
253
  // ─── HTTP Server ─────────────────────────────────────────────────────
254
  const server = http.createServer((req, res) => {
255
- // CORS
256
  res.setHeader('Access-Control-Allow-Origin', '*');
257
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, HEAD');
258
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
@@ -262,17 +158,16 @@ const server = http.createServer((req, res) => {
262
  return res.end();
263
  }
264
 
265
- const parsed = urlMod.parse(req.url, true);
266
- const pathname = parsed.pathname;
267
 
268
- // ── GET / β€” Dashboard ─────────────────────────────────────────────
269
  if (req.method === 'GET' && pathname === '/') {
270
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
271
  res.end(renderDashboard());
272
  return;
273
  }
274
 
275
- // ── GET /blobs.jsonl β€” Stream blob store ──────────────────────────
276
  if (req.method === 'GET' && pathname === '/blobs.jsonl') {
277
  const stats = getStats();
278
  res.writeHead(200, {
@@ -281,16 +176,14 @@ const server = http.createServer((req, res) => {
281
  'X-Server': 'SocialCrypt-HF'
282
  });
283
  if (fs.existsSync(BLOBS_FILE)) {
284
- const stream = fs.createReadStream(BLOBS_FILE);
285
- stream.on('error', () => res.end());
286
- stream.pipe(res);
287
  } else {
288
  res.end('');
289
  }
290
  return;
291
  }
292
 
293
- // ── POST /submit β€” Append a new blob ──────────────────────────────
294
  if (req.method === 'POST' && pathname === '/submit') {
295
  let body = '';
296
  req.on('data', chunk => (body += chunk));
@@ -299,28 +192,19 @@ const server = http.createServer((req, res) => {
299
  const data = JSON.parse(body);
300
  if (!data.id || !data.blob) {
301
  res.writeHead(400, { 'Content-Type': 'application/json' });
302
- return res.end(JSON.stringify({ error: 'Missing required fields: id, blob' }));
303
  }
304
  if (typeof data.id !== 'string' || typeof data.blob !== 'string') {
305
  res.writeHead(400, { 'Content-Type': 'application/json' });
306
  return res.end(JSON.stringify({ error: 'id and blob must be strings' }));
307
  }
308
-
309
- const line = JSON.stringify({
310
- id: data.id,
311
- ts: Date.now(),
312
- blob: data.blob
313
- }) + '\n';
314
-
315
- // Ensure directory exists
316
- const dir = path.dirname(BLOBS_FILE);
317
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
318
-
319
  fs.appendFileSync(BLOBS_FILE, line, 'utf-8');
320
-
321
  const stats = getStats();
322
- console.log(`πŸ“¨ [${new Date().toISOString()}] Stored blob for ${data.id.substring(0,16)}... | Total: ${stats.messages}`);
323
-
324
  res.writeHead(201, { 'Content-Type': 'application/json' });
325
  res.end(JSON.stringify({ ok: true, total_messages: stats.messages }));
326
  } catch (e) {
@@ -331,29 +215,17 @@ const server = http.createServer((req, res) => {
331
  return;
332
  }
333
 
334
- // ── GET /stats β€” JSON stats ───────────────────────────────────────
335
  if (req.method === 'GET' && pathname === '/stats') {
336
- const stats = getStats();
337
  res.writeHead(200, { 'Content-Type': 'application/json' });
338
- res.end(JSON.stringify({
339
- ...stats,
340
- data_dir: DATA_DIR,
341
- store_file: BLOBS_FILE,
342
- uptime_seconds: Math.floor(process.uptime()),
343
- platform: 'huggingface_spaces'
344
- }));
345
  return;
346
  }
347
 
348
- // ── GET /health β€” Health check ────────────────────────────────────
349
  if (req.method === 'GET' && pathname === '/health') {
350
- let writable = true;
351
- try {
352
- fs.accessSync(path.dirname(BLOBS_FILE), fs.constants.W_OK);
353
- } catch {
354
- writable = false;
355
- }
356
-
357
  res.writeHead(writable ? 200 : 503, { 'Content-Type': 'application/json' });
358
  res.end(JSON.stringify({
359
  status: writable ? 'ok' : 'degraded',
@@ -365,54 +237,35 @@ const server = http.createServer((req, res) => {
365
  return;
366
  }
367
 
368
- // ── 404 ───────────────────────────────────────────────────────────
369
  res.writeHead(404, { 'Content-Type': 'application/json' });
370
- res.end(JSON.stringify({
371
- error: 'Not found',
372
- available: [
373
- { path: '/', method: 'GET', desc: 'Web dashboard' },
374
- { path: '/blobs.jsonl', method: 'GET', desc: 'Fetch all blobs' },
375
- { path: '/submit', method: 'POST', desc: 'Submit a new blob' },
376
- { path: '/stats', method: 'GET', desc: 'Server statistics' },
377
- { path: '/health', method: 'GET', desc: 'Health check' }
378
- ]
379
- }));
380
  });
381
 
382
  // ─── Startup ─────────────────────────────────────────────────────────
383
  server.listen(PORT, '0.0.0.0', () => {
384
- const spaceId = process.env.SPACE_ID || 'local-dev';
385
- console.log(`β”Œβ”€β”€β”€β”€β”€β”€β”€οΏ½οΏ½β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”`);
386
- console.log(`β”‚ πŸ” SocialCrypt β€” HuggingFace Spaces Edition β”‚`);
387
- console.log(`β”‚ ─────────────────────────────────── β”‚`);
388
- console.log(`β”‚ β”‚`);
389
- console.log(`β”‚ πŸ“‘ Server: http://0.0.0.0:${String(PORT).padEnd(5)} β”‚`);
390
- console.log(`β”‚ πŸ’Ύ Volume: ${String(DATA_DIR).padEnd(31)}β”‚`);
391
- console.log(`β”‚ β”‚`);
392
- console.log(`β”‚ 🌐 Dashboard: GET / β”‚`);
393
- console.log(`β”‚ πŸ“– Read: GET /blobs.jsonl β”‚`);
394
- console.log(`β”‚ ✏️ Write: POST /submit { id, blob } β”‚`);
395
- console.log(`β”‚ πŸ“Š Stats: GET /stats β”‚`);
396
- console.log(`β”‚ ❀️ Health: GET /health β”‚`);
397
- console.log(`β”‚ β”‚`);
398
- const stats = getStats();
399
- console.log(`β”‚ πŸ“¬ ${String(stats.messages).padStart(5)} messages in store β”‚`);
400
- console.log(`β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜`);
401
- console.log(`πŸš€ Open http://localhost:${PORT} to see the dashboard.`);
402
  });
403
 
404
  // ─── Graceful Shutdown ──────────────────────────────────────────────
405
  function shutdown(signal) {
406
- console.log(`\nπŸ›‘ ${signal} received β€” shutting down gracefully...`);
407
- server.close(() => {
408
- console.log('βœ… Server closed. Data safely stored in persistent volume.');
409
- process.exit(0);
410
- });
411
- setTimeout(() => {
412
- console.error('⏰ Forced shutdown after timeout');
413
- process.exit(1);
414
- }, 5000);
415
  }
416
-
417
  process.on('SIGTERM', () => shutdown('SIGTERM'));
418
  process.on('SIGINT', () => shutdown('SIGINT'));
 
14
  *
15
  * Deploy:
16
  * 1. Create Space β†’ Docker SDK
17
+ * 2. Push server_hf.js + Dockerfile.hf
18
+ * 3. Done! Your Space URL becomes the endpoint.
 
19
  *
20
+ * Read: https://USERNAME-SPACENAME.hf.space/blobs.jsonl
21
+ * Write: https://USERNAME-SPACENAME.hf.space/submit
22
+ * Web: https://USERNAME-SPACENAME.hf.space/
23
  */
24
 
25
  const http = require('http');
 
30
  // ─── Configuration ───────────────────────────────────────────────────
31
  const PORT = parseInt(process.env.PORT) || 7860;
32
 
33
+ // HuggingFace Spaces mounts persistent storage at /data
34
+ // The Dockerfile does: RUN mkdir -p /data && chmod 777 /data
35
+ // so it's always writable. Fallback to ./data for local dev.
36
+ const DATA_DIR = fs.existsSync('/data') ? '/data' : path.join(__dirname, 'data');
37
+ const BLOBS_FILE = path.join(DATA_DIR, 'blobs.jsonl');
38
 
39
  // ─── Initialize ──────────────────────────────────────────────────────
 
 
 
 
 
 
 
40
  if (!fs.existsSync(DATA_DIR)) {
41
  fs.mkdirSync(DATA_DIR, { recursive: true });
 
 
 
42
  }
43
+ console.log(`πŸ“ Data directory: ${DATA_DIR} (${DATA_DIR === '/data' ? 'HF persistent volume' : 'local'})`);
44
 
45
+ // ─── Helper: get stats ──────────────────────────────────────────────
46
  function getStats() {
47
  if (!fs.existsSync(BLOBS_FILE)) {
48
  return { messages: 0, size_bytes: 0, size_human: '0 B' };
 
75
  const idShort = entry.id ? entry.id.substring(0, 16) + '...' : '?';
76
  const time = entry.ts ? new Date(entry.ts).toLocaleString() : '?';
77
  const blobSize = entry.blob ? (entry.blob.length / 1024).toFixed(1) + ' KB' : '?';
78
+ return `<tr><td>${idShort}</td><td>${time}</td><td>${blobSize}</td></tr>`;
 
 
 
 
79
  } catch {
80
+ return `<tr><td colspan="3" style="color:var(--red)">⚠️ Malformed</td></tr>`;
81
  }
82
  }).join('');
83
  }
84
 
85
  return `<!DOCTYPE html>
86
  <html lang="en">
87
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
 
 
88
  <title>πŸ” SocialCrypt β€” HuggingFace Endpoint</title>
89
  <style>
90
+ :root{--bg:#0d1117;--card:#161b22;--border:#30363d;--text:#e6edf3;--dim:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149}
91
+ *{box-sizing:border-box;margin:0;padding:0}
92
+ body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;line-height:1.6;padding:20px}
93
+ .container{max-width:800px;margin:0 auto}
94
+ .hero{text-align:center;padding:40px 20px;border-bottom:1px solid var(--border);margin-bottom:24px}
95
+ .hero h1{font-size:2.5rem}.hero h1 span{color:var(--accent)}
96
+ .hero p{color:var(--dim);margin-top:8px}
97
+ .hero .badge{display:inline-block;margin-top:12px;padding:4px 16px;border-radius:999px;background:rgba(255,152,0,0.15);color:#ff9800;font-size:0.85rem}
98
+ .card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;margin-bottom:16px}
99
+ .card h2{font-size:1.2rem;margin-bottom:16px}
100
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
101
+ .box{text-align:center;padding:16px;background:var(--bg);border:1px solid var(--border);border-radius:8px}
102
+ .box .num{font-size:2rem;font-weight:700;color:var(--accent)}
103
+ .box .label{font-size:0.82rem;color:var(--dim);margin-top:4px}
104
+ .list{list-style:none}
105
+ .list li{display:flex;align-items:center;gap:8px;padding:12px;margin-bottom:8px;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-family:'SF Mono','Fira Code',monospace;font-size:0.85rem;word-break:break-all}
106
+ .m{display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.75rem;font-weight:700;flex-shrink:0}
107
+ .m.g{background:rgba(88,166,255,0.15);color:var(--accent)}
108
+ .m.p{background:rgba(63,185,80,0.15);color:var(--green)}
109
+ table{width:100%;border-collapse:collapse}
110
+ th{text-align:left;padding:10px 12px;border-bottom:2px solid var(--border);color:var(--dim);font-size:0.82rem;text-transform:uppercase;letter-spacing:0.05em}
111
+ td{padding:10px 12px;border-bottom:1px solid var(--border);font-size:0.88rem;font-family:'SF Mono',monospace}
112
+ tr:hover td{background:rgba(255,255,255,0.03)}
113
+ .footer{text-align:center;padding:24px;color:var(--dim);font-size:0.85rem}
114
+ .footer a{color:var(--accent);text-decoration:none}
115
+ .tip{background:rgba(88,166,255,0.08);border:1px solid rgba(88,166,255,0.2);border-radius:8px;padding:16px;margin:12px 0;font-size:0.9rem}
116
+ .tip code{background:var(--bg);padding:2px 6px;border-radius:4px;font-size:0.82rem}
117
+ @media(max-width:600px){.hero{padding:24px 12px}.hero h1{font-size:1.8rem}.grid{grid-template-columns:1fr}}
118
+ </style></head>
119
+ <body><div class="container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  <div class="hero">
121
  <h1><span>πŸ”</span> SocialCrypt</h1>
122
+ <p>Encrypted blob store on HuggingFace Spaces</p>
123
+ <div class="badge">πŸ€— HF Spaces</div>
124
  </div>
125
  <div class="card">
126
+ <h2>πŸ“Š Stats</h2>
127
+ <div class="grid">
128
+ <div class="box"><div class="num">${stats.messages}</div><div class="label">Messages</div></div>
129
+ <div class="box"><div class="num">${stats.size_human}</div><div class="label">Total Size</div></div>
130
+ <div class="box"><div class="num">/data</div><div class="label">Persistent Volume</div></div>
 
 
 
 
 
 
 
 
 
131
  </div>
132
  </div>
133
  <div class="card">
134
  <h2>πŸ“‘ Endpoints</h2>
135
+ <ul class="list">
136
+ <li><span class="m g">GET</span><span><strong>/blobs.jsonl</strong> β€” Fetch all blobs (JSONL)</span></li>
137
+ <li><span class="m p">POST</span><span><strong>/submit</strong> β€” Submit a blob <code>{"id":"...","blob":"..."}</code></span></li>
138
+ <li><span class="m g">GET</span><span><strong>/stats</strong> β€” JSON stats</span></li>
139
+ <li><span class="m g">GET</span><span><strong>/health</strong> β€” Health check</span></li>
 
140
  </ul>
 
 
 
 
 
141
  </div>
142
  <div class="card">
143
+ <h2>πŸ“¬ Recent Messages ${hasMessages ? `(${Math.min(stats.messages,50)} of ${stats.messages})` : ''}</h2>
144
+ ${hasMessages ? `<div style="overflow-x:auto;"><table><thead><tr><th>Recipient ID</th><th>Time</th><th>Size</th></tr></thead><tbody>${messagesHtml}</tbody></table></div>` : '<p style="text-align:center;padding:40px;color:var(--dim)">πŸ“­ No messages yet</p>'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  </div>
146
+ <div class="footer"><a href="#">SocialCrypt</a> β€” Encrypted Social Messaging</div>
147
+ </div></body></html>`;
 
148
  }
149
 
150
  // ─── HTTP Server ─────────────────────────────────────────────────────
151
  const server = http.createServer((req, res) => {
 
152
  res.setHeader('Access-Control-Allow-Origin', '*');
153
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, HEAD');
154
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
 
158
  return res.end();
159
  }
160
 
161
+ const pathname = urlMod.parse(req.url, true).pathname;
 
162
 
163
+ // GET / β€” Dashboard
164
  if (req.method === 'GET' && pathname === '/') {
165
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
166
  res.end(renderDashboard());
167
  return;
168
  }
169
 
170
+ // GET /blobs.jsonl β€” Stream blob store
171
  if (req.method === 'GET' && pathname === '/blobs.jsonl') {
172
  const stats = getStats();
173
  res.writeHead(200, {
 
176
  'X-Server': 'SocialCrypt-HF'
177
  });
178
  if (fs.existsSync(BLOBS_FILE)) {
179
+ fs.createReadStream(BLOBS_FILE).on('error', () => res.end()).pipe(res);
 
 
180
  } else {
181
  res.end('');
182
  }
183
  return;
184
  }
185
 
186
+ // POST /submit β€” Append a new blob
187
  if (req.method === 'POST' && pathname === '/submit') {
188
  let body = '';
189
  req.on('data', chunk => (body += chunk));
 
192
  const data = JSON.parse(body);
193
  if (!data.id || !data.blob) {
194
  res.writeHead(400, { 'Content-Type': 'application/json' });
195
+ return res.end(JSON.stringify({ error: 'Missing id or blob' }));
196
  }
197
  if (typeof data.id !== 'string' || typeof data.blob !== 'string') {
198
  res.writeHead(400, { 'Content-Type': 'application/json' });
199
  return res.end(JSON.stringify({ error: 'id and blob must be strings' }));
200
  }
201
+ const line = JSON.stringify({ id: data.id, ts: Date.now(), blob: data.blob }) + '\n';
202
+ if (!fs.existsSync(path.dirname(BLOBS_FILE))) {
203
+ fs.mkdirSync(path.dirname(BLOBS_FILE), { recursive: true });
204
+ }
 
 
 
 
 
 
 
205
  fs.appendFileSync(BLOBS_FILE, line, 'utf-8');
 
206
  const stats = getStats();
207
+ console.log(`πŸ“¨ [${new Date().toISOString()}] Stored for ${data.id.substring(0,16)}... Total: ${stats.messages}`);
 
208
  res.writeHead(201, { 'Content-Type': 'application/json' });
209
  res.end(JSON.stringify({ ok: true, total_messages: stats.messages }));
210
  } catch (e) {
 
215
  return;
216
  }
217
 
218
+ // GET /stats
219
  if (req.method === 'GET' && pathname === '/stats') {
 
220
  res.writeHead(200, { 'Content-Type': 'application/json' });
221
+ res.end(JSON.stringify({ ...getStats(), data_dir: DATA_DIR, uptime_seconds: Math.floor(process.uptime()) }));
 
 
 
 
 
 
222
  return;
223
  }
224
 
225
+ // GET /health
226
  if (req.method === 'GET' && pathname === '/health') {
227
+ let writable = false;
228
+ try { fs.accessSync(path.dirname(BLOBS_FILE), fs.constants.W_OK); writable = true; } catch {}
 
 
 
 
 
229
  res.writeHead(writable ? 200 : 503, { 'Content-Type': 'application/json' });
230
  res.end(JSON.stringify({
231
  status: writable ? 'ok' : 'degraded',
 
237
  return;
238
  }
239
 
240
+ // 404
241
  res.writeHead(404, { 'Content-Type': 'application/json' });
242
+ res.end(JSON.stringify({ error: 'Not found', endpoints: ['/','/blobs.jsonl','/submit','/stats','/health'] }));
 
 
 
 
 
 
 
 
 
243
  });
244
 
245
  // ─── Startup ─────────────────────────────────────────────────────────
246
  server.listen(PORT, '0.0.0.0', () => {
247
+ console.log(`β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”`);
248
+ console.log(`β”‚ πŸ” SocialCrypt β€” HuggingFace Edition β”‚`);
249
+ console.log(`β”‚ ──────────────────────── β”‚`);
250
+ console.log(`β”‚ πŸ“‘ http://0.0.0.0:${String(PORT).padEnd(5)} β”‚`);
251
+ console.log(`β”‚ πŸ’Ύ Volume: ${String(DATA_DIR).padEnd(21)}β”‚`);
252
+ console.log(`β”‚ β”‚`);
253
+ console.log(`β”‚ 🌐 Dashboard: GET / β”‚`);
254
+ console.log(`β”‚ πŸ“– Read: GET /blobs.jsonl β”‚`);
255
+ console.log(`β”‚ ✏️ Write: POST /submit β”‚`);
256
+ console.log(`β”‚ πŸ“Š Stats: GET /stats β”‚`);
257
+ console.log(`β”‚ ❀️ Health: GET /health β”‚`);
258
+ console.log(`β”‚ β”‚`);
259
+ const s = getStats();
260
+ console.log(`β”‚ πŸ“¬ ${String(s.messages).padStart(5)} messages in store β”‚`);
261
+ console.log(`β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜`);
 
 
 
262
  });
263
 
264
  // ─── Graceful Shutdown ──────────────────────────────────────────────
265
  function shutdown(signal) {
266
+ console.log(`\nπŸ›‘ ${signal} received β€” shutting down...`);
267
+ server.close(() => { console.log('βœ… Closed.'); process.exit(0); });
268
+ setTimeout(() => process.exit(1), 5000);
 
 
 
 
 
 
269
  }
 
270
  process.on('SIGTERM', () => shutdown('SIGTERM'));
271
  process.on('SIGINT', () => shutdown('SIGINT'));