@fudrouter/fsrouter 0.6.98 → 0.6.100

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 (47) hide show
  1. package/dist/routes/db/route.js +141 -97
  2. package/dist/server.js +2 -1
  3. package/package.json +1 -1
  4. package/public/.vite/manifest.json +90 -90
  5. package/public/assets/EndpointPageClient.B1pL25ws.js +6 -0
  6. package/public/assets/Loading.C1s7rolj.js +1 -0
  7. package/public/assets/NoAuthProxyCard.CrFQ6U88.js +89 -0
  8. package/public/assets/index.2a3mqmx-.js +89 -0
  9. package/public/assets/index.Blvwx0HS.css +1 -0
  10. package/public/assets/page.1w2ClxJa.js +64 -0
  11. package/public/assets/page.B1WuiJNL.js +1 -0
  12. package/public/assets/page.B5pzcN8j.js +1 -0
  13. package/public/assets/page.B6HgqxIh.js +1 -0
  14. package/public/assets/page.B9nZj2bM.js +1 -0
  15. package/public/assets/page.BAjMOHLc.js +6 -0
  16. package/public/assets/page.BBAy2gwn.js +1 -0
  17. package/public/assets/page.BFCJJrr_.js +1 -0
  18. package/public/assets/page.BH4eYGdz.js +1 -0
  19. package/public/assets/page.BMlo83oM.js +1 -0
  20. package/public/assets/page.BPMtBhKd.js +1 -0
  21. package/public/assets/page.BWmMDNX0.js +5 -0
  22. package/public/assets/page.B_rBjC41.js +1 -0
  23. package/public/assets/page.Bj0MFcSW.js +1 -0
  24. package/public/assets/page.Bs99xyvS.js +23 -0
  25. package/public/assets/page.BuU3QCO4.js +1 -0
  26. package/public/assets/page.C-4fur3O.js +4 -0
  27. package/public/assets/page.C0iX9Fnf.js +1 -0
  28. package/public/assets/page.C0qR8Y5R.js +1 -0
  29. package/public/assets/page.CAAgcGqN.js +1 -0
  30. package/public/assets/page.CCTU0LkA.js +1 -0
  31. package/public/assets/page.CS8il6ZP.js +2 -0
  32. package/public/assets/page.CTDNnugg.js +1 -0
  33. package/public/assets/page.CaujBCOC.js +1 -0
  34. package/public/assets/page.CcYMJEkz.js +2 -0
  35. package/public/assets/page.CfKwOBap.js +4 -0
  36. package/public/assets/page.Ct47P4hB.js +1 -0
  37. package/public/assets/page.CtZhMjzD.js +31 -0
  38. package/public/assets/page.D6H2uDVR.js +1 -0
  39. package/public/assets/page.D7rC6efy.js +1 -0
  40. package/public/assets/page.DCY06u9A.js +5 -0
  41. package/public/assets/page.DFzXiFFW.js +1 -0
  42. package/public/assets/page.DOgtsHmp.js +1 -0
  43. package/public/assets/page.DiFYTx8K.js +1 -0
  44. package/public/assets/page.Dm8AHdlz.js +1 -0
  45. package/public/assets/page.QIyytaMO.js +1 -0
  46. package/public/assets/page.jbj6FFm0.js +6 -0
  47. package/public/index.html +4 -4
@@ -55,117 +55,161 @@ export async function GET(req, res) {
55
55
  return res.status(500).json({ error: "Failed to generate backup: " + error.message });
56
56
  }
57
57
  }
