instbyte 1.7.0 → 1.9.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instbyte",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "A self-hosted LAN sharing utility for fast, frictionless file, link, and snippet exchange across devices — no cloud required.",
5
5
  "main": "server/server.js",
6
6
  "bin": {
package/server/cleanup.js CHANGED
@@ -3,13 +3,12 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const config = require("./config");
5
5
 
6
- const DAY = config.storage.retention;
7
-
8
6
  setInterval(() => {
9
- const cutoff = Date.now() - DAY;
7
+ if (config.storage.retention === null) return;
8
+ const cutoff = Date.now() - config.storage.retention;
10
9
 
11
10
  db.all(
12
- `SELECT * FROM items WHERE created_at < ?`,
11
+ `SELECT * FROM items WHERE created_at < ? AND pinned = 0`,
13
12
  [cutoff],
14
13
  (err, rows) => {
15
14
  rows.forEach((item) => {
package/server/config.js CHANGED
@@ -30,6 +30,7 @@ function parseFileSize(val) {
30
30
  }
31
31
 
32
32
  function parseRetention(val) {
33
+ if (String(val).toLowerCase() === "never") return null;
33
34
  if (typeof val === "number") return val;
34
35
  const units = { h: 3600000, d: 86400000 };
35
36
  const match = String(val).match(/^(\d+)(h|d)$/i);
package/server/db.js CHANGED
@@ -36,6 +36,9 @@ db.serialize(() => {
36
36
  db.run(`ALTER TABLE items ADD COLUMN size INTEGER DEFAULT 0`, () => { });
37
37
  db.run(`ALTER TABLE channels ADD COLUMN pinned INTEGER DEFAULT 0`, () => { });
38
38
 
39
+ db.run(`ALTER TABLE items ADD COLUMN title TEXT DEFAULT ''`, () => { });
40
+ db.run(`ALTER TABLE items ADD COLUMN edited_at INTEGER DEFAULT NULL`, () => { });
41
+
39
42
  // Insert default channels if empty
40
43
  db.get("SELECT COUNT(*) as count FROM channels", (err, row) => {
41
44
  if (err) {
package/server/server.js CHANGED
@@ -366,16 +366,47 @@ app.post("/pin/:id", (req, res) => {
366
366
  /* GET ITEMS */
367
367
  app.get("/items/:channel", (req, res) => {
368
368
  const channel = req.params.channel;
369
+ const page = parseInt(req.query.page) || 1;
370
+ const limit = 10;
371
+ const offset = (page - 1) * limit;
369
372
 
370
- db.all(
371
- `SELECT * FROM items WHERE channel=? ORDER BY pinned DESC, created_at DESC`,
373
+ db.get(
374
+ `SELECT COUNT(*) as count FROM items WHERE channel=? AND pinned=0`,
372
375
  [channel],
373
- (err, rows) => {
374
- res.json(rows);
376
+ (err, row) => {
377
+ if (err) return res.status(500).json({ error: "DB error" });
378
+
379
+ const totalUnpinned = row.count;
380
+ const hasMore = offset + limit < totalUnpinned;
381
+
382
+ db.all(
383
+ `SELECT * FROM items WHERE channel=? AND pinned=0
384
+ ORDER BY created_at DESC LIMIT ? OFFSET ?`,
385
+ [channel, limit, offset],
386
+ (err, unpinned) => {
387
+ if (err) return res.status(500).json({ error: "DB error" });
388
+
389
+ if (page === 1) {
390
+ // only fetch pinned on first page
391
+ db.all(
392
+ `SELECT * FROM items WHERE channel=? AND pinned=1
393
+ ORDER BY created_at DESC`,
394
+ [channel],
395
+ (err, pinned) => {
396
+ if (err) return res.status(500).json({ error: "DB error" });
397
+ res.json({ items: [...pinned, ...unpinned], hasMore, page });
398
+ }
399
+ );
400
+ } else {
401
+ res.json({ items: unpinned, hasMore, page });
402
+ }
403
+ }
404
+ );
375
405
  }
376
406
  );
377
407
  });
378
408
 
409
+
379
410
  /* SEARCH */
380
411
  app.get("/search/:channel/:q", (req, res) => {
381
412
  const { channel, q } = req.params;
@@ -494,6 +525,45 @@ app.patch("/item/:id/move", (req, res) => {
494
525
  });
495
526
  });
496
527
 
528
+ /* UPDATE ITEM TITLE */
529
+ app.patch("/item/:id/title", (req, res) => {
530
+ const { id } = req.params;
531
+ const { title } = req.body;
532
+
533
+ if (title === undefined) return res.status(400).json({ error: "Title required" });
534
+
535
+ db.run(
536
+ "UPDATE items SET title=? WHERE id=?",
537
+ [title.trim(), id],
538
+ function (err) {
539
+ if (err) return res.status(500).json({ error: "Update failed" });
540
+ if (this.changes === 0) return res.status(404).json({ error: "Item not found" });
541
+ io.emit("item-updated", { id: parseInt(id), title: title.trim() });
542
+ res.json({ id, title: title.trim() });
543
+ }
544
+ );
545
+ });
546
+
547
+ /* UPDATE ITEM CONTENT */
548
+ app.patch("/item/:id/content", (req, res) => {
549
+ const { id } = req.params;
550
+ const { content } = req.body;
551
+
552
+ if (content === undefined) return res.status(400).json({ error: "Content required" });
553
+ if (content.trim() === "") return res.status(400).json({ error: "Content cannot be empty" });
554
+
555
+ db.run(
556
+ "UPDATE items SET content=?, edited_at=? WHERE id=? AND type='text'",
557
+ [content.trim(), Date.now(), id],
558
+ function (err) {
559
+ if (err) return res.status(500).json({ error: "Update failed" });
560
+ if (this.changes === 0) return res.status(404).json({ error: "Item not found or not editable" });
561
+ io.emit("item-updated", { id: parseInt(id), content: content.trim(), edited_at: Date.now() });
562
+ res.json({ id, content: content.trim() });
563
+ }
564
+ );
565
+ });
566
+
497
567
  /* RENAME CHANNEL */
498
568
  app.patch("/channels/:name", (req, res) => {
499
569
  const oldName = req.params.name;
@@ -542,7 +612,8 @@ app.post("/channels/:name/pin", (req, res) => {
542
612
  app.get("/info", (req, res) => {
543
613
  res.json({
544
614
  url: `http://${localIP}:${PORT}`,
545
- hasAuth: !!config.auth.passphrase
615
+ hasAuth: !!config.auth.passphrase,
616
+ retention: config.storage.retention // null means "never"
546
617
  });
547
618
  });
548
619
 
@@ -619,11 +690,15 @@ app.get("/logo-dynamic.png", (req, res) => {
619
690
  /* ============================
620
691
  SOCKET CONNECTION LOGGING
621
692
  ============================ */
693
+ // in-memory seen tracking — item id → Set of socket ids
694
+ // resets on server restart, no DB needed
695
+ const seenBy = new Map();
622
696
 
623
697
  let connectedUsers = 0;
624
698
 
625
699
  io.on("connection", (socket) => {
626
700
  connectedUsers++;
701
+ io.emit("user-count", connectedUsers);
627
702
 
628
703
  let username = "Unknown";
629
704
 
@@ -632,9 +707,19 @@ io.on("connection", (socket) => {
632
707
  console.log(username + " connected | total:", connectedUsers);
633
708
  });
634
709
 
710
+ socket.on("seen", ({ id, name }) => {
711
+ if (!id || !name) return;
712
+ if (!seenBy.has(id)) seenBy.set(id, new Set());
713
+ seenBy.get(id).add(name); // name instead of socket.id
714
+ const count = seenBy.get(id).size;
715
+ console.log(`seen: item ${id} | count: ${count}`);
716
+ io.emit("seen-update", { id, count });
717
+ });
718
+
635
719
  socket.on("disconnect", () => {
636
720
  connectedUsers--;
637
721
  console.log(username + " disconnected | total:", connectedUsers);
722
+ io.emit("user-count", connectedUsers);
638
723
  });
639
724
  });
640
725
 
@@ -696,3 +781,33 @@ findFreePort(PREFERRED).then(p => {
696
781
  console.log("");
697
782
  });
698
783
  });
784
+
785
+
786
+ // ========================
787
+ // GRACEFUL SHUTDOWN
788
+ // ========================
789
+ function shutdown(signal) {
790
+ console.log(`\n${signal} received — shutting down gracefully...`);
791
+
792
+ // stop accepting new connections
793
+ server.close(() => {
794
+ console.log("HTTP server closed");
795
+
796
+ // close database connection
797
+ db.close((err) => {
798
+ if (err) console.error("Error closing database:", err);
799
+ else console.log("Database connection closed");
800
+ console.log("Shutdown complete");
801
+ process.exit(0);
802
+ });
803
+ });
804
+
805
+ // force exit after 10 seconds if something hangs
806
+ setTimeout(() => {
807
+ console.error("Forced shutdown after timeout");
808
+ process.exit(1);
809
+ }, 10000);
810
+ }
811
+
812
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
813
+ process.on("SIGINT", () => shutdown("SIGINT"));