@a83/orbiter-admin 0.3.13 → 0.3.15
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/package.json +7 -3
- package/public/build.html +1 -1
- package/public/collections.html +1 -1
- package/public/dashboard.html +30 -3
- package/public/editor.html +32 -0
- package/public/import.html +1 -1
- package/public/media.html +1 -1
- package/public/schema.html +19 -1
- package/public/settings.html +76 -6
- package/public/sidebar.js +11 -0
- package/public/style.css +1142 -0
- package/public/theme.js +2 -1
- package/public/users.html +1 -1
- package/public/xfce.js +697 -0
- package/src/cli.js +30 -0
- package/src/email.js +50 -0
- package/src/routes/comments.js +2 -0
- package/src/routes/entries.js +2 -0
- package/src/routes/locks.js +48 -0
- package/src/routes/meta.js +2 -0
- package/src/server.js +2 -0
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a83/orbiter-admin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"description": "Standalone admin server for Orbiter CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"orbiter-admin": "./src/cli.js"
|
|
9
|
+
},
|
|
7
10
|
"scripts": {
|
|
8
|
-
"start": "node src/
|
|
9
|
-
"dev": "node --watch src/
|
|
11
|
+
"start": "node src/cli.js",
|
|
12
|
+
"dev": "node --watch src/cli.js"
|
|
10
13
|
},
|
|
11
14
|
"engines": {
|
|
12
15
|
"node": ">=20.0.0"
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
"@a83/orbiter-core": "^0.3.2",
|
|
30
33
|
"@hono/node-server": "^1.14.4",
|
|
31
34
|
"hono": "^4.7.11",
|
|
35
|
+
"nodemailer": "^8.0.10",
|
|
32
36
|
"sharp": "^0.34.5"
|
|
33
37
|
}
|
|
34
38
|
}
|
package/public/build.html
CHANGED
package/public/collections.html
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
</div>
|
|
42
42
|
</nav>
|
|
43
43
|
<main class="main">
|
|
44
|
-
<div class="page-header" style="display:flex;align-items:flex-start;justify-content:space-between">
|
|
44
|
+
<div class="page-header glass-card" style="display:flex;align-items:flex-start;justify-content:space-between">
|
|
45
45
|
<div>
|
|
46
46
|
<h1 class="page-title">Collections</h1>
|
|
47
47
|
<p class="page-sub">All content types in this pod</p>
|
package/public/dashboard.html
CHANGED
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
|
|
206
206
|
<!-- Workspace: Notes + To-do -->
|
|
207
207
|
<div class="workspace glass-card">
|
|
208
|
-
<div class="ws-panel">
|
|
208
|
+
<div class="ws-panel" id="notes">
|
|
209
209
|
<div class="ws-head">
|
|
210
210
|
<div class="section-title">Notes</div>
|
|
211
211
|
<div style="display:flex;align-items:center;gap:10px">
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
</div>
|
|
216
216
|
<textarea class="notes-area" id="notes-area" placeholder="Jot something down…"></textarea>
|
|
217
217
|
</div>
|
|
218
|
-
<div class="ws-panel">
|
|
218
|
+
<div class="ws-panel" id="todos">
|
|
219
219
|
<div class="ws-head">
|
|
220
220
|
<div class="section-title">To-Do</div>
|
|
221
221
|
<div style="display:flex;align-items:center;gap:10px">
|
|
@@ -236,7 +236,34 @@
|
|
|
236
236
|
</div>
|
|
237
237
|
</div>
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
<script>
|
|
240
|
+
// Scroll to workspace panel when navigated via dock hash link
|
|
241
|
+
(function () {
|
|
242
|
+
var hash = location.hash;
|
|
243
|
+
if (hash === '#notes' || hash === '#todos') {
|
|
244
|
+
window.addEventListener('load', function () {
|
|
245
|
+
var target = document.getElementById(hash.slice(1));
|
|
246
|
+
if (target) {
|
|
247
|
+
setTimeout(function () {
|
|
248
|
+
var main = document.querySelector('.main');
|
|
249
|
+
if (main) {
|
|
250
|
+
main.scrollTo({ top: target.offsetTop - 60, behavior: 'smooth' });
|
|
251
|
+
} else {
|
|
252
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
253
|
+
}
|
|
254
|
+
if (hash === '#notes') {
|
|
255
|
+
var ta = document.getElementById('notes-area');
|
|
256
|
+
if (ta) ta.focus();
|
|
257
|
+
} else {
|
|
258
|
+
var inp = document.getElementById('todo-input');
|
|
259
|
+
if (inp) inp.focus();
|
|
260
|
+
}
|
|
261
|
+
}, 400);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
266
|
+
</script>
|
|
240
267
|
<script type="module">
|
|
241
268
|
// Auth
|
|
242
269
|
const me = await fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json()).catch(() => null);
|
package/public/editor.html
CHANGED
|
@@ -409,6 +409,9 @@
|
|
|
409
409
|
<span class="logout" id="logout-btn">Sign out</span>
|
|
410
410
|
</div>
|
|
411
411
|
</header>
|
|
412
|
+
<div id="lock-banner" style="display:none;background:color-mix(in srgb,var(--gold) 12%,var(--bg2));border-bottom:1px solid color-mix(in srgb,var(--gold) 30%,transparent);padding:7px 20px;font-size:11px;color:var(--text);display:none;align-items:center;gap:8px;">
|
|
413
|
+
<span style="color:var(--gold);">⚠</span> <span id="lock-banner-msg"></span>
|
|
414
|
+
</div>
|
|
412
415
|
<div class="editor-shell" id="editor-shell">
|
|
413
416
|
<div class="editor-main" id="editor-main">
|
|
414
417
|
<div id="saved-flash" style="display:none;" class="saved-flash">Entry saved</div>
|
|
@@ -517,6 +520,35 @@
|
|
|
517
520
|
|
|
518
521
|
if (!colData) { location.replace('/collections.html'); }
|
|
519
522
|
|
|
523
|
+
// Entry locking — claim lock, warn if someone else is editing
|
|
524
|
+
let lockHeld = false;
|
|
525
|
+
let lockHeartbeat = null;
|
|
526
|
+
async function claimLock() {
|
|
527
|
+
if (IS_NEW) return;
|
|
528
|
+
const res = await fetch(`/api/locks/${COLLECTION}/${SLUG}`,{method:'POST',credentials:'include'}).catch(()=>null);
|
|
529
|
+
if (!res) return;
|
|
530
|
+
if (res.status === 409) {
|
|
531
|
+
const j = await res.json().catch(()=>({}));
|
|
532
|
+
const banner = document.getElementById('lock-banner');
|
|
533
|
+
document.getElementById('lock-banner-msg').textContent = `${j.by ?? 'Someone'} is currently editing this entry. Your changes may conflict.`;
|
|
534
|
+
banner.style.display = 'flex';
|
|
535
|
+
lockHeld = false;
|
|
536
|
+
} else {
|
|
537
|
+
lockHeld = true;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function releaseLock() {
|
|
541
|
+
if (!lockHeld || IS_NEW) return;
|
|
542
|
+
lockHeld = false;
|
|
543
|
+
await fetch(`/api/locks/${COLLECTION}/${SLUG}`,{method:'DELETE',credentials:'include'}).catch(()=>{});
|
|
544
|
+
}
|
|
545
|
+
claimLock().then(()=>{
|
|
546
|
+
if (IS_NEW) return;
|
|
547
|
+
lockHeartbeat = setInterval(()=>fetch(`/api/locks/${COLLECTION}/${SLUG}`,{method:'POST',credentials:'include'}).catch(()=>{}), 60_000);
|
|
548
|
+
});
|
|
549
|
+
window.addEventListener('beforeunload', () => { if (lockHeld) releaseLock(); });
|
|
550
|
+
document.addEventListener('visibilitychange', () => { if (document.hidden && lockHeld) releaseLock(); });
|
|
551
|
+
|
|
520
552
|
// Breadcrumb
|
|
521
553
|
document.getElementById('back-to-col').textContent = colData.label;
|
|
522
554
|
document.getElementById('back-to-col').href = `/entries.html?col=${COLLECTION}&label=${encodeURIComponent(colData.label)}`;
|
package/public/import.html
CHANGED
package/public/media.html
CHANGED
package/public/schema.html
CHANGED
|
@@ -390,9 +390,11 @@
|
|
|
390
390
|
${total>0?'<div class="delete-warn">Adding fields is non-destructive. Removing fields hides existing data but does not delete it.</div>':''}
|
|
391
391
|
<div id="edit-field-rows" class="field-rows"></div>
|
|
392
392
|
<div class="editor-actions">
|
|
393
|
-
<div style="display:flex;gap:10px;">
|
|
393
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
|
394
394
|
<button class="btn btn-primary" id="btn-save-edit">Save</button>
|
|
395
395
|
<button class="btn" id="btn-add-edit">+ Add field</button>
|
|
396
|
+
<button class="btn" id="btn-export-schema">↓ Export schema</button>
|
|
397
|
+
<label class="btn" style="cursor:pointer;">↑ Import schema<input type="file" id="import-schema-file" accept=".json" style="display:none"></label>
|
|
396
398
|
</div>
|
|
397
399
|
<button class="btn" id="btn-del-col" style="color:var(--red);border-color:var(--red);">Delete collection</button>
|
|
398
400
|
</div>
|
|
@@ -401,6 +403,22 @@
|
|
|
401
403
|
enableFieldDrag(rowsEl);
|
|
402
404
|
Object.entries(schema).forEach(([k,f])=>addFieldRow(rowsEl,k,f.label??k,f.type??'string',!!f.required,(f.options??[]).join(', '),f.collection??'',f.multiple!==false));
|
|
403
405
|
document.getElementById('btn-add-edit').addEventListener('click',()=>addFieldRow(rowsEl));
|
|
406
|
+
document.getElementById('btn-export-schema').addEventListener('click',()=>{
|
|
407
|
+
const blob=new Blob([JSON.stringify(serializeSchema(rowsEl),null,2)],{type:'application/json'});
|
|
408
|
+
const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`${col.id}-schema.json`; a.click(); URL.revokeObjectURL(a.href);
|
|
409
|
+
});
|
|
410
|
+
document.getElementById('import-schema-file').addEventListener('change',e=>{
|
|
411
|
+
const file=e.target.files[0]; if (!file) return;
|
|
412
|
+
const reader=new FileReader();
|
|
413
|
+
reader.onload=ev=>{
|
|
414
|
+
try {
|
|
415
|
+
const s=JSON.parse(ev.target.result);
|
|
416
|
+
rowsEl.innerHTML='';
|
|
417
|
+
Object.entries(s).forEach(([k,f])=>addFieldRow(rowsEl,k,f.label??k,f.type??'string',!!f.required,(f.options??[]).join(', '),f.collection??'',f.multiple!==false));
|
|
418
|
+
} catch { alert('Invalid schema JSON'); }
|
|
419
|
+
};
|
|
420
|
+
reader.readAsText(file); e.target.value='';
|
|
421
|
+
});
|
|
404
422
|
// Load existing preview URL for this collection
|
|
405
423
|
fetch(`/api/meta/preview_url~${col.id}`,{credentials:'include'}).then(r=>r.json()).then(d=>{
|
|
406
424
|
const inp=document.getElementById('edit-preview-url'); if(inp)inp.value=d.value??'';
|
package/public/settings.html
CHANGED
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
</div>
|
|
251
251
|
</nav>
|
|
252
252
|
<main class="main">
|
|
253
|
-
<div class="page-header">
|
|
253
|
+
<div class="page-header glass-card">
|
|
254
254
|
<h1 class="page-title">Settings</h1>
|
|
255
255
|
</div>
|
|
256
256
|
<div class="settings-wrap" id="settings-content">
|
|
@@ -449,6 +449,42 @@
|
|
|
449
449
|
</div>
|
|
450
450
|
</div>
|
|
451
451
|
|
|
452
|
+
<div class="settings-group">
|
|
453
|
+
<div class="group-header">Email notifications</div>
|
|
454
|
+
<div class="setting-row">
|
|
455
|
+
<div><div class="setting-label">SMTP host</div><div class="setting-desc">e.g. smtp.fastmail.com, smtp.gmail.com</div></div>
|
|
456
|
+
<input class="input" name="email.smtp_host" value="${get('email.smtp_host')||''}" placeholder="smtp.example.com" />
|
|
457
|
+
</div>
|
|
458
|
+
<div class="setting-row">
|
|
459
|
+
<div><div class="setting-label">SMTP port</div><div class="setting-desc">Usually 587 (STARTTLS) or 465 (SSL)</div></div>
|
|
460
|
+
<input class="input" name="email.smtp_port" type="number" value="${get('email.smtp_port')||'587'}" placeholder="587" style="max-width:100px" />
|
|
461
|
+
</div>
|
|
462
|
+
<div class="setting-row">
|
|
463
|
+
<div><div class="setting-label">SMTP username</div></div>
|
|
464
|
+
<input class="input" name="email.smtp_user" value="${get('email.smtp_user')||''}" placeholder="you@example.com" />
|
|
465
|
+
</div>
|
|
466
|
+
<div class="setting-row">
|
|
467
|
+
<div><div class="setting-label">SMTP password</div></div>
|
|
468
|
+
<input class="input" type="password" name="email.smtp_pass" value="${get('email.smtp_pass')||''}" placeholder="••••••••" />
|
|
469
|
+
</div>
|
|
470
|
+
<div class="setting-row">
|
|
471
|
+
<div><div class="setting-label">From address</div><div class="setting-desc">Defaults to SMTP username if blank</div></div>
|
|
472
|
+
<input class="input" name="email.smtp_from" value="${get('email.smtp_from')||''}" placeholder="Orbiter <noreply@example.com>" />
|
|
473
|
+
</div>
|
|
474
|
+
<div class="setting-row">
|
|
475
|
+
<div><div class="setting-label">Notify to</div><div class="setting-desc">Recipient email address for notifications</div></div>
|
|
476
|
+
<input class="input" name="email.notify_to" value="${get('email.notify_to')||''}" placeholder="admin@example.com" />
|
|
477
|
+
</div>
|
|
478
|
+
<div class="setting-row">
|
|
479
|
+
<div><div class="setting-label">Notify on publish</div><div class="setting-desc">Send email when an entry is published</div></div>
|
|
480
|
+
<input type="checkbox" name="email.notify_publish" value="1" ${get('email.notify_publish')==='1'?'checked':''} style="accent-color:var(--accent);width:14px;height:14px;" />
|
|
481
|
+
</div>
|
|
482
|
+
<div class="setting-row">
|
|
483
|
+
<div><div class="setting-label">Notify on comment</div><div class="setting-desc">Send email when a new comment is posted</div></div>
|
|
484
|
+
<input type="checkbox" name="email.notify_comment" value="1" ${get('email.notify_comment')==='1'?'checked':''} style="accent-color:var(--accent);width:14px;height:14px;" />
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
452
488
|
<div class="save-row">
|
|
453
489
|
<button type="submit" class="btn-save">Save settings</button>
|
|
454
490
|
</div>
|
|
@@ -594,6 +630,26 @@
|
|
|
594
630
|
<div class="theme-card-name">Glass</div>
|
|
595
631
|
<div class="theme-card-sub">Glassmorphism · blur</div>
|
|
596
632
|
</button>
|
|
633
|
+
<button class="style-card \${currentStyle==='xfce'?'active':''}" data-style="xfce">
|
|
634
|
+
<div class="style-preview sp-glass" style="position:relative;overflow:hidden;">
|
|
635
|
+
<div class="sp-orb sp-orb1"></div>
|
|
636
|
+
<div class="sp-orb sp-orb2"></div>
|
|
637
|
+
<div style="position:absolute;top:0;left:0;right:0;height:12%;background:color-mix(in srgb,var(--bg2) 60%,transparent);border-bottom:1px solid var(--line);"></div>
|
|
638
|
+
<div class="sp-body" style="margin:0;padding:8% 5% 25%;">
|
|
639
|
+
<div class="sp-row sp-glass-panel"></div>
|
|
640
|
+
<div class="sp-row sp-row-short sp-glass-panel"></div>
|
|
641
|
+
<div class="sp-row sp-glass-panel"></div>
|
|
642
|
+
</div>
|
|
643
|
+
<div style="position:absolute;bottom:4%;left:50%;transform:translateX(-50%);background:color-mix(in srgb,var(--bg2) 60%,transparent);border:1px solid var(--accent);border-radius:8px;padding:3px 8px;display:flex;gap:4px;align-items:center;">
|
|
644
|
+
<span style="width:6px;height:6px;background:var(--accent);border-radius:50%;display:block;"></span>
|
|
645
|
+
<span style="width:6px;height:6px;background:var(--muted);border-radius:50%;display:block;"></span>
|
|
646
|
+
<span style="width:6px;height:6px;background:var(--gold);border-radius:50%;display:block;"></span>
|
|
647
|
+
<span style="width:6px;height:6px;background:var(--muted);border-radius:50%;display:block;"></span>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
<div class="theme-card-name">Station</div>
|
|
651
|
+
<div class="theme-card-sub">Dock · no sidebar</div>
|
|
652
|
+
</button>
|
|
597
653
|
</div>
|
|
598
654
|
</div>
|
|
599
655
|
</div>
|
|
@@ -668,9 +724,19 @@
|
|
|
668
724
|
['media.s3_access_key', fd.get('media.s3_access_key')],
|
|
669
725
|
['media.s3_secret_key', fd.get('media.s3_secret_key')],
|
|
670
726
|
['media.s3_public_url', fd.get('media.s3_public_url')],
|
|
671
|
-
['api.enabled',
|
|
672
|
-
['api.token',
|
|
673
|
-
['preview.token',
|
|
727
|
+
['api.enabled', fd.get('api.enabled') ? '1' : '0'],
|
|
728
|
+
['api.token', fd.get('api.token')],
|
|
729
|
+
['preview.token', document.getElementById('preview-token-display').value || null],
|
|
730
|
+
['media.img_max_width', fd.get('media.img_max_width')],
|
|
731
|
+
['media.img_quality', fd.get('media.img_quality')],
|
|
732
|
+
['email.smtp_host', fd.get('email.smtp_host')],
|
|
733
|
+
['email.smtp_port', fd.get('email.smtp_port')],
|
|
734
|
+
['email.smtp_user', fd.get('email.smtp_user')],
|
|
735
|
+
['email.smtp_pass', fd.get('email.smtp_pass')],
|
|
736
|
+
['email.smtp_from', fd.get('email.smtp_from')],
|
|
737
|
+
['email.notify_to', fd.get('email.notify_to')],
|
|
738
|
+
['email.notify_publish', fd.get('email.notify_publish') ? '1' : '0'],
|
|
739
|
+
['email.notify_comment', fd.get('email.notify_comment') ? '1' : '0'],
|
|
674
740
|
]);
|
|
675
741
|
showBanner('site-banner','banner-ok','Settings saved');
|
|
676
742
|
});
|
|
@@ -733,9 +799,13 @@
|
|
|
733
799
|
const s = btn.dataset.style;
|
|
734
800
|
document.querySelectorAll('.style-card').forEach(b => b.classList.toggle('active', b.dataset.style === s));
|
|
735
801
|
const root = document.documentElement;
|
|
736
|
-
|
|
737
|
-
|
|
802
|
+
root.removeAttribute('data-style');
|
|
803
|
+
if (s === 'glass' || s === 'xfce') root.setAttribute('data-style', s);
|
|
738
804
|
localStorage.setItem('orb_style', s);
|
|
805
|
+
// xfce requires a reload to inject the dock
|
|
806
|
+
if (s === 'xfce' || document.documentElement.dataset.style === 'xfce') {
|
|
807
|
+
location.reload();
|
|
808
|
+
}
|
|
739
809
|
});
|
|
740
810
|
});
|
|
741
811
|
|
package/public/sidebar.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sidebar.js — dynamically populates collection links + pod footer in every page.
|
|
3
3
|
* Supports parent/child hierarchy (matching SidebarCollections.astro in the original).
|
|
4
|
+
* Also bootstraps xfce.js when orb_style === 'xfce'.
|
|
4
5
|
*/
|
|
6
|
+
;(function () {
|
|
7
|
+
if (localStorage.getItem('orb_style') === 'xfce') {
|
|
8
|
+
var xs = document.createElement('script');
|
|
9
|
+
xs.src = '/xfce.js';
|
|
10
|
+
var cs = document.currentScript;
|
|
11
|
+
if (cs && cs.parentNode) cs.parentNode.insertBefore(xs, cs.nextSibling);
|
|
12
|
+
else document.head.appendChild(xs);
|
|
13
|
+
}
|
|
14
|
+
})();
|
|
15
|
+
|
|
5
16
|
(function () {
|
|
6
17
|
document.addEventListener('DOMContentLoaded', function () {
|
|
7
18
|
var params = new URLSearchParams(location.search);
|