58
- // POST /api/db - Restore database & secrets
58
+ // ─── SSE helpers for streaming restore progress ───────────────────────────────
59
+ function sseInit(res) {
60
+ res.writeHead(200, {
61
+ "Content-Type": "text/event-stream",
62
+ "Cache-Control": "no-cache, no-transform",
63
+ "Connection": "keep-alive",
64
+ "X-Accel-Buffering": "no"
65
+ });
66
+ }
67
+ function sseSend(res, event, data) {
68
+ res.write(`event: ${event}\n`);
69
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
70
+ }
71
+ // POST /api/db - Restore database & secrets (streaming progress)
59
72
  export async function POST_handler(req, res) {
60
- try {
61
- const body = req.body;
62
- if (!body || body.signature !== "FUDROUTER_BACKUP") {
63
- return res.status(400).json({ error: "Invalid backup file signature." });
64
- }
65
- // Prefer logical import: raw SQLite replacement can lose rows when WAL/driver
66
- // state differs between machines. Keep raw-file restore below for old backups.
67
- if (body.database) {
68
- await importDb(body.database);
69
- // Flush WAL into the main DB file BEFORE we remove -wal/-shm below,
70
- // otherwise the just-imported rows live only in the WAL and get deleted.
71
- const db = await getAdapter();
72
- if (db && typeof db.checkpoint === "function") {
73
- try {
74
- db.checkpoint();
73
+ // Switch to SSE immediately so the client sees progress from the start.
74
+ sseInit(res);
75
+ const log = (msg, level = "info") => sseSend(res, "log", { message: msg, level, ts: new Date().toISOString() });
76
+ const progress = (percent, label) => {
77
+ sseSend(res, "progress", { percent, label });
78
+ log(label);
79
+ };
80
+ // Run restore asynchronously; don't await the whole thing on the request.
81
+ (async () => {
82
+ try {
83
+ const body = req.body;
84
+ progress(5, "Memvalidasi file backup...");
85
+ if (!body || body.signature !== "FUDROUTER_BACKUP") {
86
+ sseSend(res, "error", { message: "Invalid backup file signature." });
87
+ return res.end();
88
+ }
89
+ // Prefer logical import: raw SQLite replacement can lose rows when WAL/driver
90
+ // state differs between machines. Keep raw-file restore below for old backups.
91
+ const hasLogical = !!body.database;
92
+ if (hasLogical) {
93
+ progress(15, "Menutup koneksi database aktif...");
94
+ const db0 = await getAdapter();
95
+ if (db0 && typeof db0.close === "function") {
96
+ try {
97
+ db0.close();
98
+ }
99
+ catch { /* ignore */ }
75
100
  }
76
- catch (e) {
77
- console.warn("[Restore] checkpoint failed:", e.message);
101
+ progress(25, "Mengimpor data logical (provider, connections, proxy, api keys, combos)...");
102
+ await importDb(body.database);
103
+ progress(40, "Flush WAL ke file database utama (checkpoint)...");
104
+ const db = await getAdapter();
105
+ if (db && typeof db.checkpoint === "function") {
106
+ try {
107
+ db.checkpoint();
108
+ }
109
+ catch (e) {
110
+ log(`checkpoint gagal: ${e.message}`, "warn");
111
+ }
112
+ }
113
+ if (db && typeof db.exec === "function") {
114
+ try {
115
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
116
+ }
117
+ catch { /* ignore */ }
78
118
  }
79
119
  }
80
- if (db && typeof db.exec === "function") {
81
- try {
82
- db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
120
+ else {
121
+ progress(15, "Menutup koneksi database aktif (raw restore)...");
122
+ const db = await getAdapter();
123
+ if (db && typeof db.close === "function") {
124
+ try {
125
+ db.close();
126
+ }
127
+ catch { /* ignore */ }
83
128
  }
84
- catch { /* ignore */ }
85
129
  }
86
- }
87
- else {
88
- const db = await getAdapter();
89
- if (db && typeof db.close === "function")
90
- db.close();
91
- }
92
- // 2. Remove active -wal and -shm files to prevent corruption/mismatch.
93
- // On Windows these files can be locked by the OS/AV/SQLite even after
94
- // db.close() (EBUSY). Use a retry + rename fallback so restore never
95
- // hard-fails on a transient lock.
96
- const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
97
- const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
98
- const safeUnlink = (file) => {
99
- if (!fs.existsSync(file))
100
- return;
101
- // Give the DB a moment to fully release the file handle.
102
- const attempts = process.platform === "win32" ? 12 : 3;
103
- for (let i = 0; i < attempts; i++) {
104
- try {
105
- fs.unlinkSync(file);
130
+ // 2. Remove active -wal and -shm files to prevent corruption/mismatch.
131
+ progress(50, "Membersihkan file WAL/SHM lock...");
132
+ const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
133
+ const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
134
+ const safeUnlink = (file) => {
135
+ if (!fs.existsSync(file))
106
136
  return;
107
- }
108
- catch (e) {
109
- if (e.code === "EBUSY" || e.code === "EPERM" || e.code === "ETXTBSY") {
110
- // Fallback: rename out of the way (atomic on NTFS) then retry unlink.
111
- try {
112
- const tmp = `${file}.old-${Date.now()}`;
113
- fs.renameSync(file, tmp);
137
+ const attempts = process.platform === "win32" ? 12 : 3;
138
+ for (let i = 0; i < attempts; i++) {
139
+ try {
140
+ fs.unlinkSync(file);
141
+ return;
142
+ }
143
+ catch (e) {
144
+ if (e.code === "EBUSY" || e.code === "EPERM" || e.code === "ETXTBSY") {
114
145
  try {
115
- fs.unlinkSync(tmp);
146
+ const tmp = `${file}.old-${Date.now()}`;
147
+ fs.renameSync(file, tmp);
148
+ try {
149
+ fs.unlinkSync(tmp);
150
+ }
151
+ catch { /* ignore */ }
152
+ return;
116
153
  }
117
- catch { /* ignore */ }
118
- return;
119
- }
120
- catch {
121
- // Wait and retry the lock may be released shortly.
122
- try {
123
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
154
+ catch {
155
+ try {
156
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
157
+ }
158
+ catch { /* noop */ }
159
+ continue;
124
160
  }
125
- catch { /* noop */ }
126
- continue;
127
161
  }
162
+ throw e;
128
163
  }
129
- throw e;
130
164
  }
131
- }
132
- // Last resort: leave the file; SQLite rebuilds -wal/-shm on next open.
133
- console.warn(`[Restore] Could not remove ${file} (locked) — continuing.`);
134
- };
135
- safeUnlink(walFile);
136
- safeUnlink(shmFile);
137
- // 3. Restore secrets/raw DB only for legacy backups. Do not overwrite the
138
- // logical import with a stale/incompatible SQLite file.
139
- const filesToRestore = [
140
- { key: "db/data.sqlite", path: path.join(DATA_DIR, "db", "data.sqlite") },
141
- { key: "machine-id", path: path.join(DATA_DIR, "machine-id") },
142
- { key: "jwt-secret", path: path.join(DATA_DIR, "jwt-secret") },
143
- { key: "auth/cli-secret", path: path.join(DATA_DIR, "auth", "cli-secret") }
144
- ];
145
- for (const item of filesToRestore) {
146
- if (body.database && item.key === "db/data.sqlite")
147
- continue;
148
- const b64Data = body.files?.[item.key];
149
- if (b64Data) {
150
- // Ensure directory exists
151
- const dir = path.dirname(item.path);
152
- if (!fs.existsSync(dir)) {
153
- fs.mkdirSync(dir, { recursive: true });
165
+ log(`Tidak bisa menghapus ${file} (terkunci) — melanjutkan.`, "warn");
166
+ };
167
+ safeUnlink(walFile);
168
+ safeUnlink(shmFile);
169
+ // 3. Restore secrets/raw DB only for legacy backups. Do not overwrite the
170
+ // logical import with a stale/incompatible SQLite file.
171
+ const filesToRestore = [
172
+ { key: "db/data.sqlite", path: path.join(DATA_DIR, "db", "data.sqlite") },
173
+ { key: "machine-id", path: path.join(DATA_DIR, "machine-id") },
174
+ { key: "jwt-secret", path: path.join(DATA_DIR, "jwt-secret") },
175
+ { key: "auth/cli-secret", path: path.join(DATA_DIR, "auth", "cli-secret") }
176
+ ];
177
+ let idx = 0;
178
+ const total = filesToRestore.length;
179
+ for (const item of filesToRestore) {
180
+ if (hasLogical && item.key === "db/data.sqlite") {
181
+ idx++;
182
+ continue; // logical import already applied the DB
154
183
  }
155
- // Write file
156
- fs.writeFileSync(item.path, Buffer.from(b64Data, "base64"));
157
- console.log(`[Restore] Restored: ${item.key}`);
184
+ const b64Data = body.files?.[item.key];
185
+ const pct = 60 + Math.round((idx / total) * 35);
186
+ if (b64Data) {
187
+ progress(pct, `Menimpa file: ${item.key} ...`);
188
+ const dir = path.dirname(item.path);
189
+ if (!fs.existsSync(dir))
190
+ fs.mkdirSync(dir, { recursive: true });
191
+ fs.writeFileSync(item.path, Buffer.from(b64Data, "base64"));
192
+ log(`✓ Di-timpa: ${item.key}`, "info");
193
+ }
194
+ else {
195
+ log(`Lewati (tidak ada di backup): ${item.key}`, "warn");
196
+ }
197
+ idx++;
158
198
  }
199
+ progress(100, "Restore selesai. Server akan merestart...");
200
+ sseSend(res, "done", { success: true, message: "Restore berhasil. Server merestart..." });
201
+ // 4. Shut down process so PM2 restarts it (applies restored data cleanly)
202
+ setTimeout(() => {
203
+ try {
204
+ process.exit(0);
205
+ }
206
+ catch { /* ignore */ }
207
+ }, 1200);
159
208
  }
160
- // 4. Send success response and shut down process so PM2 restarts it
161
- res.json({ success: true, message: "Restore successful. Server is restarting..." });
162
- console.log("[Restore] Successful. Exiting process for restart...");
163
- setTimeout(() => {
164
- process.exit(0);
165
- }, 1500);
166
- }
167
- catch (error) {
168
- console.error("Restore error:", error);
169
- return res.status(500).json({ error: "Failed to restore backup: " + error.message });
170
- }
209
+ catch (error) {
210
+ console.error("Restore error:", error);
211
+ sseSend(res, "error", { message: "Restore gagal: " + (error?.message || error) });
212
+ res.end();
213
+ }
214
+ })();
171
215
  }
package/dist/server.js CHANGED
@@ -35,8 +35,9 @@ SPA_ROUTES.forEach((route) => {
35
35
  });
36
36
  });
37
37
  // ─── Health Check (no auth) ────────────────────────────────────────────────────
38
+ import { getAppVersion } from "./lib/db/version.js";
38
39
  app.get("/api/health", (_req, res) => {
39
- res.json({ status: "ok", version: "0.6.11", ts: Date.now() });
40
+ res.json({ status: "ok", version: getAppVersion(), ts: Date.now() });
40
41
  });
41
42
  // ─── Auth Middleware ───────────────────────────────────────────────────────────
42
43
  app.use(authMiddleware);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fudrouter/fsrouter",
3
- "version": "0.6.98",
3
+ "version": "0.6.100",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "9Router v2 — Express Backend (API, SSE, DB, Auth, MITM)",