Spaces:
Running
Running
refactor: enhance sync status handling for dashboard compatibility
Browse files- health-server.js +55 -15
- paperclip-sync.py +8 -0
health-server.js
CHANGED
|
@@ -9,7 +9,8 @@ const APP_HOST = "127.0.0.1";
|
|
| 9 |
const startTime = Date.now();
|
| 10 |
const INVITE_URL_FILE = "/tmp/invite-url.txt";
|
| 11 |
const SYNC_STATUS_FILE = "/tmp/sync-status.json";
|
| 12 |
-
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
|
|
|
|
| 13 |
|
| 14 |
function parseRequestUrl(url) {
|
| 15 |
try {
|
|
@@ -22,7 +23,15 @@ function parseRequestUrl(url) {
|
|
| 22 |
function getSyncStatus() {
|
| 23 |
try {
|
| 24 |
if (fs.existsSync(SYNC_STATUS_FILE)) {
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
} catch {}
|
| 28 |
if (process.env.HF_TOKEN) {
|
|
@@ -37,7 +46,9 @@ function getSyncStatus() {
|
|
| 37 |
function getKeepaliveStatus() {
|
| 38 |
try {
|
| 39 |
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE)) {
|
| 40 |
-
return JSON.parse(
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
} catch {}
|
| 43 |
return null;
|
|
@@ -96,7 +107,13 @@ function toneBadge(label, tone = "neutral") {
|
|
| 96 |
return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
|
| 97 |
}
|
| 98 |
|
| 99 |
-
function renderTile({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
return `<article class="tile ${tone}">
|
| 101 |
<div class="tile-head">
|
| 102 |
<span class="tile-title">${escapeHtml(title)}</span>
|
|
@@ -110,12 +127,16 @@ function renderTile({ title, value, detail = "", tone = "neutral", meta = "" })
|
|
| 110 |
|
| 111 |
function renderDashboard(data) {
|
| 112 |
const syncStatus = String(data.sync?.status || "unknown");
|
| 113 |
-
const syncTone = ["success", "restored", "synced", "configured"].includes(
|
|
|
|
|
|
|
| 114 |
? "ok"
|
| 115 |
: syncStatus === "disabled"
|
| 116 |
? "warn"
|
| 117 |
: "neutral";
|
| 118 |
-
const backupDetail = data.sync?.message
|
|
|
|
|
|
|
| 119 |
|
| 120 |
const keepaliveConfigured = data.keepalive?.configured === true;
|
| 121 |
const keepaliveStatus = String(
|
|
@@ -138,7 +159,10 @@ function renderDashboard(data) {
|
|
| 138 |
const tiles = [
|
| 139 |
renderTile({
|
| 140 |
title: "Paperclip Core",
|
| 141 |
-
value: toneBadge(
|
|
|
|
|
|
|
|
|
|
| 142 |
detail: `Backend Port ${APP_PORT}`,
|
| 143 |
tone: data.appReady ? "ok" : "warn",
|
| 144 |
}),
|
|
@@ -162,7 +186,10 @@ function renderDashboard(data) {
|
|
| 162 |
}),
|
| 163 |
renderTile({
|
| 164 |
title: "Keep Awake",
|
| 165 |
-
value: toneBadge(
|
|
|
|
|
|
|
|
|
|
| 166 |
detail: keepAliveDetail,
|
| 167 |
tone: keepAliveTone,
|
| 168 |
}),
|
|
@@ -175,14 +202,14 @@ function renderDashboard(data) {
|
|
| 175 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 176 |
<title>HuggingClip</title>
|
| 177 |
<style>
|
| 178 |
-
:root { color-scheme: dark; --bg:#08080f; --panel:#12111b; --panel2:#151421; --line:#26243a; --text:#f6f4ff; --muted:#7f7a9e; --soft:#b8b3d7; --good:#22c55e; --warn:#f5c542; --bad:#fb7185;
|
| 179 |
* { box-sizing:border-box; }
|
| 180 |
body { margin:0; min-height:100vh; font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); font-size:13px; }
|
| 181 |
main { width:min(720px, calc(100% - 32px)); margin:0 auto; padding:36px 0 44px; }
|
| 182 |
header { text-align:center; margin-bottom:22px; }
|
| 183 |
h1 { margin:0; font-size:1.65rem; line-height:1; letter-spacing:0; }
|
| 184 |
.subtitle { margin-top:12px; color:var(--muted); font-size:.72rem; text-transform:uppercase; letter-spacing:.14em; font-weight:800; }
|
| 185 |
-
.hero-action { display:flex; width:100%; min-height:46px; align-items:center; justify-content:center; border-radius:8px; background:
|
| 186 |
.hero-action:hover { opacity: 0.9; }
|
| 187 |
.invite-banner { background:rgba(245,197,66,.1); border:1px solid rgba(245,197,66,.2); border-radius:8px; padding:12px 16px; margin-bottom:20px; display:flex; flex-direction:column; gap:6px; }
|
| 188 |
.invite-banner span { color:var(--warn); font-weight:850; font-size:.75rem; text-transform:uppercase; }
|
|
@@ -219,11 +246,15 @@ function renderDashboard(data) {
|
|
| 219 |
<h1>🧬 HuggingClip</h1>
|
| 220 |
<div class="subtitle">Paperclip Orchestrator Dashboard</div>
|
| 221 |
</header>
|
| 222 |
-
${
|
|
|
|
|
|
|
| 223 |
<div class="invite-banner">
|
| 224 |
<span>Admin Setup Required</span>
|
| 225 |
<code>${escapeHtml(inviteUrl)}</code>
|
| 226 |
-
</div>`
|
|
|
|
|
|
|
| 227 |
<a class="hero-action" href="/app/" target="_blank" rel="noopener noreferrer">Open Paperclip UI -></a>
|
| 228 |
<section class="overview">
|
| 229 |
${tiles}
|
|
@@ -293,7 +324,12 @@ const server = http.createServer(async (req, res) => {
|
|
| 293 |
proxyReq.on("error", () => {
|
| 294 |
if (!res.headersSent) {
|
| 295 |
res.writeHead(503, { "Content-Type": "application/json" });
|
| 296 |
-
res.end(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
} else {
|
| 298 |
res.end();
|
| 299 |
}
|
|
@@ -306,7 +342,9 @@ server.on("upgrade", (req, socket, head) => {
|
|
| 306 |
const url = parseRequestUrl(req.url);
|
| 307 |
const proxyPath = url.pathname;
|
| 308 |
const proxySocket = net.connect(APP_PORT, APP_HOST, () => {
|
| 309 |
-
proxySocket.write(
|
|
|
|
|
|
|
| 310 |
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 311 |
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`);
|
| 312 |
}
|
|
@@ -320,5 +358,7 @@ server.on("upgrade", (req, socket, head) => {
|
|
| 320 |
server.timeout = 0;
|
| 321 |
server.keepAliveTimeout = 65000;
|
| 322 |
server.listen(PORT, "0.0.0.0", () =>
|
| 323 |
-
console.log(
|
|
|
|
|
|
|
| 324 |
);
|
|
|
|
| 9 |
const startTime = Date.now();
|
| 10 |
const INVITE_URL_FILE = "/tmp/invite-url.txt";
|
| 11 |
const SYNC_STATUS_FILE = "/tmp/sync-status.json";
|
| 12 |
+
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
|
| 13 |
+
"/tmp/huggingclip-cloudflare-keepalive-status.json";
|
| 14 |
|
| 15 |
function parseRequestUrl(url) {
|
| 16 |
try {
|
|
|
|
| 23 |
function getSyncStatus() {
|
| 24 |
try {
|
| 25 |
if (fs.existsSync(SYNC_STATUS_FILE)) {
|
| 26 |
+
const raw = fs.readFileSync(SYNC_STATUS_FILE, "utf8");
|
| 27 |
+
const parsed = JSON.parse(raw);
|
| 28 |
+
if (!parsed.status && parsed.db_status) parsed.status = parsed.db_status;
|
| 29 |
+
if (!parsed.message) {
|
| 30 |
+
if (parsed.last_error) parsed.message = parsed.last_error;
|
| 31 |
+
else if (parsed.last_sync_time)
|
| 32 |
+
parsed.message = `Last sync: ${parsed.last_sync_time}`;
|
| 33 |
+
}
|
| 34 |
+
return parsed;
|
| 35 |
}
|
| 36 |
} catch {}
|
| 37 |
if (process.env.HF_TOKEN) {
|
|
|
|
| 46 |
function getKeepaliveStatus() {
|
| 47 |
try {
|
| 48 |
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE)) {
|
| 49 |
+
return JSON.parse(
|
| 50 |
+
fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"),
|
| 51 |
+
);
|
| 52 |
}
|
| 53 |
} catch {}
|
| 54 |
return null;
|
|
|
|
| 107 |
return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
|
| 108 |
}
|
| 109 |
|
| 110 |
+
function renderTile({
|
| 111 |
+
title,
|
| 112 |
+
value,
|
| 113 |
+
detail = "",
|
| 114 |
+
tone = "neutral",
|
| 115 |
+
meta = "",
|
| 116 |
+
}) {
|
| 117 |
return `<article class="tile ${tone}">
|
| 118 |
<div class="tile-head">
|
| 119 |
<span class="tile-title">${escapeHtml(title)}</span>
|
|
|
|
| 127 |
|
| 128 |
function renderDashboard(data) {
|
| 129 |
const syncStatus = String(data.sync?.status || "unknown");
|
| 130 |
+
const syncTone = ["success", "restored", "synced", "configured"].includes(
|
| 131 |
+
syncStatus,
|
| 132 |
+
)
|
| 133 |
? "ok"
|
| 134 |
: syncStatus === "disabled"
|
| 135 |
? "warn"
|
| 136 |
: "neutral";
|
| 137 |
+
const backupDetail = data.sync?.message
|
| 138 |
+
? escapeHtml(data.sync.message)
|
| 139 |
+
: "No status yet";
|
| 140 |
|
| 141 |
const keepaliveConfigured = data.keepalive?.configured === true;
|
| 142 |
const keepaliveStatus = String(
|
|
|
|
| 159 |
const tiles = [
|
| 160 |
renderTile({
|
| 161 |
title: "Paperclip Core",
|
| 162 |
+
value: toneBadge(
|
| 163 |
+
data.appReady ? "Online" : "Booting",
|
| 164 |
+
data.appReady ? "ok" : "warn",
|
| 165 |
+
),
|
| 166 |
detail: `Backend Port ${APP_PORT}`,
|
| 167 |
tone: data.appReady ? "ok" : "warn",
|
| 168 |
}),
|
|
|
|
| 186 |
}),
|
| 187 |
renderTile({
|
| 188 |
title: "Keep Awake",
|
| 189 |
+
value: toneBadge(
|
| 190 |
+
keepaliveConfigured ? "CF Cron" : keepaliveStatus.toUpperCase(),
|
| 191 |
+
keepAliveTone,
|
| 192 |
+
),
|
| 193 |
detail: keepAliveDetail,
|
| 194 |
tone: keepAliveTone,
|
| 195 |
}),
|
|
|
|
| 202 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 203 |
<title>HuggingClip</title>
|
| 204 |
<style>
|
| 205 |
+
:root { color-scheme: dark; --bg:#08080f; --panel:#12111b; --panel2:#151421; --line:#26243a; --text:#f6f4ff; --muted:#7f7a9e; --soft:#b8b3d7; --good:#22c55e; --warn:#f5c542; --bad:#fb7185;}
|
| 206 |
* { box-sizing:border-box; }
|
| 207 |
body { margin:0; min-height:100vh; font-family:Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); font-size:13px; }
|
| 208 |
main { width:min(720px, calc(100% - 32px)); margin:0 auto; padding:36px 0 44px; }
|
| 209 |
header { text-align:center; margin-bottom:22px; }
|
| 210 |
h1 { margin:0; font-size:1.65rem; line-height:1; letter-spacing:0; }
|
| 211 |
.subtitle { margin-top:12px; color:var(--muted); font-size:.72rem; text-transform:uppercase; letter-spacing:.14em; font-weight:800; }
|
| 212 |
+
.hero-action { display:flex; width:100%; min-height:46px; align-items:center; justify-content:center; border-radius:8px; background:#fff; color:#000; text-decoration:none; font-weight:850; font-size:.98rem; margin:24px 0 20px; transition: opacity 0.15s ease; }
|
| 213 |
.hero-action:hover { opacity: 0.9; }
|
| 214 |
.invite-banner { background:rgba(245,197,66,.1); border:1px solid rgba(245,197,66,.2); border-radius:8px; padding:12px 16px; margin-bottom:20px; display:flex; flex-direction:column; gap:6px; }
|
| 215 |
.invite-banner span { color:var(--warn); font-weight:850; font-size:.75rem; text-transform:uppercase; }
|
|
|
|
| 246 |
<h1>🧬 HuggingClip</h1>
|
| 247 |
<div class="subtitle">Paperclip Orchestrator Dashboard</div>
|
| 248 |
</header>
|
| 249 |
+
${
|
| 250 |
+
inviteUrl
|
| 251 |
+
? `
|
| 252 |
<div class="invite-banner">
|
| 253 |
<span>Admin Setup Required</span>
|
| 254 |
<code>${escapeHtml(inviteUrl)}</code>
|
| 255 |
+
</div>`
|
| 256 |
+
: ""
|
| 257 |
+
}
|
| 258 |
<a class="hero-action" href="/app/" target="_blank" rel="noopener noreferrer">Open Paperclip UI -></a>
|
| 259 |
<section class="overview">
|
| 260 |
${tiles}
|
|
|
|
| 324 |
proxyReq.on("error", () => {
|
| 325 |
if (!res.headersSent) {
|
| 326 |
res.writeHead(503, { "Content-Type": "application/json" });
|
| 327 |
+
res.end(
|
| 328 |
+
JSON.stringify({
|
| 329 |
+
status: "starting",
|
| 330 |
+
message: "Paperclip is booting...",
|
| 331 |
+
}),
|
| 332 |
+
);
|
| 333 |
} else {
|
| 334 |
res.end();
|
| 335 |
}
|
|
|
|
| 342 |
const url = parseRequestUrl(req.url);
|
| 343 |
const proxyPath = url.pathname;
|
| 344 |
const proxySocket = net.connect(APP_PORT, APP_HOST, () => {
|
| 345 |
+
proxySocket.write(
|
| 346 |
+
`${req.method} ${proxyPath}${url.search} HTTP/${req.httpVersion}\r\n`,
|
| 347 |
+
);
|
| 348 |
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 349 |
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`);
|
| 350 |
}
|
|
|
|
| 358 |
server.timeout = 0;
|
| 359 |
server.keepAliveTimeout = 65000;
|
| 360 |
server.listen(PORT, "0.0.0.0", () =>
|
| 361 |
+
console.log(
|
| 362 |
+
`🧬 HuggingClip Dashboard on ${PORT} -> Paperclip on ${APP_PORT}`,
|
| 363 |
+
),
|
| 364 |
);
|
paperclip-sync.py
CHANGED
|
@@ -101,6 +101,14 @@ def parse_db_url(db_url: str) -> dict:
|
|
| 101 |
def write_status(status: dict):
|
| 102 |
"""Write sync status to file for dashboard"""
|
| 103 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
STATUS_FILE.write_text(json.dumps(status, indent=2))
|
| 105 |
except Exception as e:
|
| 106 |
logger.error(f'Failed to write status file: {e}')
|
|
|
|
| 101 |
def write_status(status: dict):
|
| 102 |
"""Write sync status to file for dashboard"""
|
| 103 |
try:
|
| 104 |
+
# Ensure compatibility with dashboard fields: `status` and `message`
|
| 105 |
+
try:
|
| 106 |
+
if 'db_status' in status and 'status' not in status:
|
| 107 |
+
status['status'] = status['db_status']
|
| 108 |
+
if 'last_error' in status and 'message' not in status:
|
| 109 |
+
status['message'] = status['last_error']
|
| 110 |
+
except Exception:
|
| 111 |
+
pass
|
| 112 |
STATUS_FILE.write_text(json.dumps(status, indent=2))
|
| 113 |
except Exception as e:
|
| 114 |
logger.error(f'Failed to write status file: {e}')
|