domma-cms 0.10.0 → 0.12.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/CLAUDE.md +248 -159
- package/admin/css/admin.css +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +7 -3
- package/admin/js/config/sidebar-config.js +1 -1
- package/admin/js/http-interceptor.js +1 -0
- package/admin/js/lib/safe-html.js +1 -0
- package/admin/js/templates/layouts.html +5 -4
- package/admin/js/templates/notifications.html +14 -0
- package/admin/js/templates/plugin-marketplace.html +16 -0
- package/admin/js/templates/plugins.html +17 -5
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +1 -16
- package/admin/js/views/notifications.js +1 -0
- package/admin/js/views/plugin-marketplace.js +1 -0
- package/admin/js/views/plugins.js +16 -16
- package/config/navigation.json +5 -72
- package/config/plugins.json +10 -14
- package/config/presets.json +50 -13
- package/config/site.json +11 -63
- package/package.json +2 -1
- package/plugins/_template/admin/templates/index.html +17 -0
- package/plugins/_template/admin/views/index.js +19 -0
- package/plugins/_template/config.js +8 -0
- package/plugins/_template/plugin.js +23 -0
- package/plugins/_template/plugin.json +34 -0
- package/plugins/analytics/plugin.json +41 -31
- package/plugins/blog/admin/templates/blog.html +22 -0
- package/plugins/blog/admin/templates/categories.html +7 -0
- package/plugins/blog/admin/templates/comments.html +11 -0
- package/plugins/blog/admin/templates/post-editor.html +97 -0
- package/plugins/blog/admin/templates/settings.html +11 -0
- package/plugins/blog/admin/views/blog.js +183 -0
- package/plugins/blog/admin/views/categories.js +235 -0
- package/plugins/blog/admin/views/comments.js +187 -0
- package/plugins/blog/admin/views/post-editor.js +291 -0
- package/plugins/blog/admin/views/settings.js +100 -0
- package/plugins/blog/collections/categories/schema.json +12 -0
- package/plugins/blog/collections/comments/schema.json +16 -0
- package/plugins/blog/collections/posts/schema.json +19 -0
- package/plugins/blog/config.js +8 -0
- package/plugins/blog/plugin.js +352 -0
- package/plugins/blog/plugin.json +96 -0
- package/plugins/blog/roles/blog-author.json +10 -0
- package/plugins/blog/roles/blog-editor.json +12 -0
- package/plugins/blog/templates/author.html +9 -0
- package/plugins/blog/templates/category.html +9 -0
- package/plugins/blog/templates/index.html +9 -0
- package/plugins/blog/templates/post.html +17 -0
- package/plugins/blog/templates/tag.html +9 -0
- package/plugins/contacts/collections/user-contact-groups/schema.json +1 -1
- package/plugins/contacts/collections/user-contacts/schema.json +1 -1
- package/plugins/contacts/plugin.js +4 -10
- package/plugins/contacts/plugin.json +13 -3
- package/plugins/notes/collections/user-notes/schema.json +1 -1
- package/plugins/notes/plugin.js +3 -9
- package/plugins/notes/plugin.json +13 -3
- package/plugins/site-search/plugin.json +5 -2
- package/plugins/theme-switcher/plugin.json +1 -1
- package/plugins/todo/collections/todos/schema.json +1 -1
- package/plugins/todo/plugin.js +3 -9
- package/plugins/todo/plugin.json +13 -3
- package/public/css/site.css +1 -1
- package/scripts/build.js +48 -0
- package/scripts/create-plugin.js +113 -0
- package/scripts/fresh.js +6 -7
- package/scripts/gen-instance-secret.js +46 -0
- package/scripts/reset.js +3 -3
- package/scripts/setup.js +31 -13
- package/server/middleware/auth.js +48 -0
- package/server/middleware/managerAuth.js +36 -0
- package/server/routes/api/actions.js +1 -1
- package/server/routes/api/auth.js +4 -3
- package/server/routes/api/layouts.js +173 -49
- package/server/routes/api/notifications.js +155 -0
- package/server/routes/api/plugin-marketplace.js +75 -0
- package/server/routes/api/users.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/public.js +4 -9
- package/server/server.js +32 -3
- package/server/services/actions.js +1 -1
- package/server/services/managerClient.js +182 -0
- package/server/services/permissionRegistry.js +245 -173
- package/server/services/pluginInstaller.js +301 -0
- package/server/services/plugins.js +117 -10
- package/server/services/presetCollections.js +66 -251
- package/server/services/renderer.js +99 -0
- package/server/services/roles.js +191 -39
- package/server/services/users.js +1 -1
- package/server/services/views.js +1 -1
- package/server/templates/page.html +2 -2
- package/plugins/docs/admin/templates/docs.html +0 -69
- package/plugins/docs/admin/views/docs.js +0 -276
- package/plugins/docs/config.js +0 -8
- package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +0 -11
- package/plugins/docs/data/folders.json +0 -9
- package/plugins/docs/data/templates.json +0 -1
- package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +0 -5
- package/plugins/docs/plugin.js +0 -375
- package/plugins/docs/plugin.json +0 -23
- package/plugins/form-builder/data/forms/contacts.json +0 -66
- package/plugins/form-builder/data/forms/enquiries.json +0 -103
- package/plugins/form-builder/data/forms/feedback.json +0 -131
- package/plugins/form-builder/data/forms/notes.json +0 -79
- package/plugins/form-builder/data/forms/to-do.json +0 -100
- package/plugins/form-builder/data/submissions/contacts.json +0 -1
- package/plugins/form-builder/data/submissions/enquiries.json +0 -1
- package/plugins/form-builder/data/submissions/feedback.json +0 -1
- package/plugins/form-builder/data/submissions/notes.json +0 -1
- package/plugins/form-builder/data/submissions/to-do.json +0 -1
- package/plugins/garage/admin/templates/garage.html +0 -111
- package/plugins/garage/admin/views/garage.js +0 -622
- package/plugins/garage/collections/garage-vehicles/schema.json +0 -101
- package/plugins/garage/config.js +0 -18
- package/plugins/garage/data/vehicles.json +0 -70
- package/plugins/garage/plugin.js +0 -398
- package/plugins/garage/plugin.json +0 -33
- package/scripts/seed.js +0 -1996
- package/server/services/userTypes.js +0 -227
|
@@ -1,622 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Garage Plugin — Admin View
|
|
3
|
-
*
|
|
4
|
-
* Provides the Garage admin UI: UK vehicle lookup via DVLA API, saved vehicle
|
|
5
|
-
* management ("My Garage"), and full lookup history.
|
|
6
|
-
*
|
|
7
|
-
* Pattern: follows contacts plugin conventions.
|
|
8
|
-
* - api() — fetch wrapper with CMS Bearer token
|
|
9
|
-
* - buildVehicleCard() — DOM-built result card (preserves interactive buttons)
|
|
10
|
-
* - statusBadge() — colour-coded MOT/tax status badge
|
|
11
|
-
* - loadSaved() — populates My Garage tab via T.create
|
|
12
|
-
* - loadHistory() — populates History tab via T.create with D().fromNow()
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const BASE = '/api/plugins/garage';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Authenticated fetch helper.
|
|
19
|
-
*
|
|
20
|
-
* @param {string} url
|
|
21
|
-
* @param {string} [method]
|
|
22
|
-
* @param {object} [body]
|
|
23
|
-
* @returns {Promise<any>}
|
|
24
|
-
*/
|
|
25
|
-
async function api(url, method = 'GET', body) {
|
|
26
|
-
const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
|
|
27
|
-
if (body !== undefined) {
|
|
28
|
-
opts.headers['Content-Type'] = 'application/json';
|
|
29
|
-
opts.body = JSON.stringify(body);
|
|
30
|
-
}
|
|
31
|
-
const res = await fetch(url, opts);
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
const err = await res.json().catch(() => ({error: res.statusText}));
|
|
34
|
-
throw new Error(err.error || res.statusText);
|
|
35
|
-
}
|
|
36
|
-
const text = await res.text();
|
|
37
|
-
return text ? JSON.parse(text) : {};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Escape HTML special characters to prevent XSS in T.create render functions.
|
|
42
|
-
*
|
|
43
|
-
* @param {*} str
|
|
44
|
-
* @returns {string}
|
|
45
|
-
*/
|
|
46
|
-
function esc(str) {
|
|
47
|
-
return String(str ?? '')
|
|
48
|
-
.replace(/&/g, '&')
|
|
49
|
-
.replace(/</g, '<')
|
|
50
|
-
.replace(/>/g, '>')
|
|
51
|
-
.replace(/"/g, '"')
|
|
52
|
-
.replace(/'/g, ''');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Build a colour-coded status badge element.
|
|
57
|
-
*
|
|
58
|
-
* @param {string} label - e.g. "MOT" or "Tax"
|
|
59
|
-
* @param {string} value - e.g. "Valid", "SORN", "Not valid"
|
|
60
|
-
* @returns {HTMLElement}
|
|
61
|
-
*/
|
|
62
|
-
function statusBadge(label, value) {
|
|
63
|
-
const span = document.createElement('span');
|
|
64
|
-
const v = (value || '').toLowerCase();
|
|
65
|
-
let cls = 'badge-secondary';
|
|
66
|
-
if (v === 'valid' || v === 'taxed') cls = 'badge-success';
|
|
67
|
-
else if (v === 'sorn') cls = 'badge-warning';
|
|
68
|
-
else if (v.includes('not valid') || v === 'untaxed') cls = 'badge-danger';
|
|
69
|
-
span.className = `badge ${cls}`;
|
|
70
|
-
span.textContent = `${label}: ${value || 'Unknown'}`;
|
|
71
|
-
return span;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Format a date string using D() (Moment-style), falling back gracefully.
|
|
76
|
-
*
|
|
77
|
-
* @param {string|null} dateStr
|
|
78
|
-
* @returns {string}
|
|
79
|
-
*/
|
|
80
|
-
function formatDate(dateStr) {
|
|
81
|
-
if (!dateStr) return '—';
|
|
82
|
-
try {
|
|
83
|
-
return D(dateStr).format('D MMM YYYY');
|
|
84
|
-
} catch {
|
|
85
|
-
return dateStr;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Build a badge HTML string for T.create render functions (values are escaped).
|
|
91
|
-
*
|
|
92
|
-
* @param {string} value
|
|
93
|
-
* @param {'success'|'warning'|'danger'|'secondary'} cls
|
|
94
|
-
* @returns {string}
|
|
95
|
-
*/
|
|
96
|
-
function badgeHtml(value, cls) {
|
|
97
|
-
return `<span class="badge badge-${cls}">${esc(value || '—')}</span>`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Resolve a badge class from a MOT status string.
|
|
102
|
-
*
|
|
103
|
-
* @param {string} v
|
|
104
|
-
* @returns {string}
|
|
105
|
-
*/
|
|
106
|
-
function motBadgeCls(v) {
|
|
107
|
-
if ((v || '').toLowerCase() === 'valid') return 'success';
|
|
108
|
-
if (v) return 'danger';
|
|
109
|
-
return 'secondary';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Resolve a badge class from a tax status string.
|
|
114
|
-
*
|
|
115
|
-
* @param {string} v
|
|
116
|
-
* @returns {string}
|
|
117
|
-
*/
|
|
118
|
-
function taxBadgeCls(v) {
|
|
119
|
-
const lv = (v || '').toLowerCase();
|
|
120
|
-
if (lv === 'taxed') return 'success';
|
|
121
|
-
if (lv === 'sorn') return 'warning';
|
|
122
|
-
if (v) return 'danger';
|
|
123
|
-
return 'secondary';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Plate-style HTML span for use inside T.create render functions. */
|
|
127
|
-
function plateHtml(reg) {
|
|
128
|
-
return `<span style="background:#FFD900;color:#000;font-family:'Arial Black',Arial,sans-serif;font-size:0.85rem;font-weight:900;letter-spacing:2px;padding:0.1rem 0.4rem;border-radius:3px;border:2px solid #000;">${esc(reg)}</span>`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export const garageView = {
|
|
132
|
-
templateUrl: '/plugins/garage/admin/templates/garage.html',
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Mount the Garage admin view.
|
|
136
|
-
*
|
|
137
|
-
* @param {object} $container - Domma-wrapped container element
|
|
138
|
-
*/
|
|
139
|
-
async onMount($container) {
|
|
140
|
-
|
|
141
|
-
// ---------------------------------------------------------------
|
|
142
|
-
// State
|
|
143
|
-
// ---------------------------------------------------------------
|
|
144
|
-
let savedVehicles = [];
|
|
145
|
-
let historyVehicles = [];
|
|
146
|
-
|
|
147
|
-
// ---------------------------------------------------------------
|
|
148
|
-
// DOM refs
|
|
149
|
-
// ---------------------------------------------------------------
|
|
150
|
-
const regInput = $container.find('#reg-input').get(0);
|
|
151
|
-
const lookupBtn = $container.find('#lookup-btn').get(0);
|
|
152
|
-
const rateLimitInfo = $container.find('#rate-limit-info').get(0);
|
|
153
|
-
const lookupError = $container.find('#lookup-error').get(0);
|
|
154
|
-
const vehicleResult = $container.find('#vehicle-result').get(0);
|
|
155
|
-
const resultEmpty = $container.find('#result-empty').get(0);
|
|
156
|
-
const garageCount = $container.find('#garage-count').get(0);
|
|
157
|
-
const garageTableEl = $container.find('#garage-table').get(0);
|
|
158
|
-
const garageEmpty = $container.find('#garage-empty').get(0);
|
|
159
|
-
const historyCount = $container.find('#history-count').get(0);
|
|
160
|
-
const historyTableEl = $container.find('#history-table').get(0);
|
|
161
|
-
const historyEmpty = $container.find('#history-empty').get(0);
|
|
162
|
-
const clearHistoryBtn = $container.find('#clear-history-btn').get(0);
|
|
163
|
-
const tabBtnGarage = $container.find('#tab-btn-garage').get(0);
|
|
164
|
-
const tabBtnHistory = $container.find('#tab-btn-history').get(0);
|
|
165
|
-
const settingsApiKeyInput = $container.find('#settings-api-key').get(0);
|
|
166
|
-
const settingsSaveBtn = $container.find('#settings-save-btn').get(0);
|
|
167
|
-
const settingsKeyStatus = $container.find('#settings-key-status').get(0);
|
|
168
|
-
|
|
169
|
-
// ---------------------------------------------------------------
|
|
170
|
-
// Tabs
|
|
171
|
-
// ---------------------------------------------------------------
|
|
172
|
-
E.tabs($container.find('#garage-tabs').get(0));
|
|
173
|
-
|
|
174
|
-
// ---------------------------------------------------------------
|
|
175
|
-
// Build vehicle detail card (DOM construction — preserves buttons)
|
|
176
|
-
// ---------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Builds a full vehicle detail card as a DOM node.
|
|
180
|
-
* Uses createElement throughout so interactive buttons are preserved
|
|
181
|
-
* (Domma's .html() strips buttons via DOMPurify).
|
|
182
|
-
*
|
|
183
|
-
* @param {object} vehicle
|
|
184
|
-
* @param {Function} [onSaveToggle] - optional callback after save toggle
|
|
185
|
-
* @returns {HTMLElement}
|
|
186
|
-
*/
|
|
187
|
-
function buildVehicleCard(vehicle, onSaveToggle) {
|
|
188
|
-
const card = document.createElement('div');
|
|
189
|
-
card.className = 'card';
|
|
190
|
-
card.style.cssText = 'max-width:560px;';
|
|
191
|
-
|
|
192
|
-
// Card header — UK plate style registration + make/year
|
|
193
|
-
const header = document.createElement('div');
|
|
194
|
-
header.className = 'card-header';
|
|
195
|
-
header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:1rem;';
|
|
196
|
-
|
|
197
|
-
const plate = document.createElement('span');
|
|
198
|
-
plate.style.cssText = 'background:#FFD900;color:#000;font-family:\'Arial Black\',Arial,sans-serif;font-weight:900;font-size:1.1rem;letter-spacing:3px;padding:0.2rem 0.6rem;border-radius:4px;border:2px solid #000;';
|
|
199
|
-
plate.textContent = vehicle.registrationNumber || '—';
|
|
200
|
-
|
|
201
|
-
const makeYear = document.createElement('span');
|
|
202
|
-
makeYear.style.cssText = 'font-size:0.9rem;color:var(--dm-text-muted,#888);';
|
|
203
|
-
makeYear.textContent = [vehicle.make, vehicle.yearOfManufacture].filter(Boolean).join(' · ');
|
|
204
|
-
|
|
205
|
-
header.appendChild(plate);
|
|
206
|
-
header.appendChild(makeYear);
|
|
207
|
-
card.appendChild(header);
|
|
208
|
-
|
|
209
|
-
// Card body
|
|
210
|
-
const body = document.createElement('div');
|
|
211
|
-
body.className = 'card-body';
|
|
212
|
-
|
|
213
|
-
// Detail grid
|
|
214
|
-
const grid = document.createElement('div');
|
|
215
|
-
grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:0.4rem 1rem;font-size:0.88rem;margin-bottom:0.75rem;';
|
|
216
|
-
|
|
217
|
-
[
|
|
218
|
-
['Colour', vehicle.colour],
|
|
219
|
-
['Fuel', vehicle.fuelType],
|
|
220
|
-
['Engine', vehicle.engineCapacity ? `${vehicle.engineCapacity}cc` : null],
|
|
221
|
-
['CO₂', vehicle.co2Emissions ? `${vehicle.co2Emissions}g/km` : null],
|
|
222
|
-
].forEach(([label, value]) => {
|
|
223
|
-
if (!value) return;
|
|
224
|
-
const item = document.createElement('div');
|
|
225
|
-
const lbl = document.createElement('span');
|
|
226
|
-
lbl.style.color = 'var(--dm-text-muted,#888)';
|
|
227
|
-
lbl.textContent = label + ': ';
|
|
228
|
-
const val = document.createElement('strong');
|
|
229
|
-
val.textContent = value;
|
|
230
|
-
item.appendChild(lbl);
|
|
231
|
-
item.appendChild(val);
|
|
232
|
-
grid.appendChild(item);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
body.appendChild(grid);
|
|
236
|
-
|
|
237
|
-
// Status badges
|
|
238
|
-
const badges = document.createElement('div');
|
|
239
|
-
badges.style.cssText = 'display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;margin-bottom:0.75rem;';
|
|
240
|
-
badges.appendChild(statusBadge('MOT', vehicle.motStatus));
|
|
241
|
-
if (vehicle.motExpiryDate) {
|
|
242
|
-
const expiry = document.createElement('span');
|
|
243
|
-
expiry.style.cssText = 'font-size:0.8rem;color:var(--dm-text-muted,#888);';
|
|
244
|
-
expiry.textContent = `Expires ${formatDate(vehicle.motExpiryDate)}`;
|
|
245
|
-
badges.appendChild(expiry);
|
|
246
|
-
}
|
|
247
|
-
badges.appendChild(statusBadge('Tax', vehicle.taxStatus));
|
|
248
|
-
if (vehicle.taxDueDate) {
|
|
249
|
-
const due = document.createElement('span');
|
|
250
|
-
due.style.cssText = 'font-size:0.8rem;color:var(--dm-text-muted,#888);';
|
|
251
|
-
due.textContent = `Due ${formatDate(vehicle.taxDueDate)}`;
|
|
252
|
-
badges.appendChild(due);
|
|
253
|
-
}
|
|
254
|
-
body.appendChild(badges);
|
|
255
|
-
|
|
256
|
-
// Save / Unsave button
|
|
257
|
-
const saveBtn = document.createElement('button');
|
|
258
|
-
saveBtn.className = vehicle.isSaved ? 'btn btn-sm btn-warning' : 'btn btn-sm btn-success';
|
|
259
|
-
const saveIconEl = document.createElement('span');
|
|
260
|
-
saveIconEl.setAttribute('data-icon', vehicle.isSaved ? 'bookmark-minus' : 'bookmark-plus');
|
|
261
|
-
const saveTxt = document.createTextNode(' ' + (vehicle.isSaved ? 'Remove from Garage' : 'Save to Garage'));
|
|
262
|
-
saveBtn.appendChild(saveIconEl);
|
|
263
|
-
saveBtn.appendChild(saveTxt);
|
|
264
|
-
|
|
265
|
-
saveBtn.addEventListener('click', async () => {
|
|
266
|
-
try {
|
|
267
|
-
const updated = await api(`${BASE}/vehicles/${vehicle.id}/save`, 'PATCH');
|
|
268
|
-
vehicle.isSaved = updated.isSaved;
|
|
269
|
-
saveBtn.className = updated.isSaved ? 'btn btn-sm btn-warning' : 'btn btn-sm btn-success';
|
|
270
|
-
saveIconEl.setAttribute('data-icon', updated.isSaved ? 'bookmark-minus' : 'bookmark-plus');
|
|
271
|
-
saveTxt.textContent = ' ' + (updated.isSaved ? 'Remove from Garage' : 'Save to Garage');
|
|
272
|
-
E.toast(updated.isSaved ? 'Saved to Garage' : 'Removed from Garage', {type: 'success'});
|
|
273
|
-
Domma.icons.scan();
|
|
274
|
-
if (onSaveToggle) onSaveToggle(updated);
|
|
275
|
-
} catch (err) {
|
|
276
|
-
E.toast(err.message, {type: 'error'});
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
body.appendChild(saveBtn);
|
|
281
|
-
card.appendChild(body);
|
|
282
|
-
return card;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// ---------------------------------------------------------------
|
|
286
|
-
// Lookup
|
|
287
|
-
// ---------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
function setLookupError(msg) {
|
|
290
|
-
if (msg) {
|
|
291
|
-
lookupError.textContent = msg;
|
|
292
|
-
lookupError.style.display = '';
|
|
293
|
-
} else {
|
|
294
|
-
lookupError.style.display = 'none';
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async function doLookup() {
|
|
299
|
-
const reg = (regInput.value || '').trim();
|
|
300
|
-
if (!reg) {
|
|
301
|
-
regInput.focus();
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
setLookupError(null);
|
|
306
|
-
rateLimitInfo.textContent = '';
|
|
307
|
-
lookupBtn.disabled = true;
|
|
308
|
-
lookupBtn.textContent = 'Looking up…';
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
const vehicle = await api(`${BASE}/lookup`, 'POST', {registrationNumber: reg});
|
|
312
|
-
|
|
313
|
-
// Show vehicle card in Results tab
|
|
314
|
-
while (vehicleResult.firstChild) vehicleResult.removeChild(vehicleResult.firstChild);
|
|
315
|
-
vehicleResult.appendChild(buildVehicleCard(vehicle, async () => {
|
|
316
|
-
await loadSaved();
|
|
317
|
-
await loadHistory();
|
|
318
|
-
}));
|
|
319
|
-
vehicleResult.style.display = '';
|
|
320
|
-
resultEmpty.style.display = 'none';
|
|
321
|
-
Domma.icons.scan();
|
|
322
|
-
|
|
323
|
-
loadHistory().catch(() => {
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
} catch (err) {
|
|
327
|
-
if (err.message && err.message.toLowerCase().includes('rate limit')) {
|
|
328
|
-
rateLimitInfo.textContent = err.message;
|
|
329
|
-
} else {
|
|
330
|
-
setLookupError(err.message);
|
|
331
|
-
}
|
|
332
|
-
} finally {
|
|
333
|
-
lookupBtn.disabled = false;
|
|
334
|
-
const icon = document.createElement('span');
|
|
335
|
-
icon.setAttribute('data-icon', 'search');
|
|
336
|
-
lookupBtn.textContent = '';
|
|
337
|
-
lookupBtn.appendChild(icon);
|
|
338
|
-
lookupBtn.appendChild(document.createTextNode(' Look Up'));
|
|
339
|
-
Domma.icons.scan();
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
lookupBtn.addEventListener('click', doLookup);
|
|
344
|
-
regInput.addEventListener('keydown', (e) => {
|
|
345
|
-
if (e.key === 'Enter') doLookup();
|
|
346
|
-
});
|
|
347
|
-
regInput.addEventListener('input', () => {
|
|
348
|
-
const pos = regInput.selectionStart;
|
|
349
|
-
regInput.value = regInput.value.toUpperCase();
|
|
350
|
-
regInput.setSelectionRange(pos, pos);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// ---------------------------------------------------------------
|
|
354
|
-
// My Garage (saved vehicles)
|
|
355
|
-
// ---------------------------------------------------------------
|
|
356
|
-
|
|
357
|
-
async function loadSaved() {
|
|
358
|
-
try {
|
|
359
|
-
savedVehicles = await api(`${BASE}/vehicles`);
|
|
360
|
-
} catch (err) {
|
|
361
|
-
E.toast('Could not load saved vehicles: ' + err.message, {type: 'error'});
|
|
362
|
-
savedVehicles = [];
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
try {
|
|
366
|
-
const empty = savedVehicles.length === 0;
|
|
367
|
-
garageEmpty.style.display = empty ? '' : 'none';
|
|
368
|
-
garageTableEl.style.display = empty ? 'none' : '';
|
|
369
|
-
garageCount.textContent = savedVehicles.length
|
|
370
|
-
? `${savedVehicles.length} vehicle${savedVehicles.length === 1 ? '' : 's'}`
|
|
371
|
-
: '';
|
|
372
|
-
if (tabBtnGarage) tabBtnGarage.innerHTML = savedVehicles.length
|
|
373
|
-
? `My Garage <span class="badge badge-primary" style="margin-left:0.35rem;">${savedVehicles.length}</span>`
|
|
374
|
-
: 'My Garage';
|
|
375
|
-
|
|
376
|
-
if (!empty) {
|
|
377
|
-
T.create('#garage-table', {
|
|
378
|
-
data: savedVehicles,
|
|
379
|
-
columns: [
|
|
380
|
-
{key: 'registrationNumber', title: 'Registration', render: (v) => plateHtml(v)},
|
|
381
|
-
{key: 'make', title: 'Make', render: (v) => esc(v)},
|
|
382
|
-
{key: 'colour', title: 'Colour', render: (v) => esc(v)},
|
|
383
|
-
{key: 'yearOfManufacture', title: 'Year', render: (v) => esc(v)},
|
|
384
|
-
{
|
|
385
|
-
key: 'motStatus', title: 'MOT',
|
|
386
|
-
render: (v, row) => badgeHtml(v, motBadgeCls(v)) +
|
|
387
|
-
(row && row.motExpiryDate ? `<br><span style="font-size:0.78rem;color:var(--dm-text-muted,#888);">Exp ${formatDate(row.motExpiryDate)}</span>` : '')
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
key: 'taxStatus', title: 'Road Tax',
|
|
391
|
-
render: (v, row) => badgeHtml(v, taxBadgeCls(v)) +
|
|
392
|
-
(row && row.taxDueDate ? `<br><span style="font-size:0.78rem;color:var(--dm-text-muted,#888);">Due ${formatDate(row.taxDueDate)}</span>` : '')
|
|
393
|
-
},
|
|
394
|
-
{
|
|
395
|
-
key: 'id', title: 'Actions',
|
|
396
|
-
render: (id) => `<button class="btn btn-xs btn-outline garage-unsave-btn" data-id="${esc(id)}" title="Remove from Garage"><span data-icon="bookmark-minus"></span> Remove</button>`
|
|
397
|
-
}
|
|
398
|
-
]
|
|
399
|
-
});
|
|
400
|
-
Domma.icons.scan();
|
|
401
|
-
}
|
|
402
|
-
} catch (err) {
|
|
403
|
-
console.error('[garage] loadSaved render error:', err);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
garageTableEl.addEventListener('click', async (e) => {
|
|
408
|
-
const unsaveBtn = e.target.closest('.garage-unsave-btn');
|
|
409
|
-
if (!unsaveBtn) return;
|
|
410
|
-
const id = unsaveBtn.dataset.id;
|
|
411
|
-
try {
|
|
412
|
-
await api(`${BASE}/vehicles/${id}/save`, 'PATCH');
|
|
413
|
-
E.toast('Removed from Garage', {type: 'success'});
|
|
414
|
-
await loadSaved();
|
|
415
|
-
await loadHistory();
|
|
416
|
-
} catch (err) {
|
|
417
|
-
E.toast(err.message, {type: 'error'});
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
// ---------------------------------------------------------------
|
|
422
|
-
// History
|
|
423
|
-
// ---------------------------------------------------------------
|
|
424
|
-
|
|
425
|
-
async function loadHistory() {
|
|
426
|
-
try {
|
|
427
|
-
historyVehicles = await api(`${BASE}/history`);
|
|
428
|
-
} catch (err) {
|
|
429
|
-
E.toast('Could not load history: ' + err.message, {type: 'error'});
|
|
430
|
-
historyVehicles = [];
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
try {
|
|
434
|
-
const empty = historyVehicles.length === 0;
|
|
435
|
-
historyEmpty.style.display = empty ? '' : 'none';
|
|
436
|
-
historyTableEl.style.display = empty ? 'none' : '';
|
|
437
|
-
historyCount.textContent = historyVehicles.length
|
|
438
|
-
? `${historyVehicles.length} lookup${historyVehicles.length === 1 ? '' : 's'}`
|
|
439
|
-
: '';
|
|
440
|
-
if (tabBtnHistory) tabBtnHistory.innerHTML = historyVehicles.length
|
|
441
|
-
? `History <span class="badge badge-secondary" style="margin-left:0.35rem;">${historyVehicles.length}</span>`
|
|
442
|
-
: 'History';
|
|
443
|
-
|
|
444
|
-
if (!empty) {
|
|
445
|
-
T.create('#history-table', {
|
|
446
|
-
data: historyVehicles,
|
|
447
|
-
columns: [
|
|
448
|
-
{key: 'registrationNumber', title: 'Registration', render: (v) => plateHtml(v)},
|
|
449
|
-
{key: 'make', title: 'Make', render: (v) => esc(v)},
|
|
450
|
-
{key: 'yearOfManufacture', title: 'Year', render: (v) => esc(v)},
|
|
451
|
-
{
|
|
452
|
-
key: 'lookupDate', title: 'Last Checked',
|
|
453
|
-
render: (v) => {
|
|
454
|
-
if (!v) return '—';
|
|
455
|
-
try {
|
|
456
|
-
return esc(D(v).fromNow());
|
|
457
|
-
} catch {
|
|
458
|
-
return esc(v);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
key: 'motStatus', title: 'MOT',
|
|
464
|
-
render: (v, row) => badgeHtml(v, motBadgeCls(v)) +
|
|
465
|
-
(row && row.motExpiryDate ? `<br><span style="font-size:0.78rem;color:var(--dm-text-muted,#888);">Exp ${formatDate(row.motExpiryDate)}</span>` : '')
|
|
466
|
-
},
|
|
467
|
-
{
|
|
468
|
-
key: 'taxStatus', title: 'Road Tax',
|
|
469
|
-
render: (v, row) => badgeHtml(v, taxBadgeCls(v)) +
|
|
470
|
-
(row && row.taxDueDate ? `<br><span style="font-size:0.78rem;color:var(--dm-text-muted,#888);">Due ${formatDate(row.taxDueDate)}</span>` : '')
|
|
471
|
-
},
|
|
472
|
-
{
|
|
473
|
-
key: 'isSaved', title: 'In Garage',
|
|
474
|
-
render: (v) => v
|
|
475
|
-
? '<span class="badge badge-success">Saved</span>'
|
|
476
|
-
: '<span class="badge badge-secondary">No</span>'
|
|
477
|
-
},
|
|
478
|
-
{
|
|
479
|
-
key: 'id', title: 'Actions',
|
|
480
|
-
render: (id, row) => {
|
|
481
|
-
const saved = row && row.isSaved;
|
|
482
|
-
return `<div style="display:flex;gap:0.4rem;">
|
|
483
|
-
<button class="btn btn-xs ${saved ? 'btn-warning' : 'btn-outline'} history-save-btn" data-id="${esc(id)}" title="${saved ? 'Remove from Garage' : 'Save to Garage'}">
|
|
484
|
-
<span data-icon="${saved ? 'bookmark-minus' : 'bookmark-plus'}"></span>
|
|
485
|
-
${saved ? 'Remove' : 'Save'}
|
|
486
|
-
</button>
|
|
487
|
-
<button class="btn btn-xs btn-danger history-delete-btn" data-id="${esc(id)}" title="Delete">
|
|
488
|
-
<span data-icon="trash"></span>
|
|
489
|
-
</button>
|
|
490
|
-
</div>`;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
]
|
|
494
|
-
});
|
|
495
|
-
Domma.icons.scan();
|
|
496
|
-
}
|
|
497
|
-
} catch (err) {
|
|
498
|
-
console.error('[garage] loadHistory render error:', err);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
historyTableEl.addEventListener('click', async (e) => {
|
|
503
|
-
const saveBtn = e.target.closest('.history-save-btn');
|
|
504
|
-
const deleteBtn = e.target.closest('.history-delete-btn');
|
|
505
|
-
if (!saveBtn && !deleteBtn) return;
|
|
506
|
-
const id = (saveBtn || deleteBtn).dataset.id;
|
|
507
|
-
if (saveBtn) {
|
|
508
|
-
try {
|
|
509
|
-
const updated = await api(`${BASE}/vehicles/${id}/save`, 'PATCH');
|
|
510
|
-
E.toast(updated.isSaved ? 'Saved to Garage' : 'Removed from Garage', {type: 'success'});
|
|
511
|
-
await loadSaved();
|
|
512
|
-
await loadHistory();
|
|
513
|
-
} catch (err) {
|
|
514
|
-
E.toast(err.message, {type: 'error'});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
if (deleteBtn) {
|
|
518
|
-
const confirmed = await E.confirm('Delete this lookup from your history?');
|
|
519
|
-
if (!confirmed) return;
|
|
520
|
-
try {
|
|
521
|
-
await api(`${BASE}/vehicles/${id}`, 'DELETE');
|
|
522
|
-
E.toast('Deleted', {type: 'success'});
|
|
523
|
-
await loadSaved();
|
|
524
|
-
await loadHistory();
|
|
525
|
-
} catch (err) {
|
|
526
|
-
E.toast(err.message, {type: 'error'});
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// ---------------------------------------------------------------
|
|
532
|
-
// Clear History
|
|
533
|
-
// ---------------------------------------------------------------
|
|
534
|
-
|
|
535
|
-
clearHistoryBtn.addEventListener('click', async () => {
|
|
536
|
-
const unsaved = historyVehicles.filter(v => !v.isSaved);
|
|
537
|
-
if (unsaved.length === 0) {
|
|
538
|
-
E.toast('No unsaved history to clear — saved vehicles are kept.', {type: 'info'});
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
const confirmed = await E.confirm(
|
|
542
|
-
`Delete ${unsaved.length} unsaved lookup${unsaved.length === 1 ? '' : 's'} from history? Saved vehicles will not be affected.`
|
|
543
|
-
);
|
|
544
|
-
if (!confirmed) return;
|
|
545
|
-
|
|
546
|
-
let deleted = 0;
|
|
547
|
-
for (const v of unsaved) {
|
|
548
|
-
try {
|
|
549
|
-
await api(`${BASE}/vehicles/${v.id}`, 'DELETE');
|
|
550
|
-
deleted++;
|
|
551
|
-
} catch { /* continue */
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
E.toast(`Cleared ${deleted} history item${deleted === 1 ? '' : 's'}.`, {type: 'success'});
|
|
555
|
-
await loadHistory();
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
// ---------------------------------------------------------------
|
|
559
|
-
// Settings
|
|
560
|
-
// ---------------------------------------------------------------
|
|
561
|
-
|
|
562
|
-
let currentSettings = {};
|
|
563
|
-
|
|
564
|
-
async function loadSettings() {
|
|
565
|
-
try {
|
|
566
|
-
const plugins = await api('/api/plugins');
|
|
567
|
-
const garage = plugins.find(p => p.name === 'garage');
|
|
568
|
-
currentSettings = garage?.settings || {};
|
|
569
|
-
const hasKey = !!(currentSettings.dvlaApiKey);
|
|
570
|
-
const badge = document.createElement('span');
|
|
571
|
-
badge.className = hasKey ? 'badge badge-success' : 'badge badge-warning';
|
|
572
|
-
badge.textContent = hasKey
|
|
573
|
-
? 'API key configured'
|
|
574
|
-
: 'No API key — lookups will fail until one is saved';
|
|
575
|
-
settingsKeyStatus.textContent = '';
|
|
576
|
-
settingsKeyStatus.appendChild(badge);
|
|
577
|
-
} catch (err) {
|
|
578
|
-
settingsKeyStatus.textContent = 'Could not load settings.';
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
settingsSaveBtn.addEventListener('click', async () => {
|
|
583
|
-
const key = (settingsApiKeyInput.value || '').trim();
|
|
584
|
-
if (!key) {
|
|
585
|
-
E.toast('Please enter an API key.', {type: 'warning'});
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
settingsSaveBtn.disabled = true;
|
|
590
|
-
try {
|
|
591
|
-
await api('/api/plugins/garage', 'PUT', {
|
|
592
|
-
enabled: true,
|
|
593
|
-
settings: {...currentSettings, dvlaApiKey: key}
|
|
594
|
-
});
|
|
595
|
-
currentSettings.dvlaApiKey = key;
|
|
596
|
-
settingsApiKeyInput.value = '';
|
|
597
|
-
const badge = document.createElement('span');
|
|
598
|
-
badge.className = 'badge badge-success';
|
|
599
|
-
badge.textContent = 'API key configured';
|
|
600
|
-
settingsKeyStatus.textContent = '';
|
|
601
|
-
settingsKeyStatus.appendChild(badge);
|
|
602
|
-
E.toast('Settings saved.', {type: 'success'});
|
|
603
|
-
} catch (err) {
|
|
604
|
-
E.toast('Failed to save: ' + err.message, {type: 'error'});
|
|
605
|
-
} finally {
|
|
606
|
-
settingsSaveBtn.disabled = false;
|
|
607
|
-
const icon = document.createElement('span');
|
|
608
|
-
icon.setAttribute('data-icon', 'save');
|
|
609
|
-
settingsSaveBtn.textContent = '';
|
|
610
|
-
settingsSaveBtn.appendChild(icon);
|
|
611
|
-
settingsSaveBtn.appendChild(document.createTextNode(' Save'));
|
|
612
|
-
Domma.icons.scan();
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
// ---------------------------------------------------------------
|
|
617
|
-
// Initial load
|
|
618
|
-
// ---------------------------------------------------------------
|
|
619
|
-
await Promise.all([loadSaved(), loadHistory(), loadSettings()]);
|
|
620
|
-
Domma.icons.scan();
|
|
621
|
-
}
|
|
622
|
-
};
|