@a83/orbiter-admin 0.3.12 → 0.3.13
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 +46 -3
- package/src/routes/entries.js +4 -3
- package/src/server.js +9 -3
package/package.json
CHANGED
package/public/editor.html
CHANGED
|
@@ -116,6 +116,10 @@
|
|
|
116
116
|
.btn-schedule:hover { background:var(--accent-bg); }
|
|
117
117
|
.schedule-picker { display:none; gap:4px; margin-bottom:5px; }
|
|
118
118
|
.schedule-picker.open { display:flex; }
|
|
119
|
+
.expiry-picker { display:none; gap:4px; margin-bottom:5px; }
|
|
120
|
+
.expiry-picker.open { display:flex; }
|
|
121
|
+
.btn-expiry { display:block; width:100%; padding:8px; background:transparent; border:1px solid var(--line); color:var(--muted); font-family:var(--mono); font-size:10px; cursor:pointer; transition:all .12s; border-radius:var(--radius); margin-bottom:5px; }
|
|
122
|
+
.btn-expiry:hover { border-color:var(--mid); color:var(--text); }
|
|
119
123
|
|
|
120
124
|
/* Schema fields in sidebar */
|
|
121
125
|
.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; }
|
|
@@ -399,6 +403,7 @@
|
|
|
399
403
|
<button type="button" class="view-btn" id="vbtn-preview" onclick="setViewMode('preview')">Preview</button>
|
|
400
404
|
</div>
|
|
401
405
|
<a id="btn-preview-link" href="#" target="_blank" rel="noopener" title="Open preview" style="display:none;font-size:11px;color:var(--muted);text-decoration:none;padding:4px 8px;border:1px solid var(--line);border-radius:var(--radius);">↗ Preview</a>
|
|
406
|
+
<button class="search-trigger" id="search-btn" title="Search (⌘K)" style="background:none;border:none;color:var(--muted);cursor:pointer;padding:4px 8px;font-size:13px;">⌘K</button>
|
|
402
407
|
<button class="scheme-toggle" id="scheme-toggle" title="Toggle scheme">◐</button>
|
|
403
408
|
<span class="user" id="topbar-user"></span>
|
|
404
409
|
<span class="logout" id="logout-btn">Sign out</span>
|
|
@@ -563,8 +568,10 @@
|
|
|
563
568
|
const panel = document.getElementById('meta-panel');
|
|
564
569
|
const status = IS_NEW ? 'draft' : (entryData?.status ?? 'draft');
|
|
565
570
|
const updatedAt = entryData?.updated_at;
|
|
566
|
-
const publishAt
|
|
567
|
-
const
|
|
571
|
+
const publishAt = entryData?.publish_at ?? null;
|
|
572
|
+
const unpublishAt = entryData?.unpublish_at ?? null;
|
|
573
|
+
const publishAtInput = publishAt ? publishAt.replace(' ', 'T').slice(0, 16) : '';
|
|
574
|
+
const unpublishAtInput = unpublishAt ? unpublishAt.replace(' ', 'T').slice(0, 16) : '';
|
|
568
575
|
|
|
569
576
|
let fieldsHtml = '';
|
|
570
577
|
const seoFields = {};
|
|
@@ -688,6 +695,13 @@
|
|
|
688
695
|
<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>
|
|
689
696
|
</div>
|
|
690
697
|
<button type="button" class="btn-schedule" id="btn-schedule">${status==='scheduled'?'Reschedule':'Schedule'}</button>
|
|
698
|
+
${status==='published'?`
|
|
699
|
+
<div class="expiry-picker${unpublishAt?' open':''}" id="expiry-picker">
|
|
700
|
+
<input type="datetime-local" class="field-input" id="unpublish-at-input" value="${unpublishAtInput}" style="flex:1;font-size:10px;padding:5px 8px;" />
|
|
701
|
+
<button type="button" id="btn-expiry-confirm" style="padding:5px 10px;background:var(--bg3);border:1px solid var(--line);color:var(--text);font-family:var(--mono);font-size:10px;cursor:pointer;border-radius:var(--radius);white-space:nowrap;">Set</button>
|
|
702
|
+
</div>
|
|
703
|
+
<button type="button" class="btn-expiry" id="btn-expiry">${unpublishAt?'Change expiry':'Set expiry'}</button>
|
|
704
|
+
`:''}
|
|
691
705
|
</div>
|
|
692
706
|
<div class="meta-section">
|
|
693
707
|
<div class="meta-label">Details</div>
|
|
@@ -790,6 +804,12 @@
|
|
|
790
804
|
if (!val) { alert('Please select a date and time.'); return; }
|
|
791
805
|
saveEntry('scheduled');
|
|
792
806
|
});
|
|
807
|
+
document.getElementById('btn-expiry')?.addEventListener('click', () => {
|
|
808
|
+
document.getElementById('expiry-picker').classList.toggle('open');
|
|
809
|
+
});
|
|
810
|
+
document.getElementById('btn-expiry-confirm')?.addEventListener('click', () => {
|
|
811
|
+
saveEntry('published');
|
|
812
|
+
});
|
|
793
813
|
|
|
794
814
|
// Version restore buttons
|
|
795
815
|
panel.querySelectorAll('.v-restore-btn').forEach(btn => {
|
|
@@ -1006,6 +1026,12 @@
|
|
|
1006
1026
|
if (!raw) { if (!isAutosave) alert('Please select a date and time.'); return; }
|
|
1007
1027
|
publish_at = raw.replace('T', ' ') + ':00';
|
|
1008
1028
|
}
|
|
1029
|
+
// Resolve unpublish_at for published entries with expiry
|
|
1030
|
+
let unpublish_at = null;
|
|
1031
|
+
if (status === 'published') {
|
|
1032
|
+
const raw = document.getElementById('unpublish-at-input')?.value;
|
|
1033
|
+
unpublish_at = raw ? raw.replace('T', ' ') + ':00' : null;
|
|
1034
|
+
}
|
|
1009
1035
|
|
|
1010
1036
|
const title = document.getElementById('title-input').value;
|
|
1011
1037
|
syncToHidden();
|
|
@@ -1029,6 +1055,22 @@
|
|
|
1029
1055
|
}
|
|
1030
1056
|
}
|
|
1031
1057
|
|
|
1058
|
+
// Required field validation (skip on autosave)
|
|
1059
|
+
if (!isAutosave) {
|
|
1060
|
+
const missing = extraFields
|
|
1061
|
+
.filter(([key, field]) => {
|
|
1062
|
+
if (!field.required) return false;
|
|
1063
|
+
const val = data[key];
|
|
1064
|
+
if (Array.isArray(val)) return val.length === 0;
|
|
1065
|
+
return val === '' || val === null || val === undefined;
|
|
1066
|
+
})
|
|
1067
|
+
.map(([key, field]) => field.label || key);
|
|
1068
|
+
if (missing.length) {
|
|
1069
|
+
alert(`Required field${missing.length > 1 ? 's' : ''} missing: ${missing.join(', ')}`);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1032
1074
|
if (IS_NEW && !currentSlug) {
|
|
1033
1075
|
// Create new entry
|
|
1034
1076
|
const res = await fetch(`/api/collections/${COLLECTION}/entries`,{
|
|
@@ -1053,7 +1095,7 @@
|
|
|
1053
1095
|
const res = await fetch(`/api/collections/${COLLECTION}/entries/${targetSlug}`,{
|
|
1054
1096
|
method:'PUT', credentials:'include',
|
|
1055
1097
|
headers:{'Content-Type':'application/json'},
|
|
1056
|
-
body: JSON.stringify({ slug, data, status, publish_at }),
|
|
1098
|
+
body: JSON.stringify({ slug, data, status, publish_at, unpublish_at }),
|
|
1057
1099
|
});
|
|
1058
1100
|
if (res.ok) {
|
|
1059
1101
|
const json = await res.json();
|
|
@@ -1941,6 +1983,7 @@
|
|
|
1941
1983
|
<input type="file" id="img-file-inp" accept="image/*" style="display:none">
|
|
1942
1984
|
|
|
1943
1985
|
<script src="/sidebar.js"></script>
|
|
1986
|
+
<script src="/search.js"></script>
|
|
1944
1987
|
<script src="/router.js"></script>
|
|
1945
1988
|
</body>
|
|
1946
1989
|
</html>
|
package/src/routes/entries.js
CHANGED
|
@@ -274,14 +274,15 @@ entryRoutes.post('/:collectionId/entries/:slug/duplicate', (c) => {
|
|
|
274
274
|
// PATCH /api/collections/:id/entries/:slug/status
|
|
275
275
|
entryRoutes.patch('/:collectionId/entries/:slug/status', async (c) => {
|
|
276
276
|
const { collectionId, slug } = c.req.param();
|
|
277
|
-
const { status, publish_at } = await c.req.json();
|
|
277
|
+
const { status, publish_at, unpublish_at } = await c.req.json();
|
|
278
278
|
if (!['draft', 'published', 'scheduled'].includes(status)) return c.json({ error: 'Invalid status' }, 400);
|
|
279
279
|
if (status === 'scheduled' && !publish_at) return c.json({ error: 'publish_at required for scheduled status' }, 400);
|
|
280
280
|
const db = openPod(c.get('podPath'));
|
|
281
281
|
const entry = db.getEntry(collectionId, slug);
|
|
282
282
|
if (!entry) { db.close(); return c.json({ error: 'Not found' }, 404); }
|
|
283
|
-
const pa
|
|
284
|
-
|
|
283
|
+
const pa = status === 'scheduled' ? publish_at : null;
|
|
284
|
+
const ua = status === 'published' ? (unpublish_at ?? null) : null;
|
|
285
|
+
db.updateEntry(collectionId, slug, { slug, data: entry.data, status, publish_at: pa, unpublish_at: ua });
|
|
285
286
|
const username = c.get('user')?.username ?? 'unknown';
|
|
286
287
|
if (status === 'scheduled') db.logAudit(entry.id, username, 'schedule');
|
|
287
288
|
db.close();
|
package/src/server.js
CHANGED
|
@@ -101,17 +101,23 @@ serve({ fetch: createApp(POD_PATH).fetch, port: PORT }, () => {
|
|
|
101
101
|
setInterval(() => {
|
|
102
102
|
try {
|
|
103
103
|
const db = openPod(POD_PATH);
|
|
104
|
-
const due
|
|
105
|
-
|
|
104
|
+
const due = db.getScheduledDue();
|
|
105
|
+
const expired = db.getExpiredDue();
|
|
106
|
+
if (!due.length && !expired.length) { db.close(); return; }
|
|
106
107
|
const now = new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
|
|
107
108
|
for (const entry of due) {
|
|
108
109
|
db.db.prepare("UPDATE _entries SET status = 'published', publish_at = NULL, updated_at = ? WHERE id = ?").run(now, entry.id);
|
|
109
110
|
db.logAudit(entry.id, 'scheduler', 'publish');
|
|
110
111
|
}
|
|
112
|
+
for (const entry of expired) {
|
|
113
|
+
db.db.prepare("UPDATE _entries SET status = 'draft', unpublish_at = NULL, updated_at = ? WHERE id = ?").run(now, entry.id);
|
|
114
|
+
db.logAudit(entry.id, 'scheduler', 'unpublish');
|
|
115
|
+
}
|
|
111
116
|
const webhookUrl = db.getMeta('build.webhook_url') ?? '';
|
|
112
117
|
if (webhookUrl) db.setMeta('build.last_triggered', new Date().toISOString());
|
|
113
118
|
db.close();
|
|
114
|
-
console.log(`[scheduler] Published ${due.length} scheduled entr${due.length === 1 ? 'y' : 'ies'}`);
|
|
119
|
+
if (due.length) console.log(`[scheduler] Published ${due.length} scheduled entr${due.length === 1 ? 'y' : 'ies'}`);
|
|
120
|
+
if (expired.length) console.log(`[scheduler] Unpublished ${expired.length} expired entr${expired.length === 1 ? 'y' : 'ies'}`);
|
|
115
121
|
if (webhookUrl) fetch(webhookUrl, { method: 'POST' }).catch(() => {});
|
|
116
122
|
} catch (e) {
|
|
117
123
|
console.warn('[scheduler]', e.message);
|