hsync-cli 1.1.3 → 1.1.4
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 +0 -0
- package/dist/bin.js +2820 -3048
- package/dist/web/index.html +77 -98
- package/package.json +1 -1
package/dist/web/index.html
CHANGED
|
@@ -714,6 +714,11 @@
|
|
|
714
714
|
</div>
|
|
715
715
|
</div>
|
|
716
716
|
<div class="header-actions">
|
|
717
|
+
<span class="status-badge status-inactive" id="serviceBadge">
|
|
718
|
+
<span class="pulse-dot"></span>
|
|
719
|
+
<span class="badge-text">Service stopped</span>
|
|
720
|
+
</span>
|
|
721
|
+
<button class="btn btn-primary" id="serviceToggleBtn" onclick="toggleService()">Start Sync Service</button>
|
|
717
722
|
<button class="btn btn-secondary btn-icon" onclick="loadDashboardData()" title="Reload Data">
|
|
718
723
|
<!-- Refresh icon -->
|
|
719
724
|
<svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -881,17 +886,20 @@
|
|
|
881
886
|
|
|
882
887
|
<script>
|
|
883
888
|
let syncJobs = [];
|
|
884
|
-
let
|
|
889
|
+
let serviceRunning = false;
|
|
890
|
+
// Log lines up to this one were cleared; the file still has them, so the poll would bring them back
|
|
891
|
+
let clearedThroughLine = null;
|
|
885
892
|
let autostartConfigured = false;
|
|
886
893
|
let pollIntervalId = null;
|
|
887
894
|
let logPollIntervalId = null;
|
|
895
|
+
let lastLogLine = null;
|
|
888
896
|
|
|
889
897
|
// Load everything when the DOM content loads
|
|
890
898
|
document.addEventListener('DOMContentLoaded', () => {
|
|
891
899
|
loadDashboardData();
|
|
892
900
|
|
|
893
901
|
// Setup periodic polling
|
|
894
|
-
pollIntervalId = setInterval(
|
|
902
|
+
pollIntervalId = setInterval(pollServiceState, 3000);
|
|
895
903
|
logPollIntervalId = setInterval(pollLogs, 3000);
|
|
896
904
|
});
|
|
897
905
|
|
|
@@ -909,8 +917,8 @@
|
|
|
909
917
|
// 2. Fetch Autostart status
|
|
910
918
|
await checkAutostartStatus();
|
|
911
919
|
|
|
912
|
-
// 3. Fetch
|
|
913
|
-
await
|
|
920
|
+
// 3. Fetch service status
|
|
921
|
+
await pollServiceState();
|
|
914
922
|
|
|
915
923
|
// 4. Load logs
|
|
916
924
|
await pollLogs();
|
|
@@ -961,47 +969,58 @@
|
|
|
961
969
|
const btn = document.getElementById('autostartToggleBtn');
|
|
962
970
|
btn.disabled = true;
|
|
963
971
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
// but we only have a POST /api/local/autostart/enable endpoint. Let's see if it supports disabling?
|
|
967
|
-
// If the endpoint doesn't support disable, let's notify they can only configure it, or talk to CLI.
|
|
968
|
-
// Wait, the API endpoint is /api/local/autostart/enable. Let's send enable.
|
|
969
|
-
const res = await fetch('/api/local/autostart/enable', { method: 'POST' });
|
|
970
|
-
if (!res.ok) throw new Error('Failed to toggle autostart.');
|
|
971
|
-
} else {
|
|
972
|
-
const res = await fetch('/api/local/autostart/enable', { method: 'POST' });
|
|
973
|
-
if (!res.ok) throw new Error('Failed to enable autostart.');
|
|
974
|
-
}
|
|
972
|
+
const res = await fetch(`/api/local/autostart/${autostartConfigured ? 'disable' : 'enable'}`, { method: 'POST' });
|
|
973
|
+
if (!res.ok) throw new Error('Failed to update autostart.');
|
|
975
974
|
await checkAutostartStatus();
|
|
976
975
|
} catch (err) {
|
|
977
976
|
alert(err.message);
|
|
978
977
|
}
|
|
979
978
|
}
|
|
980
979
|
|
|
981
|
-
async function
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
980
|
+
async function pollServiceState() {
|
|
981
|
+
try {
|
|
982
|
+
const res = await fetch('/api/local/service/status');
|
|
983
|
+
serviceRunning = !!(await res.json()).running;
|
|
984
|
+
} catch {
|
|
985
|
+
serviceRunning = false;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const badge = document.getElementById('serviceBadge');
|
|
989
|
+
badge.className = `status-badge ${serviceRunning ? 'status-active' : 'status-inactive'}`;
|
|
990
|
+
badge.querySelector('.badge-text').textContent = serviceRunning ? 'Service running' : 'Service stopped';
|
|
991
|
+
|
|
992
|
+
const btn = document.getElementById('serviceToggleBtn');
|
|
993
|
+
btn.className = `btn ${serviceRunning ? 'btn-danger' : 'btn-primary'}`;
|
|
994
|
+
btn.innerHTML = serviceRunning ? `<svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"/></svg> Stop Sync Service` : `<svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg> Start Sync Service`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function toggleService() {
|
|
998
|
+
const btn = document.getElementById('serviceToggleBtn');
|
|
999
|
+
btn.disabled = true;
|
|
1000
|
+
try {
|
|
1001
|
+
const res = await fetch(`/api/local/service/${serviceRunning ? 'stop' : 'start'}`, { method: 'POST' });
|
|
1002
|
+
if (!res.ok) throw new Error((await res.json()).error || 'Failed to toggle sync service.');
|
|
1003
|
+
// Give the daemon a moment to write its pid file
|
|
1004
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
alert(err.message);
|
|
1007
|
+
} finally {
|
|
1008
|
+
await pollServiceState();
|
|
1009
|
+
btn.disabled = false;
|
|
993
1010
|
}
|
|
994
|
-
runningStates = states;
|
|
995
|
-
updateUIStates();
|
|
996
1011
|
}
|
|
997
1012
|
|
|
998
1013
|
async function pollLogs() {
|
|
999
1014
|
try {
|
|
1000
1015
|
const res = await fetch('/api/local/logs');
|
|
1001
1016
|
const data = await res.json();
|
|
1002
|
-
|
|
1017
|
+
let logs = data.logs || [];
|
|
1018
|
+
if (clearedThroughLine !== null) {
|
|
1019
|
+
logs = logs.slice(logs.lastIndexOf(clearedThroughLine) + 1);
|
|
1020
|
+
}
|
|
1003
1021
|
|
|
1004
1022
|
const consoleEl = document.getElementById('consoleLogs');
|
|
1023
|
+
lastLogLine = (data.logs || []).at(-1) ?? null;
|
|
1005
1024
|
if (logs.length === 0 || (logs.length === 1 && logs[0].includes('No active logs'))) {
|
|
1006
1025
|
// Keep empty state
|
|
1007
1026
|
return;
|
|
@@ -1051,7 +1070,8 @@
|
|
|
1051
1070
|
}
|
|
1052
1071
|
|
|
1053
1072
|
function clearLogs() {
|
|
1054
|
-
//
|
|
1073
|
+
// Only hides the view; the log file on disk is untouched
|
|
1074
|
+
clearedThroughLine = lastLogLine;
|
|
1055
1075
|
const consoleEl = document.getElementById('consoleLogs');
|
|
1056
1076
|
consoleEl.innerHTML = `
|
|
1057
1077
|
<div class="console-empty">
|
|
@@ -1079,8 +1099,6 @@
|
|
|
1079
1099
|
grid.innerHTML = '';
|
|
1080
1100
|
|
|
1081
1101
|
syncJobs.forEach(job => {
|
|
1082
|
-
const isRunning = !!runningStates[job.id];
|
|
1083
|
-
|
|
1084
1102
|
const card = document.createElement('div');
|
|
1085
1103
|
card.className = 'card';
|
|
1086
1104
|
card.id = `card-${job.id}`;
|
|
@@ -1090,10 +1108,10 @@
|
|
|
1090
1108
|
<div>
|
|
1091
1109
|
<div class="card-header">
|
|
1092
1110
|
<span class="profile-title">${escapeHtml(job.name)}</span>
|
|
1093
|
-
<
|
|
1111
|
+
<button class="status-badge ${job.enabled ? 'status-active' : 'status-inactive'}" style="cursor: pointer;" onclick="toggleEnabled('${escapeJs(job.name)}')" title="${job.enabled ? 'Pause this job in the sync service' : 'Resume this job in the sync service'}">
|
|
1094
1112
|
<span class="pulse-dot"></span>
|
|
1095
|
-
<span class="badge-text">${
|
|
1096
|
-
</
|
|
1113
|
+
<span class="badge-text">${job.enabled ? 'Enabled' : 'Paused'}</span>
|
|
1114
|
+
</button>
|
|
1097
1115
|
</div>
|
|
1098
1116
|
|
|
1099
1117
|
<div class="card-body">
|
|
@@ -1141,16 +1159,8 @@
|
|
|
1141
1159
|
</button>
|
|
1142
1160
|
</div>
|
|
1143
1161
|
|
|
1144
|
-
<button class="btn
|
|
1145
|
-
|
|
1146
|
-
<svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
1147
|
-
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
|
|
1148
|
-
</svg> Stop Daemon
|
|
1149
|
-
` : `
|
|
1150
|
-
<svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
1151
|
-
<polygon points="5 3 19 12 5 21 5 3"/>
|
|
1152
|
-
</svg> Start Daemon
|
|
1153
|
-
`}
|
|
1162
|
+
<button class="btn btn-primary" id="run-btn-${job.id}" onclick="syncNow('${job.id}', '${escapeJs(job.name)}')" title="Run one sync pass now, even if paused">
|
|
1163
|
+
<svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg> Sync now
|
|
1154
1164
|
</button>
|
|
1155
1165
|
</div>
|
|
1156
1166
|
`;
|
|
@@ -1158,57 +1168,30 @@
|
|
|
1158
1168
|
});
|
|
1159
1169
|
}
|
|
1160
1170
|
|
|
1161
|
-
function
|
|
1162
|
-
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
if (toggleBtn) {
|
|
1174
|
-
toggleBtn.className = `btn ${isRunning ? 'btn-danger' : 'btn-primary'}`;
|
|
1175
|
-
toggleBtn.innerHTML = isRunning ? `
|
|
1176
|
-
<svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
1177
|
-
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
|
|
1178
|
-
</svg> Stop Daemon
|
|
1179
|
-
` : `
|
|
1180
|
-
<svg style="width: 12px; height: 12px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
1181
|
-
<polygon points="5 3 19 12 5 21 5 3"/>
|
|
1182
|
-
</svg> Start Daemon
|
|
1183
|
-
`;
|
|
1184
|
-
}
|
|
1185
|
-
});
|
|
1171
|
+
async function toggleEnabled(jobName) {
|
|
1172
|
+
try {
|
|
1173
|
+
const res = await fetch(`/api/local/jobs/${encodeURIComponent(jobName)}/toggle`, { method: 'POST' });
|
|
1174
|
+
if (!res.ok) throw new Error((await res.json()).error || 'Failed to update sync job.');
|
|
1175
|
+
const enabled = (await res.json()).enabled;
|
|
1176
|
+
syncJobs.find(j => j.name === jobName).enabled = enabled;
|
|
1177
|
+
renderProfiles();
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
alert(err.message);
|
|
1180
|
+
}
|
|
1186
1181
|
}
|
|
1187
1182
|
|
|
1188
|
-
async function
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1183
|
+
async function syncNow(jobId, jobName) {
|
|
1184
|
+
const btn = document.getElementById(`run-btn-${jobId}`);
|
|
1185
|
+
btn.disabled = true;
|
|
1186
|
+
btn.textContent = 'Syncing…';
|
|
1192
1187
|
try {
|
|
1193
|
-
const
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
const res = await fetch(`/api/local/jobs/${encodeURIComponent(jobName)}/${endpoint}`, {
|
|
1197
|
-
method: 'POST'
|
|
1198
|
-
});
|
|
1199
|
-
if (!res.ok) {
|
|
1200
|
-
const data = await res.json();
|
|
1201
|
-
throw new Error(data.error || 'Failed to toggle daemon status.');
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Wait a small moment to let system process starting, then refresh
|
|
1205
|
-
setTimeout(async () => {
|
|
1206
|
-
await pollRunningStates();
|
|
1207
|
-
if (toggleBtn) toggleBtn.disabled = false;
|
|
1208
|
-
}, 1000);
|
|
1188
|
+
const res = await fetch(`/api/local/jobs/${encodeURIComponent(jobName)}/run`, { method: 'POST' });
|
|
1189
|
+
if (!res.ok) throw new Error((await res.json()).error || 'Sync run failed.');
|
|
1209
1190
|
} catch (err) {
|
|
1210
1191
|
alert(err.message);
|
|
1211
|
-
|
|
1192
|
+
} finally {
|
|
1193
|
+
await pollLogs();
|
|
1194
|
+
renderProfiles();
|
|
1212
1195
|
}
|
|
1213
1196
|
}
|
|
1214
1197
|
|
|
@@ -1216,11 +1199,6 @@
|
|
|
1216
1199
|
if (!confirm(`Are you sure you want to delete the sync profile "${jobName}"?`)) return;
|
|
1217
1200
|
|
|
1218
1201
|
try {
|
|
1219
|
-
// Stop daemon first if running
|
|
1220
|
-
if (runningStates[jobId]) {
|
|
1221
|
-
await fetch(`/api/local/jobs/${encodeURIComponent(jobName)}/stop`, { method: 'POST' });
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
1202
|
// Filter out of config and save config back
|
|
1225
1203
|
const configRes = await fetch('/api/local/config');
|
|
1226
1204
|
const config = await configRes.json();
|
|
@@ -1386,9 +1364,10 @@
|
|
|
1386
1364
|
.replace(/'/g, ''');
|
|
1387
1365
|
}
|
|
1388
1366
|
|
|
1367
|
+
// For JS string literals inside double-quoted onclick attributes: the browser HTML-decodes before running the JS
|
|
1389
1368
|
function escapeJs(str) {
|
|
1390
1369
|
if (!str) return '';
|
|
1391
|
-
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
1370
|
+
return escapeHtml(str.replace(/\\/g, '\\\\').replace(/'/g, "\\'"));
|
|
1392
1371
|
}
|
|
1393
1372
|
|
|
1394
1373
|
function capitalize(str) {
|