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.
@@ -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 runningStates = {};
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(pollRunningStates, 3000);
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 running statuses
913
- await pollRunningStates();
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
- if (autostartConfigured) {
965
- // Note: In the existing CLI logic, autostart is disabled by removing files,
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 pollRunningStates() {
982
- if (syncJobs.length === 0) return;
983
-
984
- const states = {};
985
- for (const job of syncJobs) {
986
- try {
987
- const res = await fetch(`/api/local/jobs/${encodeURIComponent(job.name)}/status`);
988
- const data = await res.json();
989
- states[job.id] = !!data.running;
990
- } catch {
991
- states[job.id] = false;
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
- const logs = data.logs || [];
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
- // In web dashboard, we can just clear the local DOM view (polling will refill on next run if still on disk)
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
- <span class="status-badge ${isRunning ? 'status-active' : 'status-inactive'}" id="badge-${job.id}">
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">${isRunning ? 'Active' : 'Stopped'}</span>
1096
- </span>
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 ${isRunning ? 'btn-danger' : 'btn-primary'}" id="toggle-btn-${job.id}" onclick="toggleDaemon('${job.id}', '${escapeJs(job.name)}')">
1145
- ${isRunning ? `
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 updateUIStates() {
1162
- syncJobs.forEach(job => {
1163
- const isRunning = !!runningStates[job.id];
1164
-
1165
- const badge = document.getElementById(`badge-${job.id}`);
1166
- const toggleBtn = document.getElementById(`toggle-btn-${job.id}`);
1167
-
1168
- if (badge) {
1169
- badge.className = `status-badge ${isRunning ? 'status-active' : 'status-inactive'}`;
1170
- badge.querySelector('.badge-text').textContent = isRunning ? 'Active' : 'Stopped';
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 toggleDaemon(jobId, jobName) {
1189
- const isRunning = !!runningStates[jobId];
1190
- const endpoint = isRunning ? 'stop' : 'start';
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 toggleBtn = document.getElementById(`toggle-btn-${jobId}`);
1194
- if (toggleBtn) toggleBtn.disabled = true;
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
- await pollRunningStates();
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, '&#039;');
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, "\\'").replace(/"/g, '\\"');
1370
+ return escapeHtml(str.replace(/\\/g, '\\\\').replace(/'/g, "\\'"));
1392
1371
  }
1393
1372
 
1394
1373
  function capitalize(str) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hsync-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "hsync CLI for file sync operations",
5
5
  "bin": {
6
6
  "hsync": "./dist/bin.js"