@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
|
@@ -0,0 +1,688 @@
|
|
|
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 — Settings</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
|
+
.settings-wrap { max-width:720px; padding-bottom:40px; }
|
|
14
|
+
|
|
15
|
+
/* Card — einheitliches bg2, eine Farbe, kein Alternieren */
|
|
16
|
+
.settings-group {
|
|
17
|
+
background: var(--bg2);
|
|
18
|
+
border: 1px solid var(--line);
|
|
19
|
+
border-radius: 10px;
|
|
20
|
+
margin-bottom: 12px;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Section label — klein, ruhig, ◆ statt Left-Strip */
|
|
25
|
+
.group-header {
|
|
26
|
+
padding: 10px 24px;
|
|
27
|
+
font-size: 10px;
|
|
28
|
+
letter-spacing: 0.18em;
|
|
29
|
+
text-transform: uppercase;
|
|
30
|
+
color: var(--muted);
|
|
31
|
+
border-bottom: 1px solid var(--line);
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 7px;
|
|
35
|
+
}
|
|
36
|
+
.group-header::before { content: "◆"; color: var(--gold); font-size: 5px; line-height: 1; }
|
|
37
|
+
|
|
38
|
+
/* Setting row — hover, keine alternierenden Hintergründe */
|
|
39
|
+
.setting-row {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: 1fr 252px;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 24px;
|
|
44
|
+
padding: 18px 24px;
|
|
45
|
+
border-bottom: 1px solid var(--line2);
|
|
46
|
+
transition: background 0.1s;
|
|
47
|
+
}
|
|
48
|
+
.setting-row:last-child { border-bottom: none; }
|
|
49
|
+
.setting-row:hover { background: var(--hover-bg); }
|
|
50
|
+
.setting-label { font-size: 13px; color: var(--text); margin-bottom: 3px; }
|
|
51
|
+
.setting-desc { font-size: 11px; color: var(--muted); line-height: 1.55; }
|
|
52
|
+
|
|
53
|
+
/* Inputs — vollständig gerundete Box, weicher Focus */
|
|
54
|
+
.settings-wrap .input {
|
|
55
|
+
width: 100%;
|
|
56
|
+
background: var(--bg0);
|
|
57
|
+
border: 1px solid var(--line);
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
padding: 7px 11px;
|
|
60
|
+
color: var(--heading);
|
|
61
|
+
font-family: var(--mono);
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
outline: none;
|
|
64
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
65
|
+
}
|
|
66
|
+
.settings-wrap .input:focus {
|
|
67
|
+
border-color: var(--accent);
|
|
68
|
+
box-shadow: 0 0 0 3px var(--accent-bg);
|
|
69
|
+
}
|
|
70
|
+
.settings-wrap .input[readonly] {
|
|
71
|
+
color: var(--muted);
|
|
72
|
+
cursor: default;
|
|
73
|
+
background: var(--bg1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Save row */
|
|
77
|
+
.save-row {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: flex-end;
|
|
80
|
+
align-items: center;
|
|
81
|
+
padding: 14px 24px;
|
|
82
|
+
border-top: 1px solid var(--line);
|
|
83
|
+
gap: 10px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Banners */
|
|
87
|
+
.banner { padding: 10px 20px; font-size: 11px; display: flex; align-items: center; gap: 8px; border-radius: 8px; margin-bottom: 16px; }
|
|
88
|
+
.banner-ok { background: var(--jade-bg); color: var(--jade); border: 1px solid rgba(45,139,106,.2); }
|
|
89
|
+
.banner-ok::before { content: "✓"; font-size: 12px; }
|
|
90
|
+
.banner-err { background: var(--red-bg); color: var(--red); border: 1px solid rgba(240,112,128,.18); }
|
|
91
|
+
.banner-err::before { content: "✕"; font-size: 12px; }
|
|
92
|
+
|
|
93
|
+
/* Theme cards */
|
|
94
|
+
.theme-cards { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
95
|
+
.theme-card {
|
|
96
|
+
flex: 1; min-width: 110px;
|
|
97
|
+
background: var(--bg0);
|
|
98
|
+
border: 1px solid var(--line);
|
|
99
|
+
border-radius: 8px;
|
|
100
|
+
padding: 14px 14px 12px;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
text-align: left;
|
|
103
|
+
font-family: var(--mono);
|
|
104
|
+
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
|
105
|
+
}
|
|
106
|
+
.theme-card:hover { border-color: var(--mid); box-shadow: 0 2px 8px rgba(0,0,0,.12); }
|
|
107
|
+
.theme-card.active { border-color: var(--accent); background: var(--accent-bg); box-shadow: 0 0 0 1px var(--accent); }
|
|
108
|
+
.theme-swatch { display: flex; gap: 4px; margin-bottom: 10px; height: 4px; }
|
|
109
|
+
.theme-swatch span { flex: 1; border-radius: 2px; }
|
|
110
|
+
.theme-card-name { font-size: 11px; color: var(--text); margin-bottom: 2px; font-weight: 500; }
|
|
111
|
+
.theme-card-sub { font-size: 9px; color: var(--muted); letter-spacing: 0.03em; line-height: 1.4; }
|
|
112
|
+
|
|
113
|
+
/* Style cards */
|
|
114
|
+
.style-cards { display: flex; gap: 10px; }
|
|
115
|
+
.style-card {
|
|
116
|
+
flex: 1; min-width: 120px; max-width: 180px;
|
|
117
|
+
background: var(--bg0); border: 1px solid var(--line); border-radius: 8px;
|
|
118
|
+
padding: 12px 12px 10px; cursor: pointer; text-align: left;
|
|
119
|
+
font-family: var(--mono); transition: border-color .15s, background .15s, box-shadow .15s;
|
|
120
|
+
}
|
|
121
|
+
.style-card:hover { border-color: var(--mid); box-shadow: 0 2px 8px rgba(0,0,0,.12); }
|
|
122
|
+
.style-card.active { border-color: var(--accent); background: var(--accent-bg); box-shadow: 0 0 0 1px var(--accent); }
|
|
123
|
+
.style-preview {
|
|
124
|
+
display: flex; gap: 5px; height: 44px; margin-bottom: 10px;
|
|
125
|
+
background: var(--bg3); border-radius: 5px; overflow: hidden; position: relative;
|
|
126
|
+
}
|
|
127
|
+
.sp-sidebar { width: 14px; background: var(--bg2); flex-shrink: 0; }
|
|
128
|
+
.sp-body { flex: 1; padding: 5px 5px; display: flex; flex-direction: column; gap: 4px; }
|
|
129
|
+
.sp-row { height: 6px; background: var(--bg2); border-radius: 2px; }
|
|
130
|
+
.sp-row-short { width: 65%; }
|
|
131
|
+
/* Glass preview */
|
|
132
|
+
.sp-glass { background: var(--bg0); }
|
|
133
|
+
.sp-orb { position: absolute; border-radius: 50%; }
|
|
134
|
+
.sp-orb1 { width: 40px; height: 40px; top: -10px; left: 10px; background: var(--accent-bg); filter: blur(8px); }
|
|
135
|
+
.sp-orb2 { width: 30px; height: 30px; bottom: -5px; right: 10px; background: var(--gold-bg); filter: blur(6px); }
|
|
136
|
+
.sp-glass-panel { background: color-mix(in srgb, var(--bg2) 60%, transparent); backdrop-filter: blur(4px); border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent); border-radius: 3px; }
|
|
137
|
+
.sp-glass .sp-sidebar { width: 14px; background: color-mix(in srgb, var(--bg1) 55%, transparent); backdrop-filter: blur(4px); }
|
|
138
|
+
|
|
139
|
+
/* Pod section */
|
|
140
|
+
.pod-meta {
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
gap: 12px;
|
|
145
|
+
padding: 13px 24px;
|
|
146
|
+
border-bottom: 1px solid var(--line2);
|
|
147
|
+
}
|
|
148
|
+
.pod-meta-path {
|
|
149
|
+
font-family: var(--mono);
|
|
150
|
+
font-size: 11px;
|
|
151
|
+
color: var(--muted);
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
text-overflow: ellipsis;
|
|
154
|
+
white-space: nowrap;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
}
|
|
157
|
+
.pod-meta-ver {
|
|
158
|
+
flex-shrink: 0;
|
|
159
|
+
font-family: var(--mono);
|
|
160
|
+
font-size: 9px;
|
|
161
|
+
letter-spacing: 0.06em;
|
|
162
|
+
color: var(--muted);
|
|
163
|
+
background: var(--bg3);
|
|
164
|
+
border: 1px solid var(--line);
|
|
165
|
+
border-radius: 4px;
|
|
166
|
+
padding: 2px 8px;
|
|
167
|
+
}
|
|
168
|
+
.pod-col-row {
|
|
169
|
+
display: flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: space-between;
|
|
172
|
+
padding: 11px 24px;
|
|
173
|
+
border-bottom: 1px solid var(--line2);
|
|
174
|
+
transition: background 0.1s;
|
|
175
|
+
}
|
|
176
|
+
.pod-col-row:last-child { border-bottom: none; }
|
|
177
|
+
.pod-col-row:hover { background: var(--hover-bg); }
|
|
178
|
+
.pod-col-name { font-size: 13px; color: var(--text); }
|
|
179
|
+
.pod-col-count {
|
|
180
|
+
font-family: var(--mono);
|
|
181
|
+
font-size: 10px;
|
|
182
|
+
color: var(--muted);
|
|
183
|
+
background: var(--bg3);
|
|
184
|
+
border: 1px solid var(--line);
|
|
185
|
+
border-radius: 4px;
|
|
186
|
+
padding: 2px 8px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Support */
|
|
190
|
+
.support-widget {
|
|
191
|
+
border: 1px solid var(--line);
|
|
192
|
+
border-radius: 10px;
|
|
193
|
+
padding: 20px 24px;
|
|
194
|
+
margin-top: 12px;
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: space-between;
|
|
198
|
+
gap: 24px;
|
|
199
|
+
background: var(--bg2);
|
|
200
|
+
}
|
|
201
|
+
.support-copy-label { font-size: 9px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; }
|
|
202
|
+
.support-copy-text { font-size: 12px; color: var(--text); line-height: 1.6; }
|
|
203
|
+
.btn-support {
|
|
204
|
+
flex-shrink: 0;
|
|
205
|
+
padding: 8px 18px;
|
|
206
|
+
border: 1px solid var(--gold);
|
|
207
|
+
color: var(--gold);
|
|
208
|
+
font-family: var(--mono);
|
|
209
|
+
font-size: 10px;
|
|
210
|
+
letter-spacing: 0.1em;
|
|
211
|
+
text-transform: uppercase;
|
|
212
|
+
text-decoration: none;
|
|
213
|
+
white-space: nowrap;
|
|
214
|
+
transition: background 0.15s, color 0.15s;
|
|
215
|
+
border-radius: 6px;
|
|
216
|
+
}
|
|
217
|
+
.btn-support:hover { background: var(--gold); color: var(--bg0); }
|
|
218
|
+
</style>
|
|
219
|
+
</head>
|
|
220
|
+
<body>
|
|
221
|
+
<div class="app">
|
|
222
|
+
<header class="topbar">
|
|
223
|
+
<a class="logo" href="/dashboard.html"><div class="logo-mark">OR</div>Orbiter</a>
|
|
224
|
+
<div class="topbar-right">
|
|
225
|
+
<button class="search-trigger" id="search-btn" title="Search (⌘K)">
|
|
226
|
+
<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>
|
|
227
|
+
Search <kbd>⌘K</kbd>
|
|
228
|
+
</button>
|
|
229
|
+
<button class="scheme-toggle" id="scheme-toggle" title="Toggle scheme">◐</button>
|
|
230
|
+
<span class="user" id="topbar-user"></span>
|
|
231
|
+
<span class="logout" id="logout-btn">Sign out</span>
|
|
232
|
+
</div>
|
|
233
|
+
</header>
|
|
234
|
+
<nav class="sidebar">
|
|
235
|
+
<div class="nav-section">Content</div>
|
|
236
|
+
<a class="nav-item" href="/dashboard.html"><span class="nav-icon">◈</span>Dashboard</a>
|
|
237
|
+
<a class="nav-item" href="/collections.html"><span class="nav-icon">⊞</span>Collections</a>
|
|
238
|
+
<div class="nav-section">Assets</div>
|
|
239
|
+
<a class="nav-item" href="/media.html"><span class="nav-icon">⊟</span>Media</a>
|
|
240
|
+
<div class="nav-section">System</div>
|
|
241
|
+
<a class="nav-item" href="/schema.html"><span class="nav-icon">◈</span>Schema</a>
|
|
242
|
+
<a class="nav-item" href="/build.html"><span class="nav-icon">▲</span>Build</a>
|
|
243
|
+
<a class="nav-item active" href="/settings.html"><span class="nav-icon">◎</span>Settings</a>
|
|
244
|
+
<a class="nav-item admin-only" href="/users.html" style="display:none"><span class="nav-icon">◉</span>Users</a>
|
|
245
|
+
<div class="sidebar-footer">
|
|
246
|
+
<div class="pod-name" id="pod-name">content.pod</div>
|
|
247
|
+
<div class="pod-info" id="pod-info"></div>
|
|
248
|
+
<div class="pod-status"><span class="pod-dot"></span>pod synced</div>
|
|
249
|
+
</div>
|
|
250
|
+
</nav>
|
|
251
|
+
<main class="main">
|
|
252
|
+
<div class="page-header">
|
|
253
|
+
<h1 class="page-title">Settings</h1>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="settings-wrap" id="settings-content">
|
|
256
|
+
<div class="empty"><div class="spinner"></div></div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<script type="module">
|
|
260
|
+
const me = await fetch('/api/auth/me',{credentials:'include'}).then(r=>r.json()).catch(()=>null);
|
|
261
|
+
if (!me?.user) { location.replace('/login.html'); }
|
|
262
|
+
document.getElementById('topbar-user').textContent = me.user.username;
|
|
263
|
+
if (me.user.role==='admin') document.querySelectorAll('.admin-only').forEach(el=>el.style.display='');
|
|
264
|
+
document.getElementById('logout-btn').addEventListener('click',async()=>{
|
|
265
|
+
await fetch('/api/auth/logout',{method:'POST',credentials:'include'});
|
|
266
|
+
location.replace('/login.html');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const meta = await fetch('/api/meta',{credentials:'include'}).then(r=>r.json()).catch(()=>({}));
|
|
270
|
+
|
|
271
|
+
const get = k => meta[k] ?? '';
|
|
272
|
+
// pod-info managed by sidebar.js
|
|
273
|
+
|
|
274
|
+
function showBanner(id, cls, text) {
|
|
275
|
+
const el = document.getElementById(id);
|
|
276
|
+
if (!el) return;
|
|
277
|
+
el.className = 'banner ' + cls;
|
|
278
|
+
el.textContent = (cls==='banner-ok'?'✓ ':'✕ ') + text;
|
|
279
|
+
el.style.display = '';
|
|
280
|
+
setTimeout(()=>el.style.display='none', 3500);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function saveMeta(pairs) {
|
|
284
|
+
const body = Object.fromEntries(pairs);
|
|
285
|
+
await fetch('/api/meta', {
|
|
286
|
+
method:'PUT', credentials:'include',
|
|
287
|
+
headers:{'Content-Type':'application/json'},
|
|
288
|
+
body: JSON.stringify(body),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const ghConfigured = !!(get('github.token') && get('github.repo'));
|
|
293
|
+
const currentTheme = get('ui.theme') || 'space';
|
|
294
|
+
const currentScheme = localStorage.getItem('orb_scheme') || 'auto';
|
|
295
|
+
const currentStyle = localStorage.getItem('orb_style') || 'classic';
|
|
296
|
+
|
|
297
|
+
const wrap = document.getElementById('settings-content');
|
|
298
|
+
wrap.innerHTML = `
|
|
299
|
+
<div id="site-banner" class="banner" style="display:none"></div>
|
|
300
|
+
|
|
301
|
+
<!-- Site + Build + GitHub + API — one form -->
|
|
302
|
+
<form id="site-form">
|
|
303
|
+
<div class="settings-group">
|
|
304
|
+
<div class="group-header">Site</div>
|
|
305
|
+
<div class="setting-row">
|
|
306
|
+
<div><div class="setting-label">Site name</div><div class="setting-desc">Displayed in the admin header</div></div>
|
|
307
|
+
<input class="input" name="site.name" value="${get('site.name')}" placeholder="My Site" />
|
|
308
|
+
</div>
|
|
309
|
+
<div class="setting-row">
|
|
310
|
+
<div><div class="setting-label">Site URL</div><div class="setting-desc">Production URL for preview links</div></div>
|
|
311
|
+
<input class="input" name="site.url" value="${get('site.url')}" placeholder="https://example.com" />
|
|
312
|
+
</div>
|
|
313
|
+
<div class="setting-row">
|
|
314
|
+
<div><div class="setting-label">Description</div><div class="setting-desc">Short site description</div></div>
|
|
315
|
+
<input class="input" name="site.description" value="${get('site.description')}" />
|
|
316
|
+
</div>
|
|
317
|
+
<div class="setting-row">
|
|
318
|
+
<div><div class="setting-label">Primary locale</div><div class="setting-desc">Default language code, e.g. en</div></div>
|
|
319
|
+
<input class="input" name="site.locale" value="${get('site.locale')}" placeholder="en" />
|
|
320
|
+
</div>
|
|
321
|
+
<div class="setting-row">
|
|
322
|
+
<div><div class="setting-label">Additional locales</div><div class="setting-desc">Comma-separated, e.g. en,de,fr</div></div>
|
|
323
|
+
<input class="input" name="site.locales" value="${get('site.locales')}" placeholder="en,de" />
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div class="settings-group">
|
|
328
|
+
<div class="group-header">Build</div>
|
|
329
|
+
<div class="setting-row">
|
|
330
|
+
<div><div class="setting-label">Webhook URL</div><div class="setting-desc">POST to this URL to trigger a build</div></div>
|
|
331
|
+
<input class="input" name="build.webhook_url" value="${get('build.webhook_url')}" placeholder="https://api.netlify.com/build/hook/…" />
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<div class="settings-group">
|
|
336
|
+
<div class="group-header">GitHub</div>
|
|
337
|
+
<div class="setting-row">
|
|
338
|
+
<div><div class="setting-label">GitHub token</div><div class="setting-desc">Personal access token with repo scope</div></div>
|
|
339
|
+
<input class="input" type="password" name="github.token" value="${get('github.token')}" autocomplete="off" placeholder="ghp_…" />
|
|
340
|
+
</div>
|
|
341
|
+
<div class="setting-row">
|
|
342
|
+
<div><div class="setting-label">Repository</div><div class="setting-desc">owner/repo format</div></div>
|
|
343
|
+
<input class="input" name="github.repo" value="${get('github.repo')}" placeholder="owner/my-site" />
|
|
344
|
+
</div>
|
|
345
|
+
<div class="setting-row">
|
|
346
|
+
<div><div class="setting-label">Branch</div><div class="setting-desc">Target branch for commits</div></div>
|
|
347
|
+
<input class="input" name="github.branch" value="${get('github.branch')||'main'}" placeholder="main" />
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
<div class="settings-group">
|
|
352
|
+
<div class="group-header">Public API</div>
|
|
353
|
+
<div class="setting-row">
|
|
354
|
+
<div><div class="setting-label">Enable public API</div><div class="setting-desc">Expose read-only collection endpoints</div></div>
|
|
355
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
|
|
356
|
+
<input type="checkbox" name="api.enabled" value="1" ${get('api.enabled')==='1'?'checked':''} style="accent-color:var(--accent);width:14px;height:14px;" />
|
|
357
|
+
<span style="font-size:11px;color:var(--text);font-family:var(--mono);">Enabled</span>
|
|
358
|
+
</label>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="setting-row">
|
|
361
|
+
<div><div class="setting-label">API token</div><div class="setting-desc">Optional bearer token to restrict access</div></div>
|
|
362
|
+
<input class="input" type="password" name="api.token" value="${get('api.token')}" autocomplete="off" placeholder="Leave blank for open access" />
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<div class="save-row">
|
|
367
|
+
<button type="submit" class="btn-save">Save settings</button>
|
|
368
|
+
</div>
|
|
369
|
+
</form>
|
|
370
|
+
|
|
371
|
+
<!-- GitHub Sync (conditional) -->
|
|
372
|
+
<div id="github-banner" class="banner" style="display:none"></div>
|
|
373
|
+
${ghConfigured ? `
|
|
374
|
+
<div class="settings-group">
|
|
375
|
+
<div class="group-header">GitHub Sync</div>
|
|
376
|
+
<div class="setting-row">
|
|
377
|
+
<div>
|
|
378
|
+
<div class="setting-label">Push to GitHub</div>
|
|
379
|
+
<div class="setting-desc">Commits pod + media to your repo. Triggers a rebuild if you have a GitHub Actions workflow.</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
382
|
+
<button class="btn-save" id="github-push-btn">Push now</button>
|
|
383
|
+
<a class="btn btn-ghost btn-sm" href="/api/github/workflow" download title="Download GitHub Actions workflow file">⬇ Workflow</a>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
` : ''}
|
|
388
|
+
|
|
389
|
+
<!-- Account -->
|
|
390
|
+
<div id="account-banner" class="banner" style="display:none"></div>
|
|
391
|
+
<div class="settings-group">
|
|
392
|
+
<div class="group-header">Account — ${me.user.username}</div>
|
|
393
|
+
<form id="pw-form">
|
|
394
|
+
<div class="setting-row">
|
|
395
|
+
<div><div class="setting-label">Current password</div></div>
|
|
396
|
+
<input class="input" type="password" name="current_password" autocomplete="current-password" required />
|
|
397
|
+
</div>
|
|
398
|
+
<div class="setting-row">
|
|
399
|
+
<div><div class="setting-label">New password</div><div class="setting-desc">Minimum 8 characters</div></div>
|
|
400
|
+
<input class="input" type="password" name="new_password" autocomplete="new-password" required minlength="8" />
|
|
401
|
+
</div>
|
|
402
|
+
<div class="setting-row">
|
|
403
|
+
<div><div class="setting-label">Confirm new password</div></div>
|
|
404
|
+
<input class="input" type="password" name="confirm_password" autocomplete="new-password" required />
|
|
405
|
+
</div>
|
|
406
|
+
<div class="save-row">
|
|
407
|
+
<button type="submit" class="btn-save">Change password</button>
|
|
408
|
+
</div>
|
|
409
|
+
</form>
|
|
410
|
+
<form id="un-form" style="border-top:1px solid var(--line);">
|
|
411
|
+
<div class="setting-row">
|
|
412
|
+
<div><div class="setting-label">New username</div><div class="setting-desc">Current: <strong>${me.user.username}</strong></div></div>
|
|
413
|
+
<input class="input" type="text" name="new_username" autocomplete="username" required placeholder="${me.user.username}" />
|
|
414
|
+
</div>
|
|
415
|
+
<div class="setting-row">
|
|
416
|
+
<div><div class="setting-label">Confirm with password</div></div>
|
|
417
|
+
<input class="input" type="password" name="current_password" autocomplete="current-password" required />
|
|
418
|
+
</div>
|
|
419
|
+
<div class="save-row">
|
|
420
|
+
<button type="submit" class="btn-save">Change username</button>
|
|
421
|
+
</div>
|
|
422
|
+
</form>
|
|
423
|
+
</div>
|
|
424
|
+
|
|
425
|
+
<!-- Admin Language -->
|
|
426
|
+
<div class="settings-group">
|
|
427
|
+
<div class="group-header">Interface language</div>
|
|
428
|
+
<div class="setting-row">
|
|
429
|
+
<div><div class="setting-label">Admin language</div><div class="setting-desc">Language for the admin interface labels</div></div>
|
|
430
|
+
<div style="display:flex;gap:8px">
|
|
431
|
+
<button class="btn btn-ghost btn-sm lang-btn ${(localStorage.getItem('orb_lang')||'en')==='en'?'active':''}" data-lang="en">EN</button>
|
|
432
|
+
<button class="btn btn-ghost btn-sm lang-btn ${localStorage.getItem('orb_lang')==='de'?'active':''}" data-lang="de">DE</button>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<!-- Appearance -->
|
|
438
|
+
<div class="settings-group">
|
|
439
|
+
<div class="group-header">Appearance</div>
|
|
440
|
+
<div style="padding:16px 28px;border-bottom:1px solid var(--line2)">
|
|
441
|
+
<div class="setting-label" style="margin-bottom:10px">Color scheme</div>
|
|
442
|
+
<div style="display:flex;gap:8px">
|
|
443
|
+
<button class="btn btn-ghost btn-sm scheme-btn ${currentScheme==='auto'?'active':''}" data-s="auto">Auto (system)</button>
|
|
444
|
+
<button class="btn btn-ghost btn-sm scheme-btn ${currentScheme==='dark'?'active':''}" data-s="dark">● Dark</button>
|
|
445
|
+
<button class="btn btn-ghost btn-sm scheme-btn ${currentScheme==='light'?'active':''}" data-s="light">○ Light</button>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
<div style="padding:16px 28px">
|
|
449
|
+
<div class="setting-label" style="margin-bottom:10px">Palette</div>
|
|
450
|
+
<div class="theme-cards">
|
|
451
|
+
<button class="theme-card ${currentTheme==='space'?'active':''}" data-theme="space">
|
|
452
|
+
<div class="theme-swatch">
|
|
453
|
+
<span style="background:#080e18"></span>
|
|
454
|
+
<span style="background:#1898f8"></span>
|
|
455
|
+
<span style="background:#00d8b8"></span>
|
|
456
|
+
<span style="background:#00c8a0"></span>
|
|
457
|
+
</div>
|
|
458
|
+
<div class="theme-card-name">Space</div>
|
|
459
|
+
<div class="theme-card-sub">Space station · electric blue & teal</div>
|
|
460
|
+
</button>
|
|
461
|
+
<button class="theme-card ${currentTheme==='zen'?'active':''}" data-theme="zen">
|
|
462
|
+
<div class="theme-swatch">
|
|
463
|
+
<span style="background:#0f0e10"></span>
|
|
464
|
+
<span style="background:#566070"></span>
|
|
465
|
+
<span style="background:#6b8c6e"></span>
|
|
466
|
+
<span style="background:#ad7575"></span>
|
|
467
|
+
</div>
|
|
468
|
+
<div class="theme-card-name">Zen</div>
|
|
469
|
+
<div class="theme-card-sub">Japandi · slate & mauve</div>
|
|
470
|
+
</button>
|
|
471
|
+
<button class="theme-card ${currentTheme==='catppuccin'?'active':''}" data-theme="catppuccin">
|
|
472
|
+
<div class="theme-swatch">
|
|
473
|
+
<span style="background:#11111b"></span>
|
|
474
|
+
<span style="background:#cba6f7"></span>
|
|
475
|
+
<span style="background:#a6e3a1"></span>
|
|
476
|
+
<span style="background:#f9e2af"></span>
|
|
477
|
+
</div>
|
|
478
|
+
<div class="theme-card-name">Catppuccin</div>
|
|
479
|
+
<div class="theme-card-sub">Mocha / Latte · pastel</div>
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
<div style="padding:16px 28px;border-top:1px solid var(--line2)">
|
|
484
|
+
<div class="setting-label" style="margin-bottom:10px">Style</div>
|
|
485
|
+
<div class="style-cards">
|
|
486
|
+
<button class="style-card \${currentStyle==='classic'?'active':''}" data-style="classic">
|
|
487
|
+
<div class="style-preview">
|
|
488
|
+
<div class="sp-sidebar"></div>
|
|
489
|
+
<div class="sp-body">
|
|
490
|
+
<div class="sp-row"></div>
|
|
491
|
+
<div class="sp-row sp-row-short"></div>
|
|
492
|
+
<div class="sp-row"></div>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="theme-card-name">Classic</div>
|
|
496
|
+
<div class="theme-card-sub">Clean · flat panels</div>
|
|
497
|
+
</button>
|
|
498
|
+
<button class="style-card \${currentStyle==='glass'?'active':''}" data-style="glass">
|
|
499
|
+
<div class="style-preview sp-glass">
|
|
500
|
+
<div class="sp-orb sp-orb1"></div>
|
|
501
|
+
<div class="sp-orb sp-orb2"></div>
|
|
502
|
+
<div class="sp-sidebar sp-glass-panel"></div>
|
|
503
|
+
<div class="sp-body">
|
|
504
|
+
<div class="sp-row sp-glass-panel"></div>
|
|
505
|
+
<div class="sp-row sp-row-short sp-glass-panel"></div>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="theme-card-name">Glass</div>
|
|
509
|
+
<div class="theme-card-sub">Glassmorphism · blur</div>
|
|
510
|
+
</button>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
515
|
+
<!-- Data + Import -->
|
|
516
|
+
<div class="settings-group">
|
|
517
|
+
<div class="group-header">Data</div>
|
|
518
|
+
<div class="setting-row">
|
|
519
|
+
<div>
|
|
520
|
+
<div class="setting-label">Import content</div>
|
|
521
|
+
<div class="setting-desc">Import entries from a JSON or CSV file</div>
|
|
522
|
+
</div>
|
|
523
|
+
<a class="btn btn-ghost" href="/import.html">Open importer</a>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<!-- Pod info -->
|
|
528
|
+
<div class="settings-group" id="pod-info-group">
|
|
529
|
+
<div class="group-header">Pod</div>
|
|
530
|
+
<div class="pod-meta">
|
|
531
|
+
<div class="pod-meta-path" id="pod-path-field">…</div>
|
|
532
|
+
<span class="pod-meta-ver" id="pod-version-field">v1</span>
|
|
533
|
+
</div>
|
|
534
|
+
<div id="pod-collections-list">
|
|
535
|
+
<div class="pod-col-row"><span class="pod-col-name" style="color:var(--muted)">Loading…</span></div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<!-- Support widget -->
|
|
540
|
+
<div class="support-widget">
|
|
541
|
+
<div class="support-copy">
|
|
542
|
+
<div class="support-copy-label">Support</div>
|
|
543
|
+
<div class="support-copy-text">Orbiter is open source and free to use.<br>If it saves you time, consider supporting development.</div>
|
|
544
|
+
</div>
|
|
545
|
+
<a href="https://polar.sh/a83" class="btn-support" target="_blank" rel="noopener">Support on Polar →</a>
|
|
546
|
+
</div>
|
|
547
|
+
`;
|
|
548
|
+
|
|
549
|
+
// Site form
|
|
550
|
+
document.getElementById('site-form').addEventListener('submit', async e => {
|
|
551
|
+
e.preventDefault();
|
|
552
|
+
const fd = new FormData(e.target);
|
|
553
|
+
await saveMeta([
|
|
554
|
+
['site.name', fd.get('site.name')],
|
|
555
|
+
['site.url', fd.get('site.url')],
|
|
556
|
+
['site.description', fd.get('site.description')],
|
|
557
|
+
['site.locale', fd.get('site.locale')],
|
|
558
|
+
['site.locales', fd.get('site.locales')],
|
|
559
|
+
['build.webhook_url',fd.get('build.webhook_url')],
|
|
560
|
+
['github.token', fd.get('github.token')],
|
|
561
|
+
['github.repo', fd.get('github.repo')],
|
|
562
|
+
['github.branch', fd.get('github.branch')],
|
|
563
|
+
['api.enabled', fd.get('api.enabled') ? '1' : '0'],
|
|
564
|
+
['api.token', fd.get('api.token')],
|
|
565
|
+
]);
|
|
566
|
+
showBanner('site-banner','banner-ok','Settings saved');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// GitHub push (conditional: only wired if section rendered)
|
|
570
|
+
const ghPushBtn = document.getElementById('github-push-btn');
|
|
571
|
+
if (ghPushBtn) ghPushBtn.addEventListener('click', async () => {
|
|
572
|
+
ghPushBtn.disabled = true;
|
|
573
|
+
ghPushBtn.textContent = 'Pushing…';
|
|
574
|
+
const res = await fetch('/api/github/push', { method:'POST', credentials:'include' });
|
|
575
|
+
const json = await res.json();
|
|
576
|
+
showBanner('github-banner', json.ok ? 'banner-ok' : 'banner-err', json.message ?? (json.ok ? 'Pushed' : 'Push failed'));
|
|
577
|
+
ghPushBtn.textContent = 'Push now';
|
|
578
|
+
ghPushBtn.disabled = false;
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Palette cards
|
|
582
|
+
document.querySelectorAll('.theme-card').forEach(btn => {
|
|
583
|
+
btn.addEventListener('click', async () => {
|
|
584
|
+
const theme = btn.dataset.theme;
|
|
585
|
+
document.querySelectorAll('.theme-card').forEach(b => b.classList.toggle('active', b.dataset.theme === theme));
|
|
586
|
+
const root = document.documentElement;
|
|
587
|
+
if (theme === 'space') root.removeAttribute('data-theme');
|
|
588
|
+
else root.setAttribute('data-theme', theme);
|
|
589
|
+
localStorage.setItem('orb_theme', theme);
|
|
590
|
+
await saveMeta([['ui.theme', theme]]);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Scheme buttons
|
|
595
|
+
document.querySelectorAll('.scheme-btn').forEach(btn => {
|
|
596
|
+
btn.addEventListener('click', () => {
|
|
597
|
+
const s = btn.dataset.s;
|
|
598
|
+
document.querySelectorAll('.scheme-btn').forEach(b => b.classList.toggle('active', b.dataset.s === s));
|
|
599
|
+
const root = document.documentElement;
|
|
600
|
+
root.removeAttribute('data-scheme');
|
|
601
|
+
if (s === 'dark') root.setAttribute('data-scheme', 'dark');
|
|
602
|
+
if (s === 'light') root.setAttribute('data-scheme', 'light');
|
|
603
|
+
localStorage.setItem('orb_scheme', s);
|
|
604
|
+
// Update topbar toggle icon
|
|
605
|
+
const t = document.getElementById('scheme-toggle');
|
|
606
|
+
if (t) t.textContent = s === 'dark' ? '●' : s === 'light' ? '○' : '◐';
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Style cards (classic / glass)
|
|
611
|
+
document.querySelectorAll('.style-card').forEach(btn => {
|
|
612
|
+
btn.addEventListener('click', () => {
|
|
613
|
+
const s = btn.dataset.style;
|
|
614
|
+
document.querySelectorAll('.style-card').forEach(b => b.classList.toggle('active', b.dataset.style === s));
|
|
615
|
+
const root = document.documentElement;
|
|
616
|
+
if (s === 'glass') root.setAttribute('data-style', 'glass');
|
|
617
|
+
else root.removeAttribute('data-style');
|
|
618
|
+
localStorage.setItem('orb_style', s);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Password form (with confirm validation)
|
|
623
|
+
document.getElementById('pw-form').addEventListener('submit', async e => {
|
|
624
|
+
e.preventDefault();
|
|
625
|
+
const fd = new FormData(e.target);
|
|
626
|
+
if (fd.get('new_password') !== fd.get('confirm_password')) {
|
|
627
|
+
showBanner('account-banner','banner-err','Passwords do not match'); return;
|
|
628
|
+
}
|
|
629
|
+
const res = await fetch('/api/account/password', {
|
|
630
|
+
method:'PUT', credentials:'include',
|
|
631
|
+
headers:{'Content-Type':'application/json'},
|
|
632
|
+
body: JSON.stringify({ currentPassword: fd.get('current_password'), newPassword: fd.get('new_password') }),
|
|
633
|
+
});
|
|
634
|
+
const json = await res.json();
|
|
635
|
+
if (res.ok) { showBanner('account-banner','banner-ok','Password changed'); e.target.reset(); }
|
|
636
|
+
else showBanner('account-banner','banner-err', json.error ?? 'Failed');
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// Username form
|
|
640
|
+
document.getElementById('un-form').addEventListener('submit', async e => {
|
|
641
|
+
e.preventDefault();
|
|
642
|
+
const fd = new FormData(e.target);
|
|
643
|
+
const res = await fetch('/api/account/username', {
|
|
644
|
+
method:'PUT', credentials:'include',
|
|
645
|
+
headers:{'Content-Type':'application/json'},
|
|
646
|
+
body: JSON.stringify({ currentPassword: fd.get('current_password'), newUsername: fd.get('new_username') }),
|
|
647
|
+
});
|
|
648
|
+
const json = await res.json();
|
|
649
|
+
if (res.ok) { showBanner('account-banner','banner-ok','Username changed — please sign in again'); setTimeout(()=>location.replace('/login.html'), 1500); }
|
|
650
|
+
else showBanner('account-banner','banner-err', json.error ?? 'Failed');
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Language buttons
|
|
654
|
+
document.querySelectorAll('.lang-btn').forEach(btn => {
|
|
655
|
+
btn.addEventListener('click', () => {
|
|
656
|
+
document.querySelectorAll('.lang-btn').forEach(b => b.classList.toggle('active', b.dataset.lang === btn.dataset.lang));
|
|
657
|
+
localStorage.setItem('orb_lang', btn.dataset.lang);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Pod info
|
|
662
|
+
fetch('/api/info', { credentials:'include' }).then(r => r.json()).then(info => {
|
|
663
|
+
const pf = document.getElementById('pod-path-field');
|
|
664
|
+
const vf = document.getElementById('pod-version-field');
|
|
665
|
+
const cl = document.getElementById('pod-collections-list');
|
|
666
|
+
if (pf) pf.textContent = info.podPath ?? '—';
|
|
667
|
+
if (vf) vf.textContent = `v${info.formatVersion ?? 1}`;
|
|
668
|
+
if (cl) {
|
|
669
|
+
const cols = info.collections ?? [];
|
|
670
|
+
cl.innerHTML = cols.length
|
|
671
|
+
? cols.map(c =>
|
|
672
|
+
`<div class="pod-col-row">
|
|
673
|
+
<span class="pod-col-name">${c.label}</span>
|
|
674
|
+
<span class="pod-col-count">${c.total}</span>
|
|
675
|
+
</div>`
|
|
676
|
+
).join('')
|
|
677
|
+
: '<div class="pod-col-row"><span class="pod-col-name" style="color:var(--muted)">No collections</span></div>';
|
|
678
|
+
}
|
|
679
|
+
}).catch(() => {});
|
|
680
|
+
</script>
|
|
681
|
+
</main>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<script src="/search.js"></script>
|
|
685
|
+
<script src="/sidebar.js"></script>
|
|
686
|
+
<script src="/router.js"></script>
|
|
687
|
+
</body>
|
|
688
|
+
</html>
|