hsync-cli 1.1.4 → 1.1.6
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/dist/bin.js +2442 -2140
- package/dist/web/index.html +246 -130
- package/package.json +1 -1
package/dist/web/index.html
CHANGED
|
@@ -208,45 +208,45 @@
|
|
|
208
208
|
align-items: center;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
/*
|
|
211
|
+
/* Profile list */
|
|
212
212
|
.grid-layout {
|
|
213
|
-
display:
|
|
214
|
-
|
|
215
|
-
gap:
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: column;
|
|
215
|
+
gap: 8px;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
.card {
|
|
219
219
|
background: var(--surface);
|
|
220
220
|
border: 1px solid var(--surface-border);
|
|
221
221
|
border-radius: var(--radius);
|
|
222
|
-
padding:
|
|
223
|
-
backdrop-filter: blur(12px);
|
|
224
|
-
-webkit-backdrop-filter: blur(12px);
|
|
222
|
+
padding: 12px 16px;
|
|
225
223
|
display: flex;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
transition:
|
|
229
|
-
position: relative;
|
|
230
|
-
overflow: hidden;
|
|
224
|
+
align-items: center;
|
|
225
|
+
gap: 16px;
|
|
226
|
+
transition: border-color 0.2s ease;
|
|
231
227
|
}
|
|
232
228
|
|
|
233
229
|
.card:hover {
|
|
234
|
-
transform: translateY(-2px);
|
|
235
230
|
border-color: rgba(255, 255, 255, 0.12);
|
|
236
231
|
}
|
|
237
232
|
|
|
238
|
-
.card-
|
|
233
|
+
.card-name {
|
|
239
234
|
display: flex;
|
|
240
|
-
|
|
235
|
+
flex-direction: column;
|
|
241
236
|
align-items: flex-start;
|
|
242
|
-
|
|
237
|
+
gap: 6px;
|
|
238
|
+
width: 140px;
|
|
239
|
+
flex-shrink: 0;
|
|
243
240
|
}
|
|
244
241
|
|
|
245
242
|
.profile-title {
|
|
246
|
-
font-size:
|
|
243
|
+
font-size: 0.95rem;
|
|
247
244
|
font-weight: 700;
|
|
248
245
|
color: #fff;
|
|
249
|
-
|
|
246
|
+
overflow: hidden;
|
|
247
|
+
text-overflow: ellipsis;
|
|
248
|
+
white-space: nowrap;
|
|
249
|
+
max-width: 100%;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
.status-badge {
|
|
@@ -296,16 +296,26 @@
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
.card-body {
|
|
299
|
+
flex: 1;
|
|
300
|
+
min-width: 0;
|
|
299
301
|
display: flex;
|
|
300
302
|
flex-direction: column;
|
|
301
|
-
gap:
|
|
302
|
-
margin-bottom: 20px;
|
|
303
|
+
gap: 4px;
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
.path-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
.path-value {
|
|
307
|
+
font-family: var(--font-mono);
|
|
308
|
+
font-size: 0.75rem;
|
|
309
|
+
color: #e2e8f0;
|
|
310
|
+
overflow: hidden;
|
|
311
|
+
text-overflow: ellipsis;
|
|
312
|
+
white-space: nowrap;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.path-value .path-label {
|
|
316
|
+
display: inline-block;
|
|
317
|
+
width: 52px;
|
|
318
|
+
font-family: inherit;
|
|
309
319
|
}
|
|
310
320
|
|
|
311
321
|
.path-label {
|
|
@@ -316,53 +326,24 @@
|
|
|
316
326
|
letter-spacing: 0.05em;
|
|
317
327
|
}
|
|
318
328
|
|
|
319
|
-
.path-value {
|
|
320
|
-
font-family: var(--font-mono);
|
|
321
|
-
font-size: 0.75rem;
|
|
322
|
-
background: rgba(0, 0, 0, 0.25);
|
|
323
|
-
padding: 6px 10px;
|
|
324
|
-
border-radius: 8px;
|
|
325
|
-
border: 1px solid rgba(255, 255, 255, 0.03);
|
|
326
|
-
overflow-x: auto;
|
|
327
|
-
white-space: nowrap;
|
|
328
|
-
color: #e2e8f0;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
329
|
.meta-row {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
gap: 8px;
|
|
335
|
-
margin-top: 4px;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
.meta-item {
|
|
339
|
-
display: flex;
|
|
340
|
-
flex-direction: column;
|
|
341
|
-
gap: 2px;
|
|
342
|
-
background: rgba(255, 255, 255, 0.02);
|
|
343
|
-
padding: 6px 8px;
|
|
344
|
-
border-radius: 8px;
|
|
345
|
-
border: 1px solid rgba(255, 255, 255, 0.02);
|
|
346
|
-
text-align: center;
|
|
330
|
+
font-size: 0.7rem;
|
|
331
|
+
color: var(--text-muted);
|
|
347
332
|
}
|
|
348
333
|
|
|
349
|
-
.meta-
|
|
350
|
-
font-size: 0.7rem;
|
|
351
|
-
font-weight: 700;
|
|
334
|
+
.meta-row b {
|
|
352
335
|
color: #fff;
|
|
353
336
|
}
|
|
354
337
|
|
|
355
338
|
.card-actions {
|
|
356
339
|
display: flex;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
border-top: 1px solid var(--surface-border);
|
|
360
|
-
padding-top: 14px;
|
|
340
|
+
gap: 8px;
|
|
341
|
+
flex-shrink: 0;
|
|
361
342
|
}
|
|
362
343
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
@media (max-width: 720px) {
|
|
345
|
+
.card { flex-wrap: wrap; }
|
|
346
|
+
.card-name { width: 100%; flex-direction: row; justify-content: space-between; }
|
|
366
347
|
}
|
|
367
348
|
|
|
368
349
|
/* Toggle Switch */
|
|
@@ -639,6 +620,35 @@
|
|
|
639
620
|
line-height: 1.6;
|
|
640
621
|
}
|
|
641
622
|
|
|
623
|
+
.health-dot {
|
|
624
|
+
width: 8px;
|
|
625
|
+
height: 8px;
|
|
626
|
+
border-radius: 50%;
|
|
627
|
+
background: var(--text-muted);
|
|
628
|
+
flex-shrink: 0;
|
|
629
|
+
}
|
|
630
|
+
.health-dot.ok { background: var(--success); }
|
|
631
|
+
.health-dot.warn { background: var(--warning); }
|
|
632
|
+
.health-dot.bad { background: var(--danger); }
|
|
633
|
+
|
|
634
|
+
.link-btn {
|
|
635
|
+
background: none;
|
|
636
|
+
border: none;
|
|
637
|
+
color: var(--accent);
|
|
638
|
+
font: inherit;
|
|
639
|
+
cursor: pointer;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.error-banner {
|
|
643
|
+
background: var(--danger-glow);
|
|
644
|
+
border: 1px solid rgba(244, 63, 94, 0.3);
|
|
645
|
+
color: var(--danger);
|
|
646
|
+
border-radius: 12px;
|
|
647
|
+
padding: 10px 14px;
|
|
648
|
+
font-size: 0.75rem;
|
|
649
|
+
margin-bottom: 16px;
|
|
650
|
+
}
|
|
651
|
+
|
|
642
652
|
/* Autostart Card */
|
|
643
653
|
.settings-card {
|
|
644
654
|
background: var(--surface);
|
|
@@ -708,8 +718,10 @@
|
|
|
708
718
|
<div class="brand-title">
|
|
709
719
|
<h1>hsync Control Dashboard</h1>
|
|
710
720
|
<div class="brand-server">
|
|
721
|
+
<span class="health-dot" id="healthDot" title="Checking server..."></span>
|
|
711
722
|
<span>Server:</span>
|
|
712
723
|
<span class="url" id="serverUrlText">Loading...</span>
|
|
724
|
+
<button class="link-btn" onclick="changeServer()">Change</button>
|
|
713
725
|
</div>
|
|
714
726
|
</div>
|
|
715
727
|
</div>
|
|
@@ -736,6 +748,8 @@
|
|
|
736
748
|
</div>
|
|
737
749
|
</header>
|
|
738
750
|
|
|
751
|
+
<div class="error-banner" id="healthError" style="display: none;"></div>
|
|
752
|
+
|
|
739
753
|
<div class="dashboard-grid">
|
|
740
754
|
<!-- Profiles list -->
|
|
741
755
|
<div class="profiles-section">
|
|
@@ -787,6 +801,20 @@
|
|
|
787
801
|
</div>
|
|
788
802
|
</div>
|
|
789
803
|
|
|
804
|
+
<div class="settings-card" style="flex-wrap: wrap; gap: 12px;">
|
|
805
|
+
<div class="settings-info">
|
|
806
|
+
<div class="settings-title">Account</div>
|
|
807
|
+
<div class="settings-desc" id="accountDesc">Checking login...</div>
|
|
808
|
+
</div>
|
|
809
|
+
<form id="loginForm" style="display: none; flex-wrap: wrap; gap: 8px; align-items: center;" onsubmit="event.preventDefault(); doLogin();">
|
|
810
|
+
<input class="form-input" id="loginEmail" type="email" placeholder="Email" autocomplete="username" required style="width: 190px;">
|
|
811
|
+
<input class="form-input" id="loginPassword" type="password" placeholder="Password" autocomplete="current-password" required style="width: 150px;">
|
|
812
|
+
<button class="btn btn-primary" type="submit">Login</button>
|
|
813
|
+
<button class="btn btn-secondary" type="button" onclick="doApiKeyLogin()">Use API Key</button>
|
|
814
|
+
</form>
|
|
815
|
+
<button class="btn btn-secondary" id="logoutBtn" style="display: none;" onclick="doLogout()">Logout</button>
|
|
816
|
+
</div>
|
|
817
|
+
|
|
790
818
|
<!-- Logs -->
|
|
791
819
|
<div class="terminal-section">
|
|
792
820
|
<div class="console-header">
|
|
@@ -797,9 +825,14 @@
|
|
|
797
825
|
</svg>
|
|
798
826
|
<span>Sync Console Logs</span>
|
|
799
827
|
</div>
|
|
800
|
-
<
|
|
801
|
-
|
|
802
|
-
|
|
828
|
+
<div style="display: flex; gap: 8px;">
|
|
829
|
+
<button class="btn btn-danger btn-icon" onclick="deleteLogFile()" title="Delete all lines from ~/.hsync/daemon.log to free disk space" style="padding: 6px 12px; font-size: 0.7rem;">
|
|
830
|
+
Delete Log File
|
|
831
|
+
</button>
|
|
832
|
+
<button class="btn btn-secondary btn-icon" onclick="clearLogs()" title="Clear Log Feed" style="padding: 6px 12px; font-size: 0.7rem;">
|
|
833
|
+
Clear Feed
|
|
834
|
+
</button>
|
|
835
|
+
</div>
|
|
803
836
|
</div>
|
|
804
837
|
<div class="console-window" id="consoleLogs">
|
|
805
838
|
<div class="console-empty">
|
|
@@ -871,11 +904,27 @@
|
|
|
871
904
|
</div>
|
|
872
905
|
</div>
|
|
873
906
|
|
|
874
|
-
<div class="
|
|
875
|
-
<
|
|
876
|
-
|
|
907
|
+
<div class="form-row">
|
|
908
|
+
<div class="form-group">
|
|
909
|
+
<label class="form-label" for="jobInclude">Include Globs (one per line)</label>
|
|
910
|
+
<textarea class="form-input" id="jobInclude" rows="3" placeholder="src/** *.md"></textarea>
|
|
911
|
+
</div>
|
|
912
|
+
<div class="form-group">
|
|
913
|
+
<label class="form-label" for="jobExclude">Exclude Globs (one per line)</label>
|
|
914
|
+
<textarea class="form-input" id="jobExclude" rows="3" placeholder="**/*.log dist/**"></textarea>
|
|
915
|
+
</div>
|
|
877
916
|
</div>
|
|
878
917
|
|
|
918
|
+
<label class="checkbox-group">
|
|
919
|
+
<input type="checkbox" id="jobConflictCopy" checked>
|
|
920
|
+
<span class="checkbox-label">Create copy of conflicted files</span>
|
|
921
|
+
</label>
|
|
922
|
+
|
|
923
|
+
<label class="checkbox-group">
|
|
924
|
+
<input type="checkbox" id="jobGitignore" checked>
|
|
925
|
+
<span class="checkbox-label">Respect .gitignore files</span>
|
|
926
|
+
</label>
|
|
927
|
+
|
|
879
928
|
<div class="modal-footer">
|
|
880
929
|
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
|
881
930
|
<button type="submit" class="btn btn-primary" id="saveBtn">Register Profile</button>
|
|
@@ -901,8 +950,82 @@
|
|
|
901
950
|
// Setup periodic polling
|
|
902
951
|
pollIntervalId = setInterval(pollServiceState, 3000);
|
|
903
952
|
logPollIntervalId = setInterval(pollLogs, 3000);
|
|
953
|
+
pollHealth();
|
|
954
|
+
setInterval(pollHealth, 15000);
|
|
904
955
|
});
|
|
905
956
|
|
|
957
|
+
async function pollHealth() {
|
|
958
|
+
let health;
|
|
959
|
+
try {
|
|
960
|
+
health = await (await fetch('/api/local/status')).json();
|
|
961
|
+
} catch {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const ok = health.serverUp && health.loggedIn;
|
|
965
|
+
document.getElementById('serverUrlText').textContent = health.serverUrl;
|
|
966
|
+
const dot = document.getElementById('healthDot');
|
|
967
|
+
dot.className = `health-dot ${ok ? 'ok' : health.serverUp ? 'warn' : 'bad'}`;
|
|
968
|
+
dot.title = ok ? `Server up, logged in as ${health.email}` : health.error;
|
|
969
|
+
|
|
970
|
+
const errorEl = document.getElementById('healthError');
|
|
971
|
+
errorEl.textContent = health.error || '';
|
|
972
|
+
errorEl.style.display = health.error ? 'block' : 'none';
|
|
973
|
+
|
|
974
|
+
document.getElementById('accountDesc').textContent = health.loggedIn
|
|
975
|
+
? `Signed in as ${health.email} (${health.role})`
|
|
976
|
+
: health.serverUp ? 'Not logged in. Sync jobs cannot run until you log in.' : 'Server unreachable.';
|
|
977
|
+
document.getElementById('loginForm').style.display = health.loggedIn ? 'none' : 'flex';
|
|
978
|
+
document.getElementById('logoutBtn').style.display = health.loggedIn ? 'inline-flex' : 'none';
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async function postLocal(url, body) {
|
|
982
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
|
|
983
|
+
if (!res.ok) throw new Error((await res.json()).error || `Request failed (${res.status})`);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async function changeServer() {
|
|
987
|
+
const url = prompt('Server URL', document.getElementById('serverUrlText').textContent);
|
|
988
|
+
if (!url) return;
|
|
989
|
+
try {
|
|
990
|
+
await postLocal('/api/local/server', { url: url.trim() });
|
|
991
|
+
} catch (err) {
|
|
992
|
+
alert(err.message);
|
|
993
|
+
}
|
|
994
|
+
await pollHealth();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function doLogin() {
|
|
998
|
+
const passwordEl = document.getElementById('loginPassword');
|
|
999
|
+
try {
|
|
1000
|
+
await postLocal('/api/local/login', { email: document.getElementById('loginEmail').value.trim(), password: passwordEl.value });
|
|
1001
|
+
passwordEl.value = '';
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
alert(`Login failed: ${err.message}`);
|
|
1004
|
+
}
|
|
1005
|
+
await pollHealth();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function doApiKeyLogin() {
|
|
1009
|
+
const apiKey = prompt('Paste an API key (create one in the web app under Settings)');
|
|
1010
|
+
if (!apiKey) return;
|
|
1011
|
+
try {
|
|
1012
|
+
await postLocal('/api/local/login', { apiKey: apiKey.trim() });
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
alert(`Login failed: ${err.message}`);
|
|
1015
|
+
}
|
|
1016
|
+
await pollHealth();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async function doLogout() {
|
|
1020
|
+
if (!confirm('Log out? Sync jobs will stop working until you log in again.')) return;
|
|
1021
|
+
try {
|
|
1022
|
+
await postLocal('/api/local/logout');
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
alert(err.message);
|
|
1025
|
+
}
|
|
1026
|
+
await pollHealth();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
906
1029
|
async function loadDashboardData() {
|
|
907
1030
|
showLoading(true);
|
|
908
1031
|
try {
|
|
@@ -1043,7 +1166,7 @@
|
|
|
1043
1166
|
// Parse timestamps [HH:MM:SS] if present
|
|
1044
1167
|
let time = '';
|
|
1045
1168
|
let text = line;
|
|
1046
|
-
const match = line.match(/^(\[\d{1,2}:\d{2}:\d{2}
|
|
1169
|
+
const match = line.match(/^(\[\d{1,2}:\d{2}:\d{2}(?: [AP]M)?\])\s*(.*)/);
|
|
1047
1170
|
if (match) {
|
|
1048
1171
|
time = match[1];
|
|
1049
1172
|
text = match[2];
|
|
@@ -1069,6 +1192,17 @@
|
|
|
1069
1192
|
}
|
|
1070
1193
|
}
|
|
1071
1194
|
|
|
1195
|
+
async function deleteLogFile() {
|
|
1196
|
+
if (!confirm('Delete all lines from the log file on disk? This cannot be undone.')) return;
|
|
1197
|
+
try {
|
|
1198
|
+
const res = await fetch('/api/local/logs/clear', { method: 'POST' });
|
|
1199
|
+
if (!res.ok) throw new Error((await res.json()).error || 'Failed to clear log file.');
|
|
1200
|
+
clearLogs();
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
alert(err.message);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1072
1206
|
function clearLogs() {
|
|
1073
1207
|
// Only hides the view; the log file on disk is untouched
|
|
1074
1208
|
clearedThroughLine = lastLogLine;
|
|
@@ -1094,7 +1228,7 @@
|
|
|
1094
1228
|
return;
|
|
1095
1229
|
}
|
|
1096
1230
|
|
|
1097
|
-
grid.style.display = '
|
|
1231
|
+
grid.style.display = 'flex';
|
|
1098
1232
|
empty.style.display = 'none';
|
|
1099
1233
|
grid.innerHTML = '';
|
|
1100
1234
|
|
|
@@ -1105,60 +1239,37 @@
|
|
|
1105
1239
|
|
|
1106
1240
|
// Setup card HTML structure
|
|
1107
1241
|
card.innerHTML = `
|
|
1108
|
-
<div>
|
|
1109
|
-
<
|
|
1110
|
-
|
|
1111
|
-
<
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
<
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
</
|
|
1122
|
-
<
|
|
1123
|
-
|
|
1124
|
-
<span class="path-value">${escapeHtml(job.remoteBucket)}:${escapeHtml(job.remotePath || '')}</span>
|
|
1125
|
-
</div>
|
|
1126
|
-
|
|
1127
|
-
<div class="meta-row">
|
|
1128
|
-
<div class="meta-item">
|
|
1129
|
-
<span class="path-label">Direction</span>
|
|
1130
|
-
<span class="meta-val">${capitalize(job.direction)}</span>
|
|
1131
|
-
</div>
|
|
1132
|
-
<div class="meta-item">
|
|
1133
|
-
<span class="path-label">Interval</span>
|
|
1134
|
-
<span class="meta-val">${job.intervalMs ? job.intervalMs / 1000 : 10}s</span>
|
|
1135
|
-
</div>
|
|
1136
|
-
<div class="meta-item">
|
|
1137
|
-
<span class="path-label">Conflict Copy</span>
|
|
1138
|
-
<span class="meta-val">${job.conflictCopy ? 'Yes' : 'No'}</span>
|
|
1139
|
-
</div>
|
|
1140
|
-
</div>
|
|
1141
|
-
</div>
|
|
1242
|
+
<div class="card-name">
|
|
1243
|
+
<span class="profile-title" title="${escapeHtml(job.name)}">${escapeHtml(job.name)}</span>
|
|
1244
|
+
<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'}">
|
|
1245
|
+
<span class="pulse-dot"></span>
|
|
1246
|
+
<span class="badge-text">${job.enabled ? 'Enabled' : 'Paused'}</span>
|
|
1247
|
+
</button>
|
|
1248
|
+
</div>
|
|
1249
|
+
|
|
1250
|
+
<div class="card-body">
|
|
1251
|
+
<span class="path-value" title="${escapeHtml(job.localPath)}"><span class="path-label">Local</span>${escapeHtml(job.localPath)}</span>
|
|
1252
|
+
<span class="path-value" title="${escapeHtml(job.remoteBucket)}:${escapeHtml(job.remotePath || '')}"><span class="path-label">Remote</span>${escapeHtml(job.remoteBucket)}:${escapeHtml(job.remotePath || '')}</span>
|
|
1253
|
+
<span class="meta-row">
|
|
1254
|
+
<b>${capitalize(job.direction)}</b> · every <b>${job.intervalMs ? job.intervalMs / 1000 : 10}s</b>
|
|
1255
|
+
· conflict copy <b>${job.conflictCopy ? 'Yes' : 'No'}</b>
|
|
1256
|
+
· .gitignore <b>${job.respectGitignore !== false ? 'Yes' : 'No'}</b>
|
|
1257
|
+
</span>
|
|
1142
1258
|
</div>
|
|
1143
1259
|
|
|
1144
1260
|
<div class="card-actions">
|
|
1145
|
-
<
|
|
1146
|
-
<
|
|
1147
|
-
|
|
1148
|
-
<
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
1158
|
-
</svg>
|
|
1159
|
-
</button>
|
|
1160
|
-
</div>
|
|
1161
|
-
|
|
1261
|
+
<button class="btn btn-secondary btn-icon" onclick="openEditModal('${job.id}')" title="Edit Profile">
|
|
1262
|
+
<svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1263
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
|
1264
|
+
<path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
|
1265
|
+
</svg>
|
|
1266
|
+
</button>
|
|
1267
|
+
<button class="btn btn-danger btn-icon" onclick="deleteProfile('${job.id}', '${escapeJs(job.name)}')" title="Delete Profile">
|
|
1268
|
+
<svg style="width: 14px; height: 14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1269
|
+
<polyline points="3 6 5 6 21 6"></polyline>
|
|
1270
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
1271
|
+
</svg>
|
|
1272
|
+
</button>
|
|
1162
1273
|
<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
1274
|
<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
|
|
1164
1275
|
</button>
|
|
@@ -1227,6 +1338,7 @@
|
|
|
1227
1338
|
document.getElementById('jobForm').reset();
|
|
1228
1339
|
document.getElementById('saveBtn').textContent = 'Register Profile';
|
|
1229
1340
|
document.getElementById('jobConflictCopy').checked = true;
|
|
1341
|
+
document.getElementById('jobGitignore').checked = true;
|
|
1230
1342
|
|
|
1231
1343
|
document.getElementById('jobModalBackdrop').classList.add('open');
|
|
1232
1344
|
}
|
|
@@ -1245,6 +1357,9 @@
|
|
|
1245
1357
|
document.getElementById('jobStrategy').value = job.conflictStrategy || 'newer-wins';
|
|
1246
1358
|
document.getElementById('jobInterval').value = job.intervalMs ? job.intervalMs / 1000 : 10;
|
|
1247
1359
|
document.getElementById('jobConflictCopy').checked = !!job.conflictCopy;
|
|
1360
|
+
document.getElementById('jobGitignore').checked = job.respectGitignore !== false;
|
|
1361
|
+
document.getElementById('jobInclude').value = (job.filePatterns || []).join('\n');
|
|
1362
|
+
document.getElementById('jobExclude').value = (job.excludePatterns || []).join('\n');
|
|
1248
1363
|
document.getElementById('saveBtn').textContent = 'Save Changes';
|
|
1249
1364
|
|
|
1250
1365
|
document.getElementById('jobModalBackdrop').classList.add('open');
|
|
@@ -1260,14 +1375,6 @@
|
|
|
1260
1375
|
}
|
|
1261
1376
|
}
|
|
1262
1377
|
|
|
1263
|
-
function toggleCheckboxValue(e) {
|
|
1264
|
-
// Allow clicking label/row to toggle the checkbox
|
|
1265
|
-
if (e.target.id !== 'jobConflictCopy') {
|
|
1266
|
-
const checkbox = document.getElementById('jobConflictCopy');
|
|
1267
|
-
checkbox.checked = !checkbox.checked;
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
1378
|
async function saveJob(e) {
|
|
1272
1379
|
e.preventDefault();
|
|
1273
1380
|
|
|
@@ -1279,6 +1386,9 @@
|
|
|
1279
1386
|
const strategy = document.getElementById('jobStrategy').value;
|
|
1280
1387
|
const intervalSec = parseInt(document.getElementById('jobInterval').value, 10);
|
|
1281
1388
|
const conflictCopy = document.getElementById('jobConflictCopy').checked;
|
|
1389
|
+
const gitignore = document.getElementById('jobGitignore').checked;
|
|
1390
|
+
const include = document.getElementById('jobInclude').value.split('\n').map(s => s.trim()).filter(Boolean);
|
|
1391
|
+
const exclude = document.getElementById('jobExclude').value.split('\n').map(s => s.trim()).filter(Boolean);
|
|
1282
1392
|
|
|
1283
1393
|
// Sanitize name/ID to matching requirements: lowercased alphanumeric + hyphen + underscores
|
|
1284
1394
|
const name = rawName.toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
@@ -1300,7 +1410,10 @@
|
|
|
1300
1410
|
direction,
|
|
1301
1411
|
strategy,
|
|
1302
1412
|
intervalSec,
|
|
1303
|
-
noConflictCopy: !conflictCopy
|
|
1413
|
+
noConflictCopy: !conflictCopy,
|
|
1414
|
+
include,
|
|
1415
|
+
exclude,
|
|
1416
|
+
gitignore
|
|
1304
1417
|
})
|
|
1305
1418
|
});
|
|
1306
1419
|
|
|
@@ -1330,7 +1443,10 @@
|
|
|
1330
1443
|
conflictStrategy: strategy,
|
|
1331
1444
|
conflictCopy,
|
|
1332
1445
|
intervalMs: intervalSec * 1000,
|
|
1333
|
-
enabled: true
|
|
1446
|
+
enabled: true,
|
|
1447
|
+
filePatterns: include,
|
|
1448
|
+
excludePatterns: exclude,
|
|
1449
|
+
respectGitignore: gitignore
|
|
1334
1450
|
};
|
|
1335
1451
|
|
|
1336
1452
|
const configRes = await fetch('/api/local/config');
|