@a83/orbiter-admin 0.3.9 โ 0.3.10
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 +1 -1
- package/public/editor.html +54 -16
- package/public/entries.html +5 -3
- package/public/style.css +3 -2
- package/src/routes/entries.js +9 -3
- package/src/server.js +21 -0
package/package.json
CHANGED
package/public/editor.html
CHANGED
|
@@ -101,15 +101,21 @@
|
|
|
101
101
|
/* Status */
|
|
102
102
|
.status-bar { display:flex; align-items:center; gap:8px; padding:8px 0; margin-bottom:10px; }
|
|
103
103
|
.status-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
|
|
104
|
-
.status-dot.published
|
|
105
|
-
.status-dot.draft
|
|
104
|
+
.status-dot.published { background:var(--jade); }
|
|
105
|
+
.status-dot.draft { background:var(--gold); }
|
|
106
|
+
.status-dot.scheduled { background:var(--accent); }
|
|
106
107
|
.status-lbl { font-size:11px; }
|
|
107
|
-
.status-lbl.published
|
|
108
|
-
.status-lbl.draft
|
|
108
|
+
.status-lbl.published { color:var(--jade); }
|
|
109
|
+
.status-lbl.draft { color:var(--gold); }
|
|
110
|
+
.status-lbl.scheduled { color:var(--accent); }
|
|
109
111
|
.btn-publish { display:block; width:100%; padding:9px; background:var(--gold); border:none; color:var(--bg0); font-family:var(--mono); font-size:10px; letter-spacing:.12em; cursor:pointer; transition:background .15s; margin-bottom:5px; border-radius:var(--radius); }
|
|
110
112
|
.btn-publish:hover { background:#7a5520; }
|
|
111
|
-
.btn-draft { display:block; width:100%; padding:8px; background:transparent; border:1px solid var(--line); color:var(--mid); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:var(--radius); }
|
|
113
|
+
.btn-draft { display:block; width:100%; padding:8px; background:transparent; border:1px solid var(--line); color:var(--mid); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:var(--radius); margin-bottom:5px; }
|
|
112
114
|
.btn-draft:hover { border-color:var(--mid); color:var(--text); }
|
|
115
|
+
.btn-schedule { display:block; width:100%; padding:8px; background:transparent; border:1px solid var(--accent); color:var(--accent); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:var(--radius); margin-bottom:5px; }
|
|
116
|
+
.btn-schedule:hover { background:var(--accent-bg); }
|
|
117
|
+
.schedule-picker { display:none; gap:4px; margin-bottom:5px; }
|
|
118
|
+
.schedule-picker.open { display:flex; }
|
|
113
119
|
|
|
114
120
|
/* Schema fields in sidebar */
|
|
115
121
|
.field-input,.field-select { width:100%; background:var(--bg0); border:1px solid var(--line); padding:6px 8px; color:var(--heading); font-family:var(--mono); font-size:11px; outline:none; appearance:none; transition:border-color .15s; border-radius:var(--radius); box-sizing:border-box; }
|
|
@@ -539,8 +545,10 @@
|
|
|
539
545
|
|
|
540
546
|
function renderMetaPanel() {
|
|
541
547
|
const panel = document.getElementById('meta-panel');
|
|
542
|
-
const status
|
|
548
|
+
const status = IS_NEW ? 'draft' : (entryData?.status ?? 'draft');
|
|
543
549
|
const updatedAt = entryData?.updated_at;
|
|
550
|
+
const publishAt = entryData?.publish_at ?? null;
|
|
551
|
+
const publishAtInput = publishAt ? publishAt.replace(' ', 'T').slice(0, 16) : '';
|
|
544
552
|
|
|
545
553
|
let fieldsHtml = '';
|
|
546
554
|
const seoFields = {};
|
|
@@ -653,10 +661,16 @@
|
|
|
653
661
|
<div class="meta-section">
|
|
654
662
|
<div class="status-bar">
|
|
655
663
|
<div class="status-dot ${status}" id="status-dot"></div>
|
|
656
|
-
<div class="status-lbl ${status}" id="status-lbl">${status==='published'?'Published':'Draft'}</div>
|
|
664
|
+
<div class="status-lbl ${status}" id="status-lbl">${status==='published'?'Published':status==='scheduled'?'Scheduled':'Draft'}</div>
|
|
665
|
+
${status==='scheduled'&&publishAt?`<span style="font-size:10px;color:var(--muted);margin-left:auto">${new Date(publishAt.replace(' ','T')).toLocaleDateString()}</span>`:''}
|
|
657
666
|
</div>
|
|
658
|
-
<button type="button" class="btn-publish" id="btn-publish"
|
|
659
|
-
<button type="button" class="btn-draft" id="btn-draft"
|
|
667
|
+
<button type="button" class="btn-publish" id="btn-publish">${status==='published'?'Republish':'Publish now'}</button>
|
|
668
|
+
<button type="button" class="btn-draft" id="btn-draft">${status==='scheduled'?'Cancel schedule':'Save as draft'}</button>
|
|
669
|
+
<div class="schedule-picker${status==='scheduled'?' open':''}" id="schedule-picker">
|
|
670
|
+
<input type="datetime-local" class="field-input" id="publish-at-input" value="${publishAtInput}" style="flex:1;font-size:10px;padding:5px 8px;" />
|
|
671
|
+
<button type="button" id="btn-schedule-confirm" style="padding:5px 10px;background:var(--accent);border:none;color:var(--bg0);font-family:var(--mono);font-size:10px;cursor:pointer;border-radius:var(--radius);white-space:nowrap;">Set</button>
|
|
672
|
+
</div>
|
|
673
|
+
<button type="button" class="btn-schedule" id="btn-schedule">${status==='scheduled'?'Reschedule':'Schedule'}</button>
|
|
660
674
|
</div>
|
|
661
675
|
<div class="meta-section">
|
|
662
676
|
<div class="meta-label">Details</div>
|
|
@@ -669,6 +683,7 @@
|
|
|
669
683
|
<select class="field-select" id="status-select">
|
|
670
684
|
<option value="draft" ${status==='draft' ?'selected':''}>Draft</option>
|
|
671
685
|
<option value="published" ${status==='published' ?'selected':''}>Published</option>
|
|
686
|
+
<option value="scheduled" ${status==='scheduled' ?'selected':''}>Scheduled</option>
|
|
672
687
|
</select>
|
|
673
688
|
</div>
|
|
674
689
|
${updatedAt ? `<div class="meta-field"><div class="field-label">Modified</div><div class="field-readonly">${new Date(updatedAt).toLocaleDateString()}</div></div>` : ''}
|
|
@@ -729,9 +744,17 @@
|
|
|
729
744
|
});
|
|
730
745
|
});
|
|
731
746
|
|
|
732
|
-
// Publish / draft buttons
|
|
733
|
-
document.getElementById('btn-publish').addEventListener('click',()=>saveEntry('published'));
|
|
734
|
-
document.getElementById('btn-draft').addEventListener('click',()=>saveEntry('draft'));
|
|
747
|
+
// Publish / draft / schedule buttons
|
|
748
|
+
document.getElementById('btn-publish').addEventListener('click', () => saveEntry('published'));
|
|
749
|
+
document.getElementById('btn-draft').addEventListener('click', () => saveEntry('draft'));
|
|
750
|
+
document.getElementById('btn-schedule').addEventListener('click', () => {
|
|
751
|
+
document.getElementById('schedule-picker').classList.toggle('open');
|
|
752
|
+
});
|
|
753
|
+
document.getElementById('btn-schedule-confirm').addEventListener('click', () => {
|
|
754
|
+
const val = document.getElementById('publish-at-input').value;
|
|
755
|
+
if (!val) { alert('Please select a date and time.'); return; }
|
|
756
|
+
saveEntry('scheduled');
|
|
757
|
+
});
|
|
735
758
|
|
|
736
759
|
// Slug input โ slug preview
|
|
737
760
|
document.getElementById('slug-input').addEventListener('input', e=>{
|
|
@@ -802,8 +825,15 @@
|
|
|
802
825
|
function updateStatusUI(status) {
|
|
803
826
|
document.getElementById('status-dot').className = 'status-dot ' + status;
|
|
804
827
|
document.getElementById('status-lbl').className = 'status-lbl ' + status;
|
|
805
|
-
document.getElementById('status-lbl').textContent = status==='published'?'Published':'Draft';
|
|
806
|
-
document.getElementById('status-select')
|
|
828
|
+
document.getElementById('status-lbl').textContent = status === 'published' ? 'Published' : status === 'scheduled' ? 'Scheduled' : 'Draft';
|
|
829
|
+
const sel = document.getElementById('status-select');
|
|
830
|
+
if (sel) sel.value = status;
|
|
831
|
+
const publishBtn = document.getElementById('btn-publish');
|
|
832
|
+
const draftBtn = document.getElementById('btn-draft');
|
|
833
|
+
const schedBtn = document.getElementById('btn-schedule');
|
|
834
|
+
if (publishBtn) publishBtn.textContent = status === 'published' ? 'Republish' : 'Publish now';
|
|
835
|
+
if (draftBtn) draftBtn.textContent = status === 'scheduled' ? 'Cancel schedule' : 'Save as draft';
|
|
836
|
+
if (schedBtn) schedBtn.textContent = status === 'scheduled' ? 'Reschedule' : 'Schedule';
|
|
807
837
|
}
|
|
808
838
|
|
|
809
839
|
function renderTags(key) {
|
|
@@ -880,6 +910,14 @@
|
|
|
880
910
|
let currentPath = location.pathname + location.search;
|
|
881
911
|
|
|
882
912
|
async function saveEntry(status, isAutosave=false) {
|
|
913
|
+
// Resolve publish_at for scheduled entries
|
|
914
|
+
let publish_at = null;
|
|
915
|
+
if (status === 'scheduled') {
|
|
916
|
+
const raw = document.getElementById('publish-at-input')?.value;
|
|
917
|
+
if (!raw) { if (!isAutosave) alert('Please select a date and time.'); return; }
|
|
918
|
+
publish_at = raw.replace('T', ' ') + ':00';
|
|
919
|
+
}
|
|
920
|
+
|
|
883
921
|
const title = document.getElementById('title-input').value;
|
|
884
922
|
syncToHidden();
|
|
885
923
|
const body = document.getElementById('body-input').value;
|
|
@@ -907,7 +945,7 @@
|
|
|
907
945
|
const res = await fetch(`/api/collections/${COLLECTION}/entries`,{
|
|
908
946
|
method:'POST', credentials:'include',
|
|
909
947
|
headers:{'Content-Type':'application/json'},
|
|
910
|
-
body: JSON.stringify({ slug, data, status }),
|
|
948
|
+
body: JSON.stringify({ slug, data, status, publish_at }),
|
|
911
949
|
});
|
|
912
950
|
const json = await res.json();
|
|
913
951
|
if (json.slug || json.id) {
|
|
@@ -926,7 +964,7 @@
|
|
|
926
964
|
const res = await fetch(`/api/collections/${COLLECTION}/entries/${targetSlug}`,{
|
|
927
965
|
method:'PUT', credentials:'include',
|
|
928
966
|
headers:{'Content-Type':'application/json'},
|
|
929
|
-
body: JSON.stringify({ slug, data, status }),
|
|
967
|
+
body: JSON.stringify({ slug, data, status, publish_at }),
|
|
930
968
|
});
|
|
931
969
|
if (res.ok) {
|
|
932
970
|
const json = await res.json();
|
package/public/entries.html
CHANGED
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
<button class="filter-tab active" data-status="">All</button>
|
|
101
101
|
<button class="filter-tab" data-status="published">Published</button>
|
|
102
102
|
<button class="filter-tab" data-status="draft">Drafts</button>
|
|
103
|
+
<button class="filter-tab" data-status="scheduled">Scheduled</button>
|
|
103
104
|
<button class="filter-tab" data-status="trash" style="margin-left:auto">๐ Trash</button>
|
|
104
105
|
</div>
|
|
105
106
|
|
|
@@ -187,8 +188,9 @@
|
|
|
187
188
|
const title = e.data?.title || e.slug;
|
|
188
189
|
const updated = e.updated_at ? e.updated_at.split(' ')[0] : 'โ';
|
|
189
190
|
const deleted = e.deleted_at ? e.deleted_at.split(' ')[0] : 'โ';
|
|
190
|
-
const nextStatus
|
|
191
|
-
const toggleLabel = e.status === 'published' ? 'Unpublish' : 'Publish';
|
|
191
|
+
const nextStatus = e.status === 'published' ? 'draft' : 'published';
|
|
192
|
+
const toggleLabel = e.status === 'published' ? 'Unpublish' : e.status === 'scheduled' ? 'Publish now' : 'Publish';
|
|
193
|
+
const schedInfo = e.status === 'scheduled' && e.publish_at ? ` ยท ${e.publish_at.split(' ')[0]}` : '';
|
|
192
194
|
return `<tr data-slug="${e.slug}"${canSort ? ' draggable="true"' : ''}>
|
|
193
195
|
${canSort ? `<td class="drag-col"><span class="drag-handle" title="Drag to reorder">โ ฟ</span></td>` : ''}
|
|
194
196
|
<td class="cb-col"><input type="checkbox" class="row-cb" data-slug="${e.slug}" ${selected.has(e.slug) ? 'checked' : ''} /></td>
|
|
@@ -196,7 +198,7 @@
|
|
|
196
198
|
<div style="color:var(--heading);font-weight:500">${title}</div>
|
|
197
199
|
<div style="font-family:var(--mono);font-size:10px;color:var(--muted);margin-top:2px">${e.slug}</div>
|
|
198
200
|
</td>
|
|
199
|
-
<td>${isTrash ? `<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${deleted}</span>` : `<span class="badge badge-${e.status}">${e.status}</span>`}</td>
|
|
201
|
+
<td>${isTrash ? `<span style="font-family:var(--mono);font-size:11px;color:var(--muted)">${deleted}</span>` : `<span class="badge badge-${e.status}">${e.status}${schedInfo}</span>`}</td>
|
|
200
202
|
<td style="font-family:var(--mono);font-size:11px;color:var(--muted)">${isTrash ? '' : updated}</td>
|
|
201
203
|
<td style="width:1%;white-space:nowrap">
|
|
202
204
|
<div class="row-actions">
|
package/public/style.css
CHANGED
|
@@ -936,8 +936,9 @@ td { padding: 10px 20px; font-size: 12px; color: var(--text); border-bottom: 1px
|
|
|
936
936
|
tr:last-child td { border-bottom: none; }
|
|
937
937
|
tr:hover td { background: var(--row-hover); }
|
|
938
938
|
.badge { display: inline-block; font-size: 10px; font-family: var(--mono); padding: 2px 7px; border-radius: 2px; letter-spacing: 0.04em; }
|
|
939
|
-
.badge-published
|
|
940
|
-
.badge-draft
|
|
939
|
+
.badge-published { background: var(--jade-bg); color: var(--jade); }
|
|
940
|
+
.badge-draft { background: var(--gold-bg); color: var(--gold); }
|
|
941
|
+
.badge-scheduled { background: var(--accent-bg); color: var(--accent); }
|
|
941
942
|
|
|
942
943
|
/* โโ Buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
|
|
943
944
|
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 12px; font-family: var(--sans); border-radius: var(--radius); cursor: pointer; transition: all 0.12s; border: 1px solid transparent; font-weight: 400; letter-spacing: 0.01em; }
|
package/src/routes/entries.js
CHANGED
|
@@ -99,6 +99,8 @@ entryRoutes.put('/:collectionId/entries/:slug', async (c) => {
|
|
|
99
99
|
db.logAudit(updated.id, username, 'publish');
|
|
100
100
|
} else if (body.status === 'draft' && before?.status === 'published') {
|
|
101
101
|
db.logAudit(updated.id, username, 'unpublish');
|
|
102
|
+
} else if (body.status === 'scheduled' && before?.status !== 'scheduled') {
|
|
103
|
+
db.logAudit(updated.id, username, 'schedule');
|
|
102
104
|
} else {
|
|
103
105
|
db.logAudit(updated.id, username, 'update');
|
|
104
106
|
}
|
|
@@ -186,12 +188,16 @@ entryRoutes.post('/:collectionId/entries/:slug/duplicate', (c) => {
|
|
|
186
188
|
// PATCH /api/collections/:id/entries/:slug/status
|
|
187
189
|
entryRoutes.patch('/:collectionId/entries/:slug/status', async (c) => {
|
|
188
190
|
const { collectionId, slug } = c.req.param();
|
|
189
|
-
const { status } = await c.req.json();
|
|
190
|
-
if (!['draft', 'published'].includes(status)) return c.json({ error: 'Invalid status' }, 400);
|
|
191
|
+
const { status, publish_at } = await c.req.json();
|
|
192
|
+
if (!['draft', 'published', 'scheduled'].includes(status)) return c.json({ error: 'Invalid status' }, 400);
|
|
193
|
+
if (status === 'scheduled' && !publish_at) return c.json({ error: 'publish_at required for scheduled status' }, 400);
|
|
191
194
|
const db = openPod(c.get('podPath'));
|
|
192
195
|
const entry = db.getEntry(collectionId, slug);
|
|
193
196
|
if (!entry) { db.close(); return c.json({ error: 'Not found' }, 404); }
|
|
194
|
-
|
|
197
|
+
const pa = status === 'scheduled' ? publish_at : null;
|
|
198
|
+
db.updateEntry(collectionId, slug, { slug, data: entry.data, status, publish_at: pa });
|
|
199
|
+
const username = c.get('user')?.username ?? 'unknown';
|
|
200
|
+
if (status === 'scheduled') db.logAudit(entry.id, username, 'schedule');
|
|
195
201
|
db.close();
|
|
196
202
|
if (status === 'published') fireWebhook(c.get('podPath'));
|
|
197
203
|
return c.json({ ok: true });
|
package/src/server.js
CHANGED
|
@@ -93,3 +93,24 @@ serve({ fetch: createApp(POD_PATH).fetch, port: PORT }, () => {
|
|
|
93
93
|
console.log(`Orbiter Admin API โ http://localhost:${PORT}`);
|
|
94
94
|
console.log(`Pod: ${POD_PATH}`);
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
// Scheduled publishing โ check every 60 s
|
|
98
|
+
setInterval(() => {
|
|
99
|
+
try {
|
|
100
|
+
const db = openPod(POD_PATH);
|
|
101
|
+
const due = db.getScheduledDue();
|
|
102
|
+
if (!due.length) { db.close(); return; }
|
|
103
|
+
const now = new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
|
|
104
|
+
for (const entry of due) {
|
|
105
|
+
db.db.prepare("UPDATE _entries SET status = 'published', publish_at = NULL, updated_at = ? WHERE id = ?").run(now, entry.id);
|
|
106
|
+
db.logAudit(entry.id, 'scheduler', 'publish');
|
|
107
|
+
}
|
|
108
|
+
const webhookUrl = db.getMeta('build.webhook_url') ?? '';
|
|
109
|
+
if (webhookUrl) db.setMeta('build.last_triggered', new Date().toISOString());
|
|
110
|
+
db.close();
|
|
111
|
+
console.log(`[scheduler] Published ${due.length} scheduled entr${due.length === 1 ? 'y' : 'ies'}`);
|
|
112
|
+
if (webhookUrl) fetch(webhookUrl, { method: 'POST' }).catch(() => {});
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.warn('[scheduler]', e.message);
|
|
115
|
+
}
|
|
116
|
+
}, 60_000);
|