@a83/orbiter-admin 0.2.0
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/README.md +115 -0
- package/package.json +33 -0
- package/public/admin-utils.js +302 -0
- package/public/build.html +129 -0
- package/public/collections.html +100 -0
- package/public/dashboard.html +478 -0
- package/public/editor.html +1569 -0
- package/public/entries.html +367 -0
- package/public/favicon.svg +6 -0
- package/public/import.html +514 -0
- package/public/login.html +76 -0
- package/public/media.html +233 -0
- package/public/router.js +142 -0
- package/public/schema.html +366 -0
- package/public/search.js +209 -0
- package/public/settings.html +688 -0
- package/public/sidebar.js +90 -0
- package/public/style.css +1020 -0
- package/public/theme.js +63 -0
- package/public/users.html +192 -0
- package/src/index.js +4 -0
- package/src/middleware/auth.js +20 -0
- package/src/routes/account.js +41 -0
- package/src/routes/auth.js +55 -0
- package/src/routes/build.js +25 -0
- package/src/routes/collections.js +65 -0
- package/src/routes/entries.js +103 -0
- package/src/routes/github.js +133 -0
- package/src/routes/import.js +120 -0
- package/src/routes/info.js +19 -0
- package/src/routes/media.js +95 -0
- package/src/routes/meta.js +54 -0
- package/src/routes/search.js +62 -0
- package/src/routes/users.js +46 -0
- package/src/server.js +85 -0
- package/src/wp-importer.js +299 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @a83/orbiter-admin
|
|
2
|
+
|
|
3
|
+
Standalone admin UI for Orbiter CMS. A self-contained Node.js server — no Astro required.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
Orbiter stores everything in a single `.pod` file (SQLite). This package is the admin interface: a Hono HTTP server that reads and writes that file. It runs independently from your public site, on its own port or subdomain.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
content.pod ← shared file
|
|
13
|
+
↑ ↑
|
|
14
|
+
orbiter-admin orbiter-integration
|
|
15
|
+
(this package) (Astro, reads at build time)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
ORBITER_POD=/absolute/path/to/content.pod npm start
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Development (auto-reload on changes):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ORBITER_POD=/absolute/path/to/content.pod npm run dev
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Opens at **http://localhost:4322**
|
|
33
|
+
|
|
34
|
+
> Use an absolute path. The server changes its working directory internally, so relative paths break.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Demo
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# From the monorepo root
|
|
42
|
+
npm run seed
|
|
43
|
+
ORBITER_POD=$(pwd)/apps/demo/demo.pod npm run dev --workspace=packages/admin
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Login: `admin` / `admin`
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Env vars
|
|
51
|
+
|
|
52
|
+
| Variable | Required | Default | Description |
|
|
53
|
+
|-----------------|----------|---------------|------------------------------------------|
|
|
54
|
+
| `ORBITER_POD` | yes | — | Absolute path to the `.pod` file |
|
|
55
|
+
| `PORT` | no | `4322` | HTTP port |
|
|
56
|
+
| `ADMIN_ORIGIN` | no | `localhost:4321,localhost:4322` | Allowed CORS origins (comma-separated) |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## What's inside
|
|
61
|
+
|
|
62
|
+
**Dashboard** — entry counts, recently edited content, notes, to-do list, build trigger.
|
|
63
|
+
|
|
64
|
+
**Collections** — browse and manage entries per collection.
|
|
65
|
+
|
|
66
|
+
**Editor** — all schema fields rendered as inputs, autosave, version history, draft/published toggle. Inline image blocks with float alignment (left/right/center/full). Video embedding — YouTube, Vimeo, Wistia, direct mp4/webm URL. Media picker with cloud URL import (Dropbox, Google Drive, OneDrive). Relation picker, conditional field visibility.
|
|
67
|
+
|
|
68
|
+
**Media library** — upload, browse and manage files. Stored as BLOBs in the pod. Images, video, PDF, any file type.
|
|
69
|
+
|
|
70
|
+
**Schema** — add, reorder, and remove fields on any collection. Live changes, no restart.
|
|
71
|
+
|
|
72
|
+
**Settings** — site name, URL, locale, API token, build webhook, theme.
|
|
73
|
+
|
|
74
|
+
**Users** — manage admin users (admin role only).
|
|
75
|
+
|
|
76
|
+
**Import** — WordPress WXR importer.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Themes
|
|
81
|
+
|
|
82
|
+
Three themes, two schemes (dark / light), two layouts (classic / glass).
|
|
83
|
+
|
|
84
|
+
| Theme | Character |
|
|
85
|
+
|-------------|----------------------------------------|
|
|
86
|
+
| Space | Dark: space station HUD — cyan + blue |
|
|
87
|
+
| | Light: solar command — ice blue |
|
|
88
|
+
| Zen | Japandi — slate, mauve, moss |
|
|
89
|
+
| Catppuccin | Mocha (dark) / Latte (light) |
|
|
90
|
+
|
|
91
|
+
Glass layout is the default — frosted panels, backdrop blur, animated gradient background.
|
|
92
|
+
|
|
93
|
+
Preference saved to `localStorage`.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Health check
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
curl http://localhost:4322/health
|
|
101
|
+
# {"ok":true,"pod":"/path/to/content.pod"}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Part of Orbiter
|
|
107
|
+
|
|
108
|
+
| Package | Description |
|
|
109
|
+
|---------|-------------|
|
|
110
|
+
| `@a83/orbiter-core` | SQLite engine, pod management, auth |
|
|
111
|
+
| `@a83/orbiter-integration` | Astro integration, `orbiter:collections` virtual module |
|
|
112
|
+
| `@a83/orbiter-admin` | This package — standalone admin server |
|
|
113
|
+
| `@a83/orbiter-cli` | `orbiter init`, `add-user`, `export`, `pack`, `unpack` |
|
|
114
|
+
|
|
115
|
+
**orbiter.sh** · MIT · [github.com/aeon022/orbiter](https://github.com/aeon022/orbiter)
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a83/orbiter-admin",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Standalone admin server for Orbiter CMS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/server.js",
|
|
9
|
+
"dev": "node --watch src/server.js"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20.0.0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"astro",
|
|
16
|
+
"cms",
|
|
17
|
+
"sqlite",
|
|
18
|
+
"headless-cms",
|
|
19
|
+
"orbiter"
|
|
20
|
+
],
|
|
21
|
+
"author": "ABTEILUNG83",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/aeon022/orbiter.git",
|
|
26
|
+
"directory": "packages/admin"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@a83/orbiter-core": "^0.2.0",
|
|
30
|
+
"@hono/node-server": "^1.14.4",
|
|
31
|
+
"hono": "^4.7.11"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Orbiter Admin Utilities — port of packages/integration/src/admin-utils.js
|
|
2
|
+
// Dark mode (html.dark class) + Space theme (html.theme-space class) + Command Palette
|
|
3
|
+
|
|
4
|
+
(function () {
|
|
5
|
+
|
|
6
|
+
// ── Dark mode ────────────────────────────────────────────────────────
|
|
7
|
+
if (localStorage.getItem('orb-dark') === '1') {
|
|
8
|
+
document.documentElement.classList.add('dark');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function darkLabel(isDark) { return isDark ? '○ light' : '● dark'; }
|
|
12
|
+
|
|
13
|
+
window.__orbToggleDark = function () {
|
|
14
|
+
var dark = document.documentElement.classList.toggle('dark');
|
|
15
|
+
localStorage.setItem('orb-dark', dark ? '1' : '0');
|
|
16
|
+
var btn = document.getElementById('orb-dark-btn');
|
|
17
|
+
if (btn) btn.textContent = darkLabel(dark);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function syncDarkBtn() {
|
|
21
|
+
var btn = document.getElementById('orb-dark-btn');
|
|
22
|
+
if (btn) btn.textContent = darkLabel(document.documentElement.classList.contains('dark'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Theme ────────────────────────────────────────────────────────────
|
|
26
|
+
var THEMES = ['space'];
|
|
27
|
+
|
|
28
|
+
var _savedTheme = localStorage.getItem('orb-theme') || '';
|
|
29
|
+
if (_savedTheme) document.documentElement.classList.add('theme-' + _savedTheme);
|
|
30
|
+
|
|
31
|
+
if (_savedTheme === 'space' && !document.querySelector('link[href*="Space+Mono"]')) {
|
|
32
|
+
var _fl = document.createElement('link');
|
|
33
|
+
_fl.rel = 'stylesheet';
|
|
34
|
+
_fl.href = 'https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap';
|
|
35
|
+
document.head.appendChild(_fl);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
window.__orbSetTheme = function (theme) {
|
|
39
|
+
THEMES.forEach(function (t) { document.documentElement.classList.remove('theme-' + t); });
|
|
40
|
+
localStorage.setItem('orb-theme', theme);
|
|
41
|
+
if (theme) document.documentElement.classList.add('theme-' + theme);
|
|
42
|
+
if (theme === 'space' && !document.querySelector('link[href*="Space+Mono"]')) {
|
|
43
|
+
var fl2 = document.createElement('link');
|
|
44
|
+
fl2.rel = 'stylesheet';
|
|
45
|
+
fl2.href = 'https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap';
|
|
46
|
+
document.head.appendChild(fl2);
|
|
47
|
+
}
|
|
48
|
+
document.querySelectorAll('[data-theme-btn]').forEach(function (btn) {
|
|
49
|
+
var active = btn.dataset.themeBtn === theme;
|
|
50
|
+
btn.style.borderColor = active ? 'var(--gold)' : 'var(--line)';
|
|
51
|
+
btn.style.color = active ? 'var(--gold)' : 'var(--muted)';
|
|
52
|
+
btn.style.background = active ? 'var(--gold-bg)' : 'transparent';
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
57
|
+
syncDarkBtn();
|
|
58
|
+
|
|
59
|
+
var cur = localStorage.getItem('orb-theme') || '';
|
|
60
|
+
document.querySelectorAll('[data-theme-btn]').forEach(function (btn) {
|
|
61
|
+
var active = btn.dataset.themeBtn === cur;
|
|
62
|
+
btn.style.borderColor = active ? 'var(--gold)' : 'var(--line)';
|
|
63
|
+
btn.style.color = active ? 'var(--gold)' : 'var(--muted)';
|
|
64
|
+
btn.style.background = active ? 'var(--gold-bg)' : 'transparent';
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Sidebar sub-menus ────────────────────────────────────────────────
|
|
69
|
+
window.__orbToggleNav = function (colId) {
|
|
70
|
+
var sub = document.getElementById('nav-sub-' + colId);
|
|
71
|
+
var chev = document.getElementById('chev-' + colId);
|
|
72
|
+
if (!sub) return;
|
|
73
|
+
var open = sub.style.display !== 'none';
|
|
74
|
+
sub.style.display = open ? 'none' : '';
|
|
75
|
+
if (chev) chev.textContent = open ? '▸' : '▾';
|
|
76
|
+
try {
|
|
77
|
+
var state = JSON.parse(localStorage.getItem('orb_nav') || '{}');
|
|
78
|
+
state[colId] = !open;
|
|
79
|
+
localStorage.setItem('orb_nav', JSON.stringify(state));
|
|
80
|
+
} catch {}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
84
|
+
try {
|
|
85
|
+
var state = JSON.parse(localStorage.getItem('orb_nav') || '{}');
|
|
86
|
+
Object.keys(state).forEach(function (colId) {
|
|
87
|
+
var sub = document.getElementById('nav-sub-' + colId);
|
|
88
|
+
var chev = document.getElementById('chev-' + colId);
|
|
89
|
+
if (!sub) return;
|
|
90
|
+
if (!state[colId] && sub.querySelector('.active')) return;
|
|
91
|
+
sub.style.display = state[colId] ? '' : 'none';
|
|
92
|
+
if (chev) chev.textContent = state[colId] ? '▾' : '▸';
|
|
93
|
+
});
|
|
94
|
+
} catch {}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Sign out ─────────────────────────────────────────────────────────
|
|
98
|
+
window.__orbSignOut = async function () {
|
|
99
|
+
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }).catch(function () {});
|
|
100
|
+
window.location.replace('/login.html');
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ── Breadcrumbs ───────────────────────────────────────────────────────
|
|
104
|
+
window.__orbBreadcrumbs = function (crumbs) {
|
|
105
|
+
var el = document.getElementById('topbar-breadcrumb');
|
|
106
|
+
if (!el || !crumbs.length) return;
|
|
107
|
+
el.innerHTML = crumbs.map(function (c, i) {
|
|
108
|
+
var sep = i > 0 ? '<span class="sep">/</span>' : '';
|
|
109
|
+
var item = c.href
|
|
110
|
+
? '<a href="' + c.href + '">' + c.label + '</a>'
|
|
111
|
+
: '<span class="current">' + c.label + '</span>';
|
|
112
|
+
return sep + item;
|
|
113
|
+
}).join('');
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ── Command Palette ──────────────────────────────────────────────────
|
|
117
|
+
var palette = null;
|
|
118
|
+
var palInput = null;
|
|
119
|
+
var palResults = null;
|
|
120
|
+
var palOpen = false;
|
|
121
|
+
var palIdx = -1;
|
|
122
|
+
var palTimer = null;
|
|
123
|
+
|
|
124
|
+
var NAV = [
|
|
125
|
+
{ title: 'Dashboard', href: '/dashboard.html', hint: 'nav' },
|
|
126
|
+
{ title: 'Media', href: '/media.html', hint: 'nav' },
|
|
127
|
+
{ title: 'Schema', href: '/schema.html', hint: 'nav' },
|
|
128
|
+
{ title: 'Build', href: '/build.html', hint: 'nav' },
|
|
129
|
+
{ title: 'Settings', href: '/settings.html', hint: 'nav' },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
function buildPalette() {
|
|
133
|
+
palette = document.createElement('div');
|
|
134
|
+
palette.style.cssText = 'position:fixed;inset:0;z-index:9000;background:rgba(0,0,0,0.5);display:flex;align-items:flex-start;justify-content:center;padding-top:15vh;';
|
|
135
|
+
|
|
136
|
+
var box = document.createElement('div');
|
|
137
|
+
box.style.cssText = 'width:520px;max-width:calc(100vw - 32px);background:var(--bg2);border:1px solid var(--line);box-shadow:0 20px 60px rgba(0,0,0,0.2);font-family:var(--mono);overflow:hidden;';
|
|
138
|
+
|
|
139
|
+
var inputRow = document.createElement('div');
|
|
140
|
+
inputRow.style.cssText = 'display:flex;align-items:center;padding:0 16px;border-bottom:1px solid var(--line);';
|
|
141
|
+
|
|
142
|
+
var prompt = document.createElement('span');
|
|
143
|
+
prompt.textContent = '›';
|
|
144
|
+
prompt.style.cssText = 'color:var(--muted);font-size:14px;margin-right:10px;flex-shrink:0;';
|
|
145
|
+
|
|
146
|
+
palInput = document.createElement('input');
|
|
147
|
+
palInput.type = 'text';
|
|
148
|
+
palInput.placeholder = 'Search, navigate…';
|
|
149
|
+
palInput.style.cssText = 'flex:1;border:none;outline:none;background:transparent;padding:14px 0;font-size:13px;color:var(--text);font-family:var(--mono);';
|
|
150
|
+
|
|
151
|
+
var hint = document.createElement('span');
|
|
152
|
+
hint.textContent = 'esc';
|
|
153
|
+
hint.style.cssText = 'font-size:9px;color:var(--muted);border:1px solid var(--line);padding:2px 6px;letter-spacing:0.06em;';
|
|
154
|
+
|
|
155
|
+
inputRow.appendChild(prompt);
|
|
156
|
+
inputRow.appendChild(palInput);
|
|
157
|
+
inputRow.appendChild(hint);
|
|
158
|
+
|
|
159
|
+
palResults = document.createElement('div');
|
|
160
|
+
palResults.style.cssText = 'max-height:340px;overflow-y:auto;';
|
|
161
|
+
|
|
162
|
+
box.appendChild(inputRow);
|
|
163
|
+
box.appendChild(palResults);
|
|
164
|
+
palette.appendChild(box);
|
|
165
|
+
|
|
166
|
+
palette.addEventListener('mousedown', function (e) { if (e.target === palette) closePalette(); });
|
|
167
|
+
palInput.addEventListener('input', function () { clearTimeout(palTimer); palTimer = setTimeout(runSearch, 120); });
|
|
168
|
+
palInput.addEventListener('keydown', handlePalKey);
|
|
169
|
+
document.body.appendChild(palette);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
window.__orbOpenPalette = openPalette;
|
|
173
|
+
|
|
174
|
+
function openPalette() {
|
|
175
|
+
if (!palette) buildPalette();
|
|
176
|
+
palOpen = true;
|
|
177
|
+
palette.style.display = 'flex';
|
|
178
|
+
palInput.value = '';
|
|
179
|
+
palIdx = -1;
|
|
180
|
+
renderResults(NAV, []);
|
|
181
|
+
setTimeout(function () { palInput.focus(); }, 10);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function closePalette() {
|
|
185
|
+
if (!palette) return;
|
|
186
|
+
palOpen = false;
|
|
187
|
+
palette.style.display = 'none';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handlePalKey(e) {
|
|
191
|
+
if (e.key === 'Escape') { e.preventDefault(); closePalette(); return; }
|
|
192
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); movePal(1); return; }
|
|
193
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); movePal(-1); return; }
|
|
194
|
+
if (e.key === 'Enter') {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
var active = palResults.querySelector('.pal-item.pal-active');
|
|
197
|
+
if (active) window.location.href = active.dataset.href;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function movePal(dir) {
|
|
202
|
+
var items = palResults.querySelectorAll('.pal-item');
|
|
203
|
+
if (!items.length) return;
|
|
204
|
+
palIdx = Math.max(-1, Math.min(items.length - 1, palIdx + dir));
|
|
205
|
+
items.forEach(function (el, i) {
|
|
206
|
+
el.classList.toggle('pal-active', i === palIdx);
|
|
207
|
+
el.style.background = i === palIdx ? 'var(--accent-bg)' : '';
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function palRow(item) {
|
|
212
|
+
var el = document.createElement('a');
|
|
213
|
+
el.href = item.href;
|
|
214
|
+
el.dataset.href = item.href;
|
|
215
|
+
el.className = 'pal-item';
|
|
216
|
+
el.style.cssText = 'display:flex;align-items:center;gap:12px;padding:9px 16px;text-decoration:none;border-bottom:1px solid var(--line2);transition:background 0.08s;cursor:pointer;';
|
|
217
|
+
el.addEventListener('mouseenter', function () {
|
|
218
|
+
palIdx = Array.from(palResults.querySelectorAll('.pal-item')).indexOf(el);
|
|
219
|
+
palResults.querySelectorAll('.pal-item').forEach(function (x) { x.style.background = ''; x.classList.remove('pal-active'); });
|
|
220
|
+
el.style.background = 'var(--accent-bg)';
|
|
221
|
+
el.classList.add('pal-active');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
var icon = document.createElement('div');
|
|
225
|
+
icon.style.cssText = 'width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--muted);flex-shrink:0;';
|
|
226
|
+
icon.textContent = item.hint === 'action' ? '→' : item.hint === 'nav' ? '◈' : item.status === 'published' ? '▪' : '▫';
|
|
227
|
+
|
|
228
|
+
var main = document.createElement('div');
|
|
229
|
+
main.style.cssText = 'flex:1;min-width:0;';
|
|
230
|
+
|
|
231
|
+
var title = document.createElement('div');
|
|
232
|
+
title.style.cssText = 'font-size:12px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
|
|
233
|
+
title.textContent = item.title;
|
|
234
|
+
main.appendChild(title);
|
|
235
|
+
|
|
236
|
+
if (item.collLabel) {
|
|
237
|
+
var sub = document.createElement('div');
|
|
238
|
+
sub.style.cssText = 'font-size:9px;color:var(--muted);margin-top:1px;';
|
|
239
|
+
sub.textContent = item.collLabel + ' · ' + item.slug;
|
|
240
|
+
main.appendChild(sub);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
var badge = document.createElement('div');
|
|
244
|
+
badge.style.cssText = 'font-size:9px;color:var(--muted);flex-shrink:0;';
|
|
245
|
+
badge.textContent = item.hint || item.status || '';
|
|
246
|
+
|
|
247
|
+
el.appendChild(icon);
|
|
248
|
+
el.appendChild(main);
|
|
249
|
+
el.appendChild(badge);
|
|
250
|
+
return el;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderResults(navItems, entryItems) {
|
|
254
|
+
palResults.innerHTML = '';
|
|
255
|
+
palIdx = -1;
|
|
256
|
+
|
|
257
|
+
if (navItems.length) {
|
|
258
|
+
var navSec = document.createElement('div');
|
|
259
|
+
navSec.style.cssText = 'font-size:9px;letter-spacing:0.2em;text-transform:uppercase;color:var(--muted);padding:8px 16px 4px;';
|
|
260
|
+
navSec.textContent = 'Navigation';
|
|
261
|
+
palResults.appendChild(navSec);
|
|
262
|
+
navItems.forEach(function (item) { palResults.appendChild(palRow(item)); });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (entryItems.length) {
|
|
266
|
+
var entrySec = document.createElement('div');
|
|
267
|
+
entrySec.style.cssText = 'font-size:9px;letter-spacing:0.2em;text-transform:uppercase;color:var(--muted);padding:8px 16px 4px;border-top:1px solid var(--line);';
|
|
268
|
+
entrySec.textContent = 'Entries';
|
|
269
|
+
palResults.appendChild(entrySec);
|
|
270
|
+
entryItems.forEach(function (item) { palResults.appendChild(palRow(item)); });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!navItems.length && !entryItems.length) {
|
|
274
|
+
var empty = document.createElement('div');
|
|
275
|
+
empty.style.cssText = 'padding:20px 16px;font-size:11px;color:var(--muted);text-align:center;';
|
|
276
|
+
empty.textContent = 'No results';
|
|
277
|
+
palResults.appendChild(empty);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function runSearch() {
|
|
282
|
+
var q = palInput.value.trim();
|
|
283
|
+
if (!q) { renderResults(NAV, []); return; }
|
|
284
|
+
try {
|
|
285
|
+
var res = await fetch('/api/search?q=' + encodeURIComponent(q), { credentials: 'include' });
|
|
286
|
+
var json = await res.json();
|
|
287
|
+
var navMatches = NAV.filter(function (n) { return n.title.toLowerCase().includes(q.toLowerCase()); });
|
|
288
|
+
renderResults(navMatches, (json.results || []).map(function (r) {
|
|
289
|
+
return { title: r.title || r.slug, href: '/editor.html?collection=' + r.collection_id + '&slug=' + r.slug, collLabel: r.collection_id, slug: r.slug, status: r.status };
|
|
290
|
+
}));
|
|
291
|
+
} catch {}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Cmd+K (Mac) or Ctrl+K (other)
|
|
295
|
+
document.addEventListener('keydown', function (e) {
|
|
296
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
palOpen ? closePalette() : openPalette();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
})();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Orbiter Admin — Build</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Space+Grotesk:wght@300;400;500;600&family=Noto+Serif+JP:wght@200;300&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/style.css" />
|
|
11
|
+
<script src="/theme.js"></script>
|
|
12
|
+
<style>
|
|
13
|
+
.build-card { background:var(--bg2); border:1px solid var(--line); border-radius:var(--radius); padding:28px 32px; max-width:520px; }
|
|
14
|
+
.build-status { display:flex; align-items:center; gap:10px; margin-bottom:20px; }
|
|
15
|
+
.build-dot { width:8px; height:8px; border-radius:50%; background:var(--line); flex-shrink:0; }
|
|
16
|
+
.build-dot.configured { background:var(--jade); }
|
|
17
|
+
.build-dot.unconfigured { background:var(--muted); }
|
|
18
|
+
.build-label { font-size:12px; color:var(--text); }
|
|
19
|
+
.build-meta { font-size:10px; color:var(--muted); margin-bottom:24px; }
|
|
20
|
+
.build-last { font-size:10px; color:var(--muted); margin-top:12px; }
|
|
21
|
+
.banner { padding:8px 16px; font-size:10px; display:flex; align-items:center; gap:6px; border-radius:var(--radius); margin-bottom:14px; max-width:520px; }
|
|
22
|
+
.banner-ok { background:var(--jade-bg); color:var(--jade); border:1px solid rgba(45,139,106,.2); }
|
|
23
|
+
.banner-ok::before { content:"✓"; }
|
|
24
|
+
.banner-err { background:rgba(139,38,53,.07); color:var(--red); border:1px solid rgba(139,38,53,.15); }
|
|
25
|
+
.banner-err::before { content:"✕"; }
|
|
26
|
+
.config-hint { font-size:11px; color:var(--muted); margin-top:12px; line-height:1.7; }
|
|
27
|
+
.config-hint a { color:var(--accent); text-decoration:none; }
|
|
28
|
+
.config-hint a:hover { text-decoration:underline; }
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<div class="app">
|
|
33
|
+
<header class="topbar">
|
|
34
|
+
<a class="logo" href="/dashboard.html"><div class="logo-mark">OR</div>Orbiter</a>
|
|
35
|
+
<div class="topbar-right">
|
|
36
|
+
<button class="search-trigger" id="search-btn" title="Search (⌘K)">
|
|
37
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
38
|
+
Search <kbd>⌘K</kbd>
|
|
39
|
+
</button>
|
|
40
|
+
<button class="scheme-toggle" id="scheme-toggle" title="Toggle scheme">◐</button>
|
|
41
|
+
<span class="user" id="topbar-user"></span>
|
|
42
|
+
<span class="logout" id="logout-btn">Sign out</span>
|
|
43
|
+
</div>
|
|
44
|
+
</header>
|
|
45
|
+
<nav class="sidebar">
|
|
46
|
+
<div class="nav-section">Content</div>
|
|
47
|
+
<a class="nav-item" href="/dashboard.html"><span class="nav-icon">◈</span>Dashboard</a>
|
|
48
|
+
<a class="nav-item" href="/collections.html"><span class="nav-icon">⊞</span>Collections</a>
|
|
49
|
+
<div class="nav-section">Assets</div>
|
|
50
|
+
<a class="nav-item" href="/media.html"><span class="nav-icon">⊟</span>Media</a>
|
|
51
|
+
<div class="nav-section">System</div>
|
|
52
|
+
<a class="nav-item" href="/schema.html"><span class="nav-icon">◈</span>Schema</a>
|
|
53
|
+
<a class="nav-item active" href="/build.html"><span class="nav-icon">▲</span>Build</a>
|
|
54
|
+
<a class="nav-item" href="/settings.html"><span class="nav-icon">◎</span>Settings</a>
|
|
55
|
+
<a class="nav-item admin-only" href="/users.html" style="display:none"><span class="nav-icon">◉</span>Users</a>
|
|
56
|
+
<div class="sidebar-footer">
|
|
57
|
+
<div class="pod-name" id="pod-name">content.pod</div>
|
|
58
|
+
<div class="pod-info" id="pod-info"></div>
|
|
59
|
+
<div class="pod-status"><span class="pod-dot"></span>pod synced</div>
|
|
60
|
+
</div>
|
|
61
|
+
</nav>
|
|
62
|
+
<main class="main">
|
|
63
|
+
<div class="page-header">
|
|
64
|
+
<h1 class="page-title">Build</h1>
|
|
65
|
+
<p class="page-sub" id="last-triggered"></p>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="banner" class="banner" style="display:none"></div>
|
|
68
|
+
<div id="content"><div class="empty"><div class="spinner"></div></div></div>
|
|
69
|
+
|
|
70
|
+
<script type="module">
|
|
71
|
+
const me = await fetch('/api/auth/me',{credentials:'include'}).then(r=>r.json()).catch(()=>null);
|
|
72
|
+
if (!me?.user) { location.replace('/login.html'); }
|
|
73
|
+
document.getElementById('topbar-user').textContent = me.user.username;
|
|
74
|
+
if (me.user.role==='admin') document.querySelectorAll('.admin-only').forEach(el=>el.style.display='');
|
|
75
|
+
document.getElementById('logout-btn').addEventListener('click',async()=>{
|
|
76
|
+
await fetch('/api/auth/logout',{method:'POST',credentials:'include'});
|
|
77
|
+
location.replace('/login.html');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function showBanner(cls, text) {
|
|
81
|
+
const el = document.getElementById('banner');
|
|
82
|
+
el.className = 'banner ' + cls;
|
|
83
|
+
el.textContent = text;
|
|
84
|
+
el.style.display = '';
|
|
85
|
+
setTimeout(()=>el.style.display='none', 4000);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const status = await fetch('/api/build/status',{credentials:'include'}).then(r=>r.json()).catch(()=>({configured:false,lastTriggered:null}));
|
|
89
|
+
const lastEl = document.getElementById('last-triggered');
|
|
90
|
+
lastEl.textContent = status.lastTriggered ? `Last: ${new Date(status.lastTriggered).toLocaleString()}` : 'Never triggered';
|
|
91
|
+
|
|
92
|
+
const wrap = document.getElementById('content');
|
|
93
|
+
wrap.innerHTML = `
|
|
94
|
+
<div class="build-card">
|
|
95
|
+
<div class="build-status">
|
|
96
|
+
<div class="build-dot ${status.configured?'configured':'unconfigured'}"></div>
|
|
97
|
+
<div class="build-label">${status.configured ? 'Webhook configured' : 'No webhook configured'}</div>
|
|
98
|
+
</div>
|
|
99
|
+
${status.configured
|
|
100
|
+
? `<div class="build-meta">Triggering a build will POST to the configured webhook URL.</div>
|
|
101
|
+
<button class="btn btn-primary" id="trigger-btn" style="min-width:160px;">▲ Trigger build</button>
|
|
102
|
+
${status.lastTriggered ? `<div class="build-last">Last triggered: ${new Date(status.lastTriggered).toLocaleString()}</div>` : ''}`
|
|
103
|
+
: `<div class="config-hint">
|
|
104
|
+
No webhook URL is configured. To enable build triggers, add a webhook URL in
|
|
105
|
+
<a href="/settings.html">Settings → Build</a>.
|
|
106
|
+
</div>`
|
|
107
|
+
}
|
|
108
|
+
</div>
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
document.getElementById('trigger-btn')?.addEventListener('click', async btn=>{
|
|
112
|
+
btn = document.getElementById('trigger-btn');
|
|
113
|
+
btn.disabled = true;
|
|
114
|
+
btn.textContent = 'Triggering…';
|
|
115
|
+
const res = await fetch('/api/build/trigger',{method:'POST',credentials:'include'});
|
|
116
|
+
const json = await res.json();
|
|
117
|
+
btn.disabled = false;
|
|
118
|
+
btn.textContent = '▲ Trigger build';
|
|
119
|
+
if (res.ok) showBanner('banner-ok','Build triggered');
|
|
120
|
+
else showBanner('banner-err', json.error ?? 'Webhook error');
|
|
121
|
+
});
|
|
122
|
+
</script>
|
|
123
|
+
</main>
|
|
124
|
+
</div>
|
|
125
|
+
<script src="/search.js"></script>
|
|
126
|
+
<script src="/sidebar.js"></script>
|
|
127
|
+
<script src="/router.js"></script>
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|