@developer.krd/discord-dashboard 0.1.3 → 0.1.7
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 +145 -136
- package/dist/index.cjs +2453 -2025
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +292 -125
- package/dist/index.d.ts +292 -125
- package/dist/index.js +2448 -2023
- package/dist/index.js.map +1 -1
- package/package.json +11 -2
package/dist/index.cjs
CHANGED
|
@@ -31,18 +31,44 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
DashboardDesigner: () => DashboardDesigner,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
DashboardEngine: () => DashboardEngine,
|
|
35
|
+
SessionSchema: () => SessionSchema,
|
|
36
|
+
createElysiaAdapter: () => createElysiaAdapter,
|
|
37
|
+
createExpressAdapter: () => createExpressAdapter,
|
|
38
|
+
createFastifyAdapter: () => createFastifyAdapter
|
|
38
39
|
});
|
|
39
40
|
module.exports = __toCommonJS(index_exports);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
var
|
|
43
|
-
var
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
|
|
42
|
+
// src/Types/elysia.ts
|
|
43
|
+
var import_elysia = require("elysia");
|
|
44
|
+
var UserSchema = import_elysia.t.Object({
|
|
45
|
+
id: import_elysia.t.String(),
|
|
46
|
+
username: import_elysia.t.String(),
|
|
47
|
+
discriminator: import_elysia.t.String(),
|
|
48
|
+
avatar: import_elysia.t.Nullable(import_elysia.t.String()),
|
|
49
|
+
global_name: import_elysia.t.Optional(import_elysia.t.Nullable(import_elysia.t.String())),
|
|
50
|
+
avatarUrl: import_elysia.t.Optional(import_elysia.t.Nullable(import_elysia.t.String()))
|
|
51
|
+
});
|
|
52
|
+
var GuildSchema = import_elysia.t.Object({
|
|
53
|
+
id: import_elysia.t.String(),
|
|
54
|
+
name: import_elysia.t.String(),
|
|
55
|
+
icon: import_elysia.t.Nullable(import_elysia.t.String()),
|
|
56
|
+
owner: import_elysia.t.Boolean(),
|
|
57
|
+
permissions: import_elysia.t.String(),
|
|
58
|
+
iconUrl: import_elysia.t.Optional(import_elysia.t.Nullable(import_elysia.t.String())),
|
|
59
|
+
botInGuild: import_elysia.t.Optional(import_elysia.t.Boolean()),
|
|
60
|
+
inviteUrl: import_elysia.t.Optional(import_elysia.t.String())
|
|
61
|
+
});
|
|
62
|
+
var SessionSchema = import_elysia.t.Object({
|
|
63
|
+
oauthState: import_elysia.t.Optional(import_elysia.t.String()),
|
|
64
|
+
discordAuth: import_elysia.t.Optional(
|
|
65
|
+
import_elysia.t.Object({
|
|
66
|
+
accessToken: import_elysia.t.String(),
|
|
67
|
+
user: UserSchema,
|
|
68
|
+
guilds: import_elysia.t.Array(GuildSchema)
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
});
|
|
46
72
|
|
|
47
73
|
// src/handlers/DiscordHelpers.ts
|
|
48
74
|
var DiscordHelpers = class {
|
|
@@ -120,2234 +146,2636 @@ var DiscordHelpers = class {
|
|
|
120
146
|
async getGuildMember(guildId, userId) {
|
|
121
147
|
return await this.fetchDiscordWithBot(`/guilds/${guildId}/members/${userId}`);
|
|
122
148
|
}
|
|
149
|
+
getGuildIconUrl(guildId, iconHash) {
|
|
150
|
+
if (!iconHash) return null;
|
|
151
|
+
const extension = iconHash.startsWith("a_") ? "gif" : "png";
|
|
152
|
+
return `https://cdn.discordapp.com/icons/${guildId}/${iconHash}.${extension}?size=128`;
|
|
153
|
+
}
|
|
154
|
+
getUserAvatarUrl(userId, avatarHash) {
|
|
155
|
+
if (!avatarHash) return null;
|
|
156
|
+
const extension = avatarHash.startsWith("a_") ? "gif" : "png";
|
|
157
|
+
return `https://cdn.discordapp.com/avatars/${userId}/${avatarHash}.${extension}?size=128`;
|
|
158
|
+
}
|
|
123
159
|
};
|
|
124
160
|
|
|
125
|
-
// src/templates/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.
|
|
270
|
-
.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.
|
|
346
|
-
.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
161
|
+
// src/templates/scripts/client.ts
|
|
162
|
+
function getClientScript(basePath, setupDesign = {}) {
|
|
163
|
+
const scriptData = JSON.stringify({ basePath, setupDesign });
|
|
164
|
+
return `
|
|
165
|
+
const dashboardConfig = ${scriptData};
|
|
166
|
+
const state = {
|
|
167
|
+
session: null,
|
|
168
|
+
guilds: [],
|
|
169
|
+
selectedGuildId: null,
|
|
170
|
+
homeCategories: [],
|
|
171
|
+
selectedHomeCategoryId: null,
|
|
172
|
+
activeMainTab: "home"
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const el = {
|
|
176
|
+
serverRail: document.getElementById("serverRail"),
|
|
177
|
+
userMeta: document.getElementById("userMeta"),
|
|
178
|
+
centerTitle: document.getElementById("centerTitle"),
|
|
179
|
+
tabHome: document.getElementById("tabHome"),
|
|
180
|
+
tabPlugins: document.getElementById("tabPlugins"),
|
|
181
|
+
homeArea: document.getElementById("homeArea"),
|
|
182
|
+
pluginsArea: document.getElementById("pluginsArea"),
|
|
183
|
+
homeCategories: document.getElementById("homeCategories"),
|
|
184
|
+
homeSections: document.getElementById("homeSections"),
|
|
185
|
+
overviewArea: document.getElementById("overviewArea"),
|
|
186
|
+
overviewCards: document.getElementById("overviewCards"),
|
|
187
|
+
plugins: document.getElementById("plugins")
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const fetchJson = async (url, init) => {
|
|
191
|
+
const response = await fetch(url, init);
|
|
192
|
+
if (!response.ok) throw new Error(await response.text());
|
|
193
|
+
return response.json();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const buildApiUrl = (path) => {
|
|
197
|
+
if (!state.selectedGuildId) return dashboardConfig.basePath + path;
|
|
198
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
199
|
+
return dashboardConfig.basePath + path + separator + "guildId=" + encodeURIComponent(state.selectedGuildId);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const escapeHtml = (value) => String(value)
|
|
203
|
+
.replaceAll("&", "&")
|
|
204
|
+
.replaceAll("<", "<")
|
|
205
|
+
.replaceAll(">", ">")
|
|
206
|
+
.replaceAll('"', """)
|
|
207
|
+
.replaceAll("'", "'");
|
|
208
|
+
|
|
209
|
+
const normalizeBoxWidth = (value) => {
|
|
210
|
+
const numeric = Number(value);
|
|
211
|
+
if (numeric === 50 || numeric === 33 || numeric === 20) return numeric;
|
|
212
|
+
return 100;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const makeButton = (action, pluginId, panelId, panelElement) => {
|
|
216
|
+
const button = document.createElement("button");
|
|
217
|
+
button.textContent = action.label;
|
|
218
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
219
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
220
|
+
|
|
221
|
+
button.addEventListener("click", async () => {
|
|
222
|
+
button.disabled = true;
|
|
223
|
+
try {
|
|
224
|
+
let payload = {};
|
|
225
|
+
if (action.collectFields && panelElement) {
|
|
226
|
+
const values = {};
|
|
227
|
+
const inputs = panelElement.querySelectorAll("[data-plugin-field-id]");
|
|
228
|
+
inputs.forEach((inputEl) => {
|
|
229
|
+
const fieldId = inputEl.dataset.pluginFieldId;
|
|
230
|
+
const fieldType = inputEl.dataset.pluginFieldType || "text";
|
|
231
|
+
if (!fieldId) return;
|
|
232
|
+
values[fieldId] = toFieldValue({ type: fieldType }, inputEl);
|
|
233
|
+
});
|
|
234
|
+
payload = { panelId, values };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const actionUrl = buildApiUrl("/api/plugins/" + encodeURIComponent(pluginId) + "/" + encodeURIComponent(action.id));
|
|
238
|
+
const result = await fetchJson(actionUrl, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { "Content-Type": "application/json" },
|
|
241
|
+
body: JSON.stringify(payload)
|
|
242
|
+
});
|
|
243
|
+
if (result.message) alert(result.message);
|
|
244
|
+
if (result.refresh) await refreshContent();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
alert(error instanceof Error ? error.message : "Action failed");
|
|
247
|
+
} finally {
|
|
248
|
+
button.disabled = false;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return button;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const renderCards = (cards) => {
|
|
255
|
+
if (!cards.length) {
|
|
256
|
+
el.overviewCards.innerHTML = '<div class="empty">No cards configured yet.</div>';
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
el.overviewCards.innerHTML = cards.map((card) => {
|
|
260
|
+
const subtitle = card.subtitle ? '<div class="subtitle">' + escapeHtml(card.subtitle) + '</div>' : "";
|
|
261
|
+
return '<article class="panel">'
|
|
262
|
+
+ '<div class="title">' + escapeHtml(card.title) + '</div>'
|
|
263
|
+
+ '<div class="value">' + escapeHtml(card.value) + '</div>'
|
|
264
|
+
+ subtitle
|
|
265
|
+
+ '</article>';
|
|
266
|
+
}).join("");
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const shortName = (name) => {
|
|
270
|
+
if (!name) return "?";
|
|
271
|
+
const parts = String(name).trim().split(/\\s+/).filter(Boolean);
|
|
272
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
273
|
+
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const addAvatarOrFallback = (button, item) => {
|
|
277
|
+
if (!item.avatarUrl) {
|
|
278
|
+
const fallback = document.createElement("span");
|
|
279
|
+
fallback.className = "server-fallback";
|
|
280
|
+
fallback.textContent = item.short;
|
|
281
|
+
button.appendChild(fallback);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const avatar = document.createElement("img");
|
|
285
|
+
avatar.className = "server-avatar";
|
|
286
|
+
avatar.src = item.avatarUrl;
|
|
287
|
+
avatar.alt = item.name;
|
|
288
|
+
avatar.addEventListener("error", () => {
|
|
289
|
+
avatar.remove();
|
|
290
|
+
const fallback = document.createElement("span");
|
|
291
|
+
fallback.className = "server-fallback";
|
|
292
|
+
fallback.textContent = item.short;
|
|
293
|
+
button.appendChild(fallback);
|
|
294
|
+
});
|
|
295
|
+
button.appendChild(avatar);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const renderServerRail = () => {
|
|
299
|
+
const items = [{ id: null, name: "User Dashboard", short: "ME", avatarUrl: state.session?.user?.avatarUrl ?? null, botInGuild: true }].concat(
|
|
300
|
+
state.guilds.map((guild) => ({
|
|
301
|
+
id: guild.id,
|
|
302
|
+
name: guild.name,
|
|
303
|
+
short: shortName(guild.name),
|
|
304
|
+
avatarUrl: guild.iconUrl ?? null,
|
|
305
|
+
botInGuild: guild.botInGuild !== false,
|
|
306
|
+
inviteUrl: guild.inviteUrl
|
|
307
|
+
}))
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
el.serverRail.innerHTML = "";
|
|
311
|
+
items.forEach((item) => {
|
|
312
|
+
const button = document.createElement("button");
|
|
313
|
+
button.className = "server-item cursor-pointer" + (item.id === state.selectedGuildId ? " active" : "");
|
|
314
|
+
button.title = item.id && !item.botInGuild ? (item.name + " \u2022 Invite bot") : item.name;
|
|
315
|
+
|
|
316
|
+
const activeIndicator = document.createElement("span");
|
|
317
|
+
activeIndicator.className = "server-item-indicator";
|
|
318
|
+
button.appendChild(activeIndicator);
|
|
319
|
+
|
|
320
|
+
addAvatarOrFallback(button, item);
|
|
321
|
+
|
|
322
|
+
if (item.id) {
|
|
323
|
+
const status = document.createElement("span");
|
|
324
|
+
status.className = "server-status" + (item.botInGuild ? "" : " offline");
|
|
325
|
+
button.appendChild(status);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
button.addEventListener("click", async () => {
|
|
329
|
+
if (item.id && !item.botInGuild && item.inviteUrl) {
|
|
330
|
+
const opened = window.open(item.inviteUrl, "_blank", "noopener,noreferrer");
|
|
331
|
+
if (!opened && typeof alert === "function") alert("Popup blocked. Please allow popups.");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
state.selectedGuildId = item.id;
|
|
335
|
+
renderServerRail();
|
|
336
|
+
updateContextLabel();
|
|
337
|
+
await refreshContent();
|
|
338
|
+
});
|
|
339
|
+
el.serverRail.appendChild(button);
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const applyMainTab = () => {
|
|
344
|
+
const homeActive = state.activeMainTab === "home";
|
|
345
|
+
el.homeArea.style.display = homeActive ? "block" : "none";
|
|
346
|
+
el.pluginsArea.style.display = homeActive ? "none" : "block";
|
|
347
|
+
el.tabHome.className = "main-tab cursor-pointer" + (homeActive ? " active" : "");
|
|
348
|
+
el.tabPlugins.className = "main-tab cursor-pointer" + (!homeActive ? " active" : "");
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const updateContextLabel = () => {
|
|
352
|
+
if (!state.selectedGuildId) {
|
|
353
|
+
el.centerTitle.textContent = "User Dashboard";
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const selectedGuild = state.guilds.find((guild) => guild.id === state.selectedGuildId);
|
|
357
|
+
el.centerTitle.textContent = selectedGuild ? (selectedGuild.name + " Dashboard") : "Server Dashboard";
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const renderHomeCategories = () => {
|
|
361
|
+
if (!state.homeCategories.length) {
|
|
362
|
+
el.homeCategories.innerHTML = "";
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
el.homeCategories.innerHTML = "";
|
|
366
|
+
state.homeCategories.forEach((category) => {
|
|
367
|
+
const button = document.createElement("button");
|
|
368
|
+
button.className = "home-category-btn cursor-pointer" + (state.selectedHomeCategoryId === category.id ? " active" : "");
|
|
369
|
+
button.textContent = category.label;
|
|
370
|
+
button.title = category.description || category.label;
|
|
371
|
+
button.addEventListener("click", async () => {
|
|
372
|
+
state.selectedHomeCategoryId = category.id;
|
|
373
|
+
renderHomeCategories();
|
|
374
|
+
await refreshContent();
|
|
375
|
+
});
|
|
376
|
+
el.homeCategories.appendChild(button);
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const toFieldValue = (field, element) => {
|
|
381
|
+
if (field.type === "string-list") {
|
|
382
|
+
const raw = element.dataset.listValues;
|
|
383
|
+
if (!raw) return [];
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(raw);
|
|
386
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
387
|
+
} catch { return []; }
|
|
388
|
+
}
|
|
389
|
+
if (field.type === "role-search" || field.type === "channel-search" || field.type === "member-search") {
|
|
390
|
+
const raw = element.dataset.selectedObject;
|
|
391
|
+
if (!raw) return null;
|
|
392
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
393
|
+
}
|
|
394
|
+
if (field.type === "boolean") return Boolean(element.checked);
|
|
395
|
+
if (field.type === "number") return element.value === "" ? null : Number(element.value);
|
|
396
|
+
return element.value;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const setupStringListField = (field, input, fieldWrap) => {
|
|
400
|
+
const editor = document.createElement("div");
|
|
401
|
+
editor.className = "list-editor";
|
|
402
|
+
const itemsWrap = document.createElement("div");
|
|
403
|
+
itemsWrap.className = "list-items";
|
|
404
|
+
const addButton = document.createElement("button");
|
|
405
|
+
addButton.type = "button";
|
|
406
|
+
addButton.className = "list-add cursor-pointer";
|
|
407
|
+
addButton.textContent = "Add Button";
|
|
408
|
+
|
|
409
|
+
const normalizeValues = () => {
|
|
410
|
+
const values = Array.from(itemsWrap.querySelectorAll(".list-input")).map((item) => item.value.trim()).filter((item) => item.length > 0);
|
|
411
|
+
input.dataset.listValues = JSON.stringify(values);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const makeRow = (value = "") => {
|
|
415
|
+
const row = document.createElement("div");
|
|
416
|
+
row.className = "list-item";
|
|
417
|
+
row.draggable = true;
|
|
418
|
+
const handle = document.createElement("span");
|
|
419
|
+
handle.className = "drag-handle cursor-pointer";
|
|
420
|
+
handle.textContent = "\u22EE\u22EE";
|
|
421
|
+
const textInput = document.createElement("input");
|
|
422
|
+
textInput.type = "text";
|
|
423
|
+
textInput.className = "list-input";
|
|
424
|
+
textInput.value = value;
|
|
425
|
+
textInput.placeholder = "Button label";
|
|
426
|
+
textInput.addEventListener("input", normalizeValues);
|
|
427
|
+
const removeButton = document.createElement("button");
|
|
428
|
+
removeButton.type = "button";
|
|
429
|
+
removeButton.className = "cursor-pointer";
|
|
430
|
+
removeButton.textContent = "\xD7";
|
|
431
|
+
removeButton.addEventListener("click", () => { row.remove(); normalizeValues(); });
|
|
432
|
+
|
|
433
|
+
row.addEventListener("dragstart", () => row.classList.add("dragging"));
|
|
434
|
+
row.addEventListener("dragend", () => { row.classList.remove("dragging"); normalizeValues(); });
|
|
435
|
+
|
|
436
|
+
row.appendChild(handle); row.appendChild(textInput); row.appendChild(removeButton);
|
|
437
|
+
return row;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
itemsWrap.addEventListener("dragover", (event) => {
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
const dragging = itemsWrap.querySelector(".dragging");
|
|
443
|
+
if (!dragging) return;
|
|
444
|
+
const siblings = Array.from(itemsWrap.querySelectorAll(".list-item:not(.dragging)"));
|
|
445
|
+
let inserted = false;
|
|
446
|
+
for (const sibling of siblings) {
|
|
447
|
+
const rect = sibling.getBoundingClientRect();
|
|
448
|
+
if (event.clientY < rect.top + rect.height / 2) {
|
|
449
|
+
itemsWrap.insertBefore(dragging, sibling);
|
|
450
|
+
inserted = true;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (!inserted) itemsWrap.appendChild(dragging);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const initialValues = Array.isArray(field.value) ? field.value.map((item) => String(item)) : [];
|
|
458
|
+
if (initialValues.length === 0) initialValues.push("Yes", "No");
|
|
459
|
+
initialValues.forEach((value) => itemsWrap.appendChild(makeRow(value)));
|
|
460
|
+
|
|
461
|
+
addButton.addEventListener("click", () => { itemsWrap.appendChild(makeRow("")); normalizeValues(); });
|
|
462
|
+
editor.appendChild(itemsWrap); editor.appendChild(addButton); fieldWrap.appendChild(editor);
|
|
463
|
+
normalizeValues();
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const showLookupResults = (container, items, labelResolver, onSelect) => {
|
|
467
|
+
container.innerHTML = "";
|
|
468
|
+
if (!items.length) { container.style.display = "none"; return; }
|
|
469
|
+
items.forEach((item) => {
|
|
470
|
+
const btn = document.createElement("button");
|
|
471
|
+
btn.type = "button";
|
|
472
|
+
btn.className = "lookup-item cursor-pointer";
|
|
473
|
+
btn.textContent = labelResolver(item);
|
|
474
|
+
btn.addEventListener("click", () => { onSelect(item); container.style.display = "none"; });
|
|
475
|
+
container.appendChild(btn);
|
|
476
|
+
});
|
|
477
|
+
container.style.display = "block";
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const setupLookupField = (field, input, fieldWrap) => {
|
|
481
|
+
const wrap = document.createElement("div");
|
|
482
|
+
wrap.className = "lookup-wrap";
|
|
483
|
+
const results = document.createElement("div");
|
|
484
|
+
results.className = "lookup-results";
|
|
485
|
+
const selected = document.createElement("div");
|
|
486
|
+
selected.className = "lookup-selected";
|
|
487
|
+
selected.textContent = "No selection";
|
|
488
|
+
|
|
489
|
+
wrap.appendChild(input); wrap.appendChild(results); fieldWrap.appendChild(wrap); fieldWrap.appendChild(selected);
|
|
490
|
+
|
|
491
|
+
const minQueryLength = Math.max(0, field.lookup?.minQueryLength ?? 1);
|
|
492
|
+
const limit = Math.max(1, Math.min(field.lookup?.limit ?? 10, 50));
|
|
493
|
+
|
|
494
|
+
const runSearch = async () => {
|
|
495
|
+
const query = String(input.value || "");
|
|
496
|
+
if (query.length < minQueryLength) { results.style.display = "none"; return; }
|
|
497
|
+
try {
|
|
498
|
+
if (field.type === "role-search") {
|
|
499
|
+
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
500
|
+
if (field.lookup?.includeManaged !== undefined) params.set("includeManaged", String(Boolean(field.lookup.includeManaged)));
|
|
501
|
+
const payload = await fetchJson(buildApiUrl("/api/lookup/roles?" + params.toString()));
|
|
502
|
+
showLookupResults(results, payload.roles || [], (item) => "@" + item.name, (item) => {
|
|
503
|
+
input.value = item.name;
|
|
504
|
+
input.dataset.selectedObject = JSON.stringify(item);
|
|
505
|
+
selected.textContent = "Selected role: @" + item.name + " (" + item.id + ")";
|
|
506
|
+
});
|
|
507
|
+
} else if (field.type === "channel-search") {
|
|
508
|
+
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
509
|
+
if (field.lookup?.nsfw !== undefined) params.set("nsfw", String(Boolean(field.lookup.nsfw)));
|
|
510
|
+
if (field.lookup?.channelTypes && field.lookup.channelTypes.length > 0) params.set("channelTypes", field.lookup.channelTypes.join(","));
|
|
511
|
+
const payload = await fetchJson(buildApiUrl("/api/lookup/channels?" + params.toString()));
|
|
512
|
+
showLookupResults(results, payload.channels || [], (item) => "#" + item.name, (item) => {
|
|
513
|
+
input.value = item.name;
|
|
514
|
+
input.dataset.selectedObject = JSON.stringify(item);
|
|
515
|
+
selected.textContent = "Selected channel: #" + item.name + " (" + item.id + ")";
|
|
516
|
+
});
|
|
517
|
+
} else if (field.type === "member-search") {
|
|
518
|
+
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
519
|
+
const payload = await fetchJson(buildApiUrl("/api/lookup/members?" + params.toString()));
|
|
520
|
+
showLookupResults(results, payload.members || [],
|
|
521
|
+
(item) => (item?.user?.username || "unknown") + (item?.nick ? " (" + item.nick + ")" : ""),
|
|
522
|
+
(item) => {
|
|
523
|
+
const username = item?.user?.username || "unknown";
|
|
524
|
+
input.value = username;
|
|
525
|
+
input.dataset.selectedObject = JSON.stringify(item);
|
|
526
|
+
selected.textContent = "Selected member: " + username + " (" + (item?.user?.id || "unknown") + ")";
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
} catch { results.style.display = "none"; }
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
input.addEventListener("input", () => { input.dataset.selectedObject = ""; selected.textContent = "No selection"; runSearch(); });
|
|
533
|
+
input.addEventListener("blur", () => setTimeout(() => { results.style.display = "none"; }, 120));
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const renderHomeSections = (sections) => {
|
|
537
|
+
if (!sections.length) { el.homeSections.innerHTML = '<div class="empty">No home sections configured.</div>'; return; }
|
|
538
|
+
el.homeSections.innerHTML = "";
|
|
539
|
+
sections.forEach((section) => {
|
|
540
|
+
const wrap = document.createElement("article");
|
|
541
|
+
wrap.className = "panel home-section-panel home-width-" + normalizeBoxWidth(section.width);
|
|
542
|
+
|
|
543
|
+
const heading = document.createElement("h3");
|
|
544
|
+
heading.textContent = section.title; heading.style.margin = "0"; wrap.appendChild(heading);
|
|
545
|
+
|
|
546
|
+
if (section.description) {
|
|
547
|
+
const desc = document.createElement("div");
|
|
548
|
+
desc.className = "subtitle"; desc.textContent = section.description; wrap.appendChild(desc);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const message = document.createElement("div");
|
|
552
|
+
message.className = "home-message";
|
|
553
|
+
|
|
554
|
+
if (section.fields?.length) {
|
|
555
|
+
const fieldsWrap = document.createElement("div");
|
|
556
|
+
fieldsWrap.className = "home-fields";
|
|
557
|
+
|
|
558
|
+
section.fields.forEach((field) => {
|
|
559
|
+
const fieldWrap = document.createElement("div");
|
|
560
|
+
fieldWrap.className = "home-field";
|
|
561
|
+
|
|
562
|
+
const label = document.createElement("label");
|
|
563
|
+
label.textContent = field.label; fieldWrap.appendChild(label);
|
|
564
|
+
|
|
565
|
+
let input;
|
|
566
|
+
if (field.type === "textarea") {
|
|
567
|
+
input = document.createElement("textarea"); input.className = "home-textarea"; input.value = field.value == null ? "" : String(field.value);
|
|
568
|
+
} else if (field.type === "select") {
|
|
569
|
+
input = document.createElement("select"); input.className = "home-select cursor-pointer";
|
|
570
|
+
(field.options || []).forEach((option) => {
|
|
571
|
+
const optionEl = document.createElement("option"); optionEl.value = option.value; optionEl.textContent = option.label;
|
|
572
|
+
if (String(field.value ?? "") === option.value) optionEl.selected = true;
|
|
573
|
+
input.appendChild(optionEl);
|
|
574
|
+
});
|
|
575
|
+
} else if (field.type === "boolean") {
|
|
576
|
+
const row = document.createElement("div"); row.className = "home-field-row";
|
|
577
|
+
input = document.createElement("input"); input.type = "checkbox"; input.className = "home-checkbox cursor-pointer"; input.checked = Boolean(field.value);
|
|
578
|
+
const stateText = document.createElement("span"); stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
579
|
+
input.addEventListener("change", () => stateText.textContent = input.checked ? "Enabled" : "Disabled");
|
|
580
|
+
row.appendChild(input); row.appendChild(stateText); fieldWrap.appendChild(row);
|
|
581
|
+
} else {
|
|
582
|
+
input = document.createElement("input"); input.className = "home-input";
|
|
583
|
+
input.type = field.type === "number" ? "number" : "text"; input.value = field.value == null ? "" : String(field.value);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (input) {
|
|
587
|
+
input.dataset.homeFieldId = field.id; input.dataset.homeFieldType = field.type;
|
|
588
|
+
if (field.placeholder && "placeholder" in input) input.placeholder = field.placeholder;
|
|
589
|
+
if (field.required && "required" in input) input.required = true;
|
|
590
|
+
if (field.readOnly) { if ("readOnly" in input) input.readOnly = true; if ("disabled" in input) input.disabled = true; }
|
|
591
|
+
|
|
592
|
+
if (field.type === "role-search" || field.type === "channel-search" || field.type === "member-search") { setupLookupField(field, input, fieldWrap); }
|
|
593
|
+
else if (field.type !== "boolean") { fieldWrap.appendChild(input); }
|
|
594
|
+
}
|
|
595
|
+
fieldsWrap.appendChild(fieldWrap);
|
|
596
|
+
});
|
|
597
|
+
wrap.appendChild(fieldsWrap);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (section.actions?.length) {
|
|
601
|
+
const actions = document.createElement("div"); actions.className = "actions";
|
|
602
|
+
section.actions.forEach((action) => {
|
|
603
|
+
const button = document.createElement("button"); button.textContent = action.label;
|
|
604
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
605
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
606
|
+
|
|
607
|
+
button.addEventListener("click", async () => {
|
|
608
|
+
button.disabled = true;
|
|
609
|
+
try {
|
|
610
|
+
const values = {};
|
|
611
|
+
const inputs = wrap.querySelectorAll("[data-home-field-id]");
|
|
612
|
+
inputs.forEach((inputEl) => {
|
|
613
|
+
const fieldId = inputEl.dataset.homeFieldId;
|
|
614
|
+
const fieldType = inputEl.dataset.homeFieldType || "text";
|
|
615
|
+
if (!fieldId) return;
|
|
616
|
+
values[fieldId] = toFieldValue({ type: fieldType }, inputEl);
|
|
617
|
+
});
|
|
618
|
+
const result = await fetchJson(buildApiUrl("/api/home/" + encodeURIComponent(action.id)), {
|
|
619
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sectionId: section.id, values })
|
|
620
|
+
});
|
|
621
|
+
message.textContent = result.message || "Saved.";
|
|
622
|
+
if (result.refresh) await refreshContent();
|
|
623
|
+
} catch (error) { message.textContent = error instanceof Error ? error.message : "Save failed"; }
|
|
624
|
+
finally { button.disabled = false; }
|
|
625
|
+
});
|
|
626
|
+
actions.appendChild(button);
|
|
627
|
+
});
|
|
628
|
+
wrap.appendChild(actions);
|
|
629
|
+
}
|
|
630
|
+
wrap.appendChild(message); el.homeSections.appendChild(wrap);
|
|
631
|
+
});
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const renderPlugins = (plugins) => {
|
|
635
|
+
if (!plugins.length) { el.plugins.innerHTML = '<div class="empty">No plugins configured yet.</div>'; return; }
|
|
636
|
+
el.plugins.innerHTML = "";
|
|
637
|
+
plugins.forEach((plugin) => {
|
|
638
|
+
const wrap = document.createElement("article"); wrap.className = "panel";
|
|
639
|
+
const heading = document.createElement("div"); heading.className = "title"; heading.textContent = plugin.name; wrap.appendChild(heading);
|
|
640
|
+
if (plugin.description) { const desc = document.createElement("div"); desc.className = "subtitle"; desc.textContent = plugin.description; wrap.appendChild(desc); }
|
|
641
|
+
|
|
642
|
+
(plugin.panels || []).forEach((panel) => {
|
|
643
|
+
const panelBody = document.createElement("div");
|
|
644
|
+
const panelTitle = document.createElement("h4"); panelTitle.textContent = panel.title; panelTitle.style.marginBottom = "4px"; panelBody.appendChild(panelTitle);
|
|
645
|
+
if (panel.description) { const p = document.createElement("div"); p.className = "subtitle"; p.textContent = panel.description; panelBody.appendChild(p); }
|
|
646
|
+
|
|
647
|
+
if (panel.fields?.length) {
|
|
648
|
+
const fieldsWrap = document.createElement("div"); fieldsWrap.className = "plugin-fields";
|
|
649
|
+
panel.fields.forEach((field) => {
|
|
650
|
+
const fieldWrap = document.createElement("div"); fieldWrap.className = field.editable ? "plugin-field" : "kv-item";
|
|
651
|
+
|
|
652
|
+
if (!field.editable) {
|
|
653
|
+
const display = field.value == null ? "" : typeof field.value === "object" ? JSON.stringify(field.value) : String(field.value);
|
|
654
|
+
fieldWrap.innerHTML = '<strong>' + escapeHtml(field.label) + '</strong><span>' + escapeHtml(display) + '</span>';
|
|
655
|
+
fieldsWrap.appendChild(fieldWrap); return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const label = document.createElement("label"); label.textContent = field.label; fieldWrap.appendChild(label);
|
|
659
|
+
|
|
660
|
+
let input;
|
|
661
|
+
if (field.type === "textarea") { input = document.createElement("textarea"); input.className = "home-textarea"; input.value = field.value == null ? "" : String(field.value); }
|
|
662
|
+
else if (field.type === "select") {
|
|
663
|
+
input = document.createElement("select"); input.className = "home-select cursor-pointer";
|
|
664
|
+
(field.options || []).forEach((option) => {
|
|
665
|
+
const optionEl = document.createElement("option"); optionEl.value = option.value; optionEl.textContent = option.label;
|
|
666
|
+
if (String(field.value ?? "") === option.value) optionEl.selected = true;
|
|
667
|
+
input.appendChild(optionEl);
|
|
668
|
+
});
|
|
669
|
+
} else if (field.type === "boolean") {
|
|
670
|
+
const row = document.createElement("div"); row.className = "home-field-row";
|
|
671
|
+
input = document.createElement("input"); input.type = "checkbox"; input.className = "home-checkbox cursor-pointer"; input.checked = Boolean(field.value);
|
|
672
|
+
const stateText = document.createElement("span"); stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
673
|
+
input.addEventListener("change", () => stateText.textContent = input.checked ? "Enabled" : "Disabled");
|
|
674
|
+
row.appendChild(input); row.appendChild(stateText); fieldWrap.appendChild(row);
|
|
675
|
+
} else {
|
|
676
|
+
input = document.createElement("input"); input.className = "home-input";
|
|
677
|
+
input.type = field.type === "number" ? "number" : field.type === "url" ? "url" : "text"; input.value = field.value == null ? "" : String(field.value);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (input) {
|
|
681
|
+
input.dataset.pluginFieldId = field.id || field.label; input.dataset.pluginFieldType = field.type || "text";
|
|
682
|
+
if (field.placeholder && "placeholder" in input) input.placeholder = field.placeholder;
|
|
683
|
+
if (field.required && "required" in input) input.required = true;
|
|
684
|
+
|
|
685
|
+
const isLookup = field.type === "role-search" || field.type === "channel-search" || field.type === "member-search";
|
|
686
|
+
if (isLookup) { setupLookupField(field, input, fieldWrap); }
|
|
687
|
+
else if (field.type === "string-list") { setupStringListField(field, input, fieldWrap); }
|
|
688
|
+
else if (field.type !== "boolean") { fieldWrap.appendChild(input); }
|
|
689
|
+
}
|
|
690
|
+
fieldsWrap.appendChild(fieldWrap);
|
|
691
|
+
});
|
|
692
|
+
panelBody.appendChild(fieldsWrap);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (panel.actions?.length) {
|
|
696
|
+
const actions = document.createElement("div"); actions.className = "actions";
|
|
697
|
+
panel.actions.forEach((action) => { actions.appendChild(makeButton(action, plugin.id, panel.id, panelBody)); });
|
|
698
|
+
panelBody.appendChild(actions);
|
|
699
|
+
}
|
|
700
|
+
wrap.appendChild(panelBody);
|
|
701
|
+
});
|
|
702
|
+
el.plugins.appendChild(wrap);
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const refreshContent = async () => {
|
|
707
|
+
const categoriesPayload = await fetchJson(buildApiUrl("/api/home/categories"));
|
|
708
|
+
state.homeCategories = categoriesPayload.categories || [];
|
|
709
|
+
if (!state.selectedHomeCategoryId || !state.homeCategories.some((item) => item.id === state.selectedHomeCategoryId)) {
|
|
710
|
+
const overviewCategory = state.homeCategories.find((item) => item.id === "overview");
|
|
711
|
+
state.selectedHomeCategoryId = overviewCategory ? overviewCategory.id : (state.homeCategories[0]?.id ?? null);
|
|
712
|
+
}
|
|
713
|
+
renderHomeCategories();
|
|
714
|
+
|
|
715
|
+
const homePath = state.selectedHomeCategoryId ? "/api/home?categoryId=" + encodeURIComponent(state.selectedHomeCategoryId) : "/api/home";
|
|
716
|
+
const [home, overview, plugins] = await Promise.all([ fetchJson(buildApiUrl(homePath)), fetchJson(buildApiUrl("/api/overview")), fetchJson(buildApiUrl("/api/plugins")) ]);
|
|
717
|
+
|
|
718
|
+
renderHomeSections(home.sections || []);
|
|
719
|
+
const showOverviewArea = state.selectedHomeCategoryId === "overview";
|
|
720
|
+
el.overviewArea.style.display = showOverviewArea ? "block" : "none";
|
|
721
|
+
renderCards(overview.cards || []);
|
|
722
|
+
renderPlugins(plugins.plugins || []);
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const loadInitialData = async () => {
|
|
726
|
+
const session = await fetchJson(dashboardConfig.basePath + "/api/session");
|
|
727
|
+
if (!session.authenticated) { window.location.href = dashboardConfig.basePath + "/login"; return; }
|
|
728
|
+
|
|
729
|
+
state.session = session;
|
|
730
|
+
el.userMeta.textContent = session.user.username + " \u2022 " + session.guildCount + " guild(s)";
|
|
731
|
+
const guilds = await fetchJson(dashboardConfig.basePath + "/api/guilds");
|
|
732
|
+
state.guilds = guilds.guilds || [];
|
|
733
|
+
state.selectedGuildId = null;
|
|
734
|
+
renderServerRail(); updateContextLabel();
|
|
735
|
+
|
|
736
|
+
el.tabHome.addEventListener("click", () => { state.activeMainTab = "home"; applyMainTab(); });
|
|
737
|
+
el.tabPlugins.addEventListener("click", () => { state.activeMainTab = "plugins"; applyMainTab(); });
|
|
738
|
+
|
|
739
|
+
applyMainTab(); await refreshContent();
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
loadInitialData().catch((error) => { el.userMeta.textContent = "Load failed"; console.error(error); });
|
|
743
|
+
`;
|
|
459
744
|
}
|
|
460
|
-
|
|
461
|
-
.
|
|
462
|
-
`;
|
|
745
|
+
|
|
746
|
+
// src/templates/layouts/compact.ts
|
|
463
747
|
function escapeHtml(value) {
|
|
464
748
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
465
749
|
}
|
|
466
|
-
function
|
|
467
|
-
const safeName = escapeHtml(
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
<style>${setupDesign.customCss}</style>` : "";
|
|
750
|
+
function renderCompactLayout(context) {
|
|
751
|
+
const safeName = escapeHtml(context.dashboardName);
|
|
752
|
+
const design = context.setupDesign ?? {};
|
|
753
|
+
const clientScript = getClientScript(context.basePath, design);
|
|
471
754
|
return `<!DOCTYPE html>
|
|
472
755
|
<html lang="en">
|
|
473
756
|
<head>
|
|
474
757
|
<meta charset="UTF-8" />
|
|
475
758
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
476
759
|
<title>${safeName}</title>
|
|
477
|
-
<style
|
|
760
|
+
<style>
|
|
761
|
+
:root {
|
|
762
|
+
color-scheme: dark;
|
|
763
|
+
--bg: ${design.bg ?? "#0f1221"};
|
|
764
|
+
--rail: ${design.rail ?? "#171a2d"};
|
|
765
|
+
--content-bg: ${design.contentBg ?? "#0f1426"};
|
|
766
|
+
--panel: ${design.panel ?? "#1f243b"};
|
|
767
|
+
--panel-2: ${design.panel2 ?? "#2a314e"};
|
|
768
|
+
--text: ${design.text ?? "#f5f7ff"};
|
|
769
|
+
--muted: ${design.muted ?? "#aab1d6"};
|
|
770
|
+
--primary: ${design.primary ?? "#7c87ff"};
|
|
771
|
+
--success: ${design.success ?? "#2bd4a6"};
|
|
772
|
+
--warning: ${design.warning ?? "#ffd166"};
|
|
773
|
+
--danger: ${design.danger ?? "#ff6f91"};
|
|
774
|
+
--info: ${design.info ?? "#66d9ff"};
|
|
775
|
+
--border: ${design.border ?? "rgba(255, 255, 255, 0.12)"};
|
|
776
|
+
}
|
|
777
|
+
* { box-sizing: border-box; }
|
|
778
|
+
body {
|
|
779
|
+
margin: 0;
|
|
780
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
|
781
|
+
background: radial-gradient(circle at 0% 0%, #1b2140 0%, var(--bg) 45%);
|
|
782
|
+
color: var(--text);
|
|
783
|
+
}
|
|
784
|
+
.shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; }
|
|
785
|
+
.topbar { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--border); background: rgba(15, 20, 38, 0.7); backdrop-filter: blur(8px); }
|
|
786
|
+
.brand { font-weight: 800; letter-spacing: .2px; }
|
|
787
|
+
.center-title { text-align: center; font-weight: 700; color: #d8defc; }
|
|
788
|
+
.pill { justify-self: end; padding: 4px 8px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); font-size: .75rem; }
|
|
789
|
+
.layout { display: grid; grid-template-columns: 80px 1fr; min-height: 0; }
|
|
790
|
+
.sidebar { border-right: 1px solid var(--border); background: linear-gradient(180deg, var(--rail), #121528); padding: 10px 0; }
|
|
791
|
+
.server-rail { display: flex; flex-direction: column; align-items: center; gap: 10px; }
|
|
792
|
+
.server-item { position: relative; width: 46px; height: 46px; border: none; border-radius: 14px; overflow: visible; background: var(--panel); color: #fff; font-weight: 700; transition: transform .15s ease, background .15s ease; }
|
|
793
|
+
.server-item:hover { transform: translateY(-1px); background: #323b5f; }
|
|
794
|
+
.server-item.active { background: var(--primary); transform: translateY(-2px); }
|
|
795
|
+
.server-item-indicator { position: absolute; left: -8px; top: 50%; transform: translateY(-50%) scaleY(.5); width: 3px; height: 18px; background: #fff; border-radius: 999px; opacity: 0; transition: opacity .15s ease, transform .15s ease; }
|
|
796
|
+
.server-item.active .server-item-indicator { opacity: 1; transform: translateY(-50%) scaleY(1); }
|
|
797
|
+
.server-avatar { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
|
|
798
|
+
.server-fallback { display: grid; place-items: center; width: 100%; height: 100%; }
|
|
799
|
+
.server-status { position: absolute; right: -3px; bottom: -3px; width: 11px; height: 11px; border-radius: 999px; border: 2px solid var(--rail); background: #35d489; }
|
|
800
|
+
.server-status.offline { background: #7f8bb3; }
|
|
801
|
+
.content { min-width: 0; padding: 12px; }
|
|
802
|
+
.container { background: rgba(23, 28, 48, 0.6); border: 1px solid var(--border); border-radius: 12px; padding: 12px; }
|
|
803
|
+
.main-tabs { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
804
|
+
button { border: 1px solid var(--border); background: var(--panel-2); color: var(--text); border-radius: 8px; padding: 7px 10px; }
|
|
805
|
+
button.primary { background: var(--primary); border: none; }
|
|
806
|
+
button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
|
|
807
|
+
.main-tab.active, .home-category-btn.active { background: var(--primary); border-color: transparent; }
|
|
808
|
+
.section-title { margin: 12px 0 8px; color: #dce3ff; font-size: .95rem; }
|
|
809
|
+
.grid { display: grid; gap: 10px; }
|
|
810
|
+
.cards { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
|
|
811
|
+
.panel { background: linear-gradient(180deg, rgba(42,49,78,.7), rgba(31,36,59,.85)); border: 1px solid var(--border); border-radius: 10px; padding: 12px; }
|
|
812
|
+
.title { color: var(--muted); font-size: .83rem; }
|
|
813
|
+
.value { font-size: 1.25rem; font-weight: 800; margin-top: 5px; }
|
|
814
|
+
.subtitle { margin-top: 5px; color: var(--muted); font-size: .8rem; }
|
|
815
|
+
.home-categories { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
816
|
+
.home-sections { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
817
|
+
.home-section-panel { flex: 0 0 100%; max-width: 100%; }
|
|
818
|
+
.home-width-50 { flex-basis: calc(50% - 5px); max-width: calc(50% - 5px); }
|
|
819
|
+
.home-width-33 { flex-basis: calc(33.333333% - 6.67px); max-width: calc(33.333333% - 6.67px); }
|
|
820
|
+
.home-width-20 { flex-basis: calc(20% - 8px); max-width: calc(20% - 8px); }
|
|
821
|
+
.home-fields, .plugin-fields { display: grid; gap: 8px; margin-top: 8px; }
|
|
822
|
+
.home-field, .plugin-field { display: grid; gap: 5px; }
|
|
823
|
+
.home-field label, .plugin-field > label { color: var(--muted); font-size: .8rem; }
|
|
824
|
+
.home-input, .home-textarea, .home-select { width: 100%; border: 1px solid var(--border); background: var(--panel-2); color: var(--text); border-radius: 8px; padding: 7px 9px; }
|
|
825
|
+
.home-textarea { min-height: 88px; resize: vertical; }
|
|
826
|
+
.home-checkbox { width: 17px; height: 17px; }
|
|
827
|
+
.home-field-row { display: flex; align-items: center; gap: 8px; }
|
|
828
|
+
.home-message { margin-top: 6px; color: var(--muted); font-size: .8rem; }
|
|
829
|
+
.lookup-wrap { position: relative; }
|
|
830
|
+
.lookup-results { position: absolute; left: 0; right: 0; top: calc(100% + 5px); z-index: 20; border: 1px solid var(--border); background: #1f2742; border-radius: 8px; max-height: 220px; overflow: auto; display: none; }
|
|
831
|
+
.lookup-item { width: 100%; border: none; border-radius: 0; text-align: left; padding: 8px 10px; background: transparent; }
|
|
832
|
+
.lookup-item:hover { background: #2d3658; }
|
|
833
|
+
.lookup-selected { margin-top: 5px; font-size: .8rem; color: var(--muted); }
|
|
834
|
+
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
|
835
|
+
.kv-item { display: flex; justify-content: space-between; border: 1px solid var(--border); border-radius: 8px; padding: 7px 9px; background: var(--panel-2); }
|
|
836
|
+
.list-editor { border: 1px solid var(--border); border-radius: 8px; background: var(--panel-2); padding: 8px; display: grid; gap: 8px; }
|
|
837
|
+
.list-items { display: grid; gap: 6px; }
|
|
838
|
+
.list-item { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: center; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; background: var(--panel); }
|
|
839
|
+
.list-item.dragging { opacity: .6; }
|
|
840
|
+
.drag-handle { color: var(--muted); user-select: none; font-size: .9rem; }
|
|
841
|
+
.list-input { width: 100%; border: none; outline: none; background: transparent; color: var(--text); }
|
|
842
|
+
.list-add { justify-self: start; }
|
|
843
|
+
.empty { color: var(--muted); font-size: .9rem; }
|
|
844
|
+
.cursor-pointer { cursor: pointer; }
|
|
845
|
+
@media (max-width: 980px) {
|
|
846
|
+
.layout { grid-template-columns: 70px 1fr; }
|
|
847
|
+
.home-width-50, .home-width-33, .home-width-20 { flex-basis: 100%; max-width: 100%; }
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/* Inject User Custom CSS Here */
|
|
851
|
+
${design.customCss ?? ""}
|
|
852
|
+
</style>
|
|
478
853
|
</head>
|
|
479
854
|
<body>
|
|
480
|
-
<div class="
|
|
481
|
-
<
|
|
482
|
-
<div
|
|
483
|
-
|
|
855
|
+
<div class="shell">
|
|
856
|
+
<header class="topbar">
|
|
857
|
+
<div class="brand">${safeName}</div>
|
|
858
|
+
<div id="centerTitle" class="center-title">User Dashboard</div>
|
|
859
|
+
<div id="userMeta" class="pill">Loading...</div>
|
|
860
|
+
</header>
|
|
484
861
|
|
|
485
|
-
<
|
|
486
|
-
<
|
|
487
|
-
<div class="
|
|
488
|
-
|
|
489
|
-
<div id="userMeta" class="pill topbar-right">Loading...</div>
|
|
490
|
-
</header>
|
|
862
|
+
<div class="layout">
|
|
863
|
+
<aside class="sidebar">
|
|
864
|
+
<div id="serverRail" class="server-rail"></div>
|
|
865
|
+
</aside>
|
|
491
866
|
|
|
492
|
-
<
|
|
493
|
-
<div class="
|
|
494
|
-
<
|
|
495
|
-
|
|
496
|
-
|
|
867
|
+
<main class="content">
|
|
868
|
+
<div class="container">
|
|
869
|
+
<div class="main-tabs">
|
|
870
|
+
<button id="tabHome" class="main-tab active cursor-pointer">Home</button>
|
|
871
|
+
<button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
|
|
872
|
+
</div>
|
|
497
873
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
874
|
+
<section id="homeArea">
|
|
875
|
+
<div class="section-title">Home</div>
|
|
876
|
+
<section id="homeCategories" class="home-categories"></section>
|
|
877
|
+
<section id="homeSections" class="home-sections"></section>
|
|
502
878
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
879
|
+
<section id="overviewArea">
|
|
880
|
+
<div class="section-title">Dashboard Stats</div>
|
|
881
|
+
<section id="overviewCards" class="grid cards"></section>
|
|
882
|
+
</section>
|
|
506
883
|
</section>
|
|
507
|
-
</section>
|
|
508
884
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
885
|
+
<section id="pluginsArea" style="display:none;">
|
|
886
|
+
<div class="section-title">Plugins</div>
|
|
887
|
+
<section id="plugins" class="grid"></section>
|
|
888
|
+
</section>
|
|
889
|
+
</div>
|
|
890
|
+
</main>
|
|
891
|
+
</div>
|
|
515
892
|
</div>
|
|
516
893
|
|
|
517
|
-
<script>
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
guilds: [],
|
|
522
|
-
selectedGuildId: null,
|
|
523
|
-
homeCategories: [],
|
|
524
|
-
selectedHomeCategoryId: null,
|
|
525
|
-
activeMainTab: "home"
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
const el = {
|
|
529
|
-
serverRail: document.getElementById("serverRail"),
|
|
530
|
-
userMeta: document.getElementById("userMeta"),
|
|
531
|
-
centerTitle: document.getElementById("centerTitle"),
|
|
532
|
-
tabHome: document.getElementById("tabHome"),
|
|
533
|
-
tabPlugins: document.getElementById("tabPlugins"),
|
|
534
|
-
homeArea: document.getElementById("homeArea"),
|
|
535
|
-
pluginsArea: document.getElementById("pluginsArea"),
|
|
536
|
-
homeCategories: document.getElementById("homeCategories"),
|
|
537
|
-
homeSections: document.getElementById("homeSections"),
|
|
538
|
-
overviewArea: document.getElementById("overviewArea"),
|
|
539
|
-
overviewCards: document.getElementById("overviewCards"),
|
|
540
|
-
plugins: document.getElementById("plugins")
|
|
541
|
-
};
|
|
894
|
+
<script>${clientScript}</script>
|
|
895
|
+
</body>
|
|
896
|
+
</html>`;
|
|
897
|
+
}
|
|
542
898
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
899
|
+
// src/templates/layouts/default.ts
|
|
900
|
+
function escapeHtml2(value) {
|
|
901
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
902
|
+
}
|
|
903
|
+
function renderDefaultLayout(context) {
|
|
904
|
+
const safeName = escapeHtml2(context.dashboardName);
|
|
905
|
+
const design = context.setupDesign ?? {};
|
|
906
|
+
const scriptData = JSON.stringify({ basePath: context.basePath });
|
|
907
|
+
return `<!DOCTYPE html>
|
|
908
|
+
<html lang="en">
|
|
909
|
+
<head>
|
|
910
|
+
<meta charset="UTF-8" />
|
|
911
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
912
|
+
<title>${safeName}</title>
|
|
913
|
+
<style>
|
|
914
|
+
:root {
|
|
915
|
+
color-scheme: dark;
|
|
916
|
+
--bg: ${design.bg ?? "#08090f"};
|
|
917
|
+
--content-bg: ${design.contentBg ?? "radial-gradient(circle at top right, #171a2f, #08090f)"};
|
|
918
|
+
--panel: ${design.panel ?? "rgba(20, 23, 43, 0.5)"};
|
|
919
|
+
--panel-2: ${design.panel2 ?? "rgba(0, 0, 0, 0.4)"};
|
|
920
|
+
--text: ${design.text ?? "#e0e6ff"};
|
|
921
|
+
--muted: ${design.muted ?? "#8a93bc"};
|
|
922
|
+
--primary: ${design.primary ?? "#5865F2"};
|
|
923
|
+
--primary-glow: rgba(88, 101, 242, 0.4);
|
|
924
|
+
--success: ${design.success ?? "#00E676"};
|
|
925
|
+
--danger: ${design.danger ?? "#FF3D00"};
|
|
926
|
+
--border: ${design.border ?? "rgba(255, 255, 255, 0.05)"};
|
|
547
927
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
928
|
+
|
|
929
|
+
* { box-sizing: border-box; }
|
|
930
|
+
|
|
931
|
+
body {
|
|
932
|
+
margin: 0;
|
|
933
|
+
padding: 24px; /* Creates the outer gap for the floating effect */
|
|
934
|
+
height: 100vh;
|
|
935
|
+
font-family: "Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
936
|
+
background: var(--content-bg); /* Applies gradient to entire screen */
|
|
937
|
+
color: var(--text);
|
|
938
|
+
overflow: hidden;
|
|
554
939
|
}
|
|
555
940
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
.replaceAll("<", "<")
|
|
563
|
-
.replaceAll(">", ">")
|
|
564
|
-
.replaceAll('"', """)
|
|
565
|
-
.replaceAll("'", "'");
|
|
566
|
-
|
|
567
|
-
const normalizeBoxWidth = (value) => {
|
|
568
|
-
const numeric = Number(value);
|
|
569
|
-
if (numeric === 50 || numeric === 33 || numeric === 20) {
|
|
570
|
-
return numeric;
|
|
941
|
+
/* FLOATING ISLAND STRUCTURE */
|
|
942
|
+
.layout {
|
|
943
|
+
display: flex;
|
|
944
|
+
gap: 24px; /* Gap between the floating islands */
|
|
945
|
+
height: 100%;
|
|
946
|
+
width: 100%;
|
|
571
947
|
}
|
|
572
|
-
|
|
573
|
-
return 100;
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
const applySetupDesign = () => {
|
|
577
|
-
const root = document.documentElement;
|
|
578
|
-
const design = dashboardConfig.setupDesign || {};
|
|
579
|
-
const mappings = {
|
|
580
|
-
bg: "--bg",
|
|
581
|
-
rail: "--rail",
|
|
582
|
-
contentBg: "--content-bg",
|
|
583
|
-
panel: "--panel",
|
|
584
|
-
panel2: "--panel-2",
|
|
585
|
-
text: "--text",
|
|
586
|
-
muted: "--muted",
|
|
587
|
-
primary: "--primary",
|
|
588
|
-
success: "--success",
|
|
589
|
-
warning: "--warning",
|
|
590
|
-
danger: "--danger",
|
|
591
|
-
info: "--info",
|
|
592
|
-
border: "--border"
|
|
593
|
-
};
|
|
594
|
-
|
|
595
|
-
Object.entries(mappings).forEach(([key, cssVar]) => {
|
|
596
|
-
const value = design[key];
|
|
597
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
598
|
-
root.style.setProperty(cssVar, value.trim());
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
const makeButton = (action, pluginId, panelId, panelElement) => {
|
|
604
|
-
const button = document.createElement("button");
|
|
605
|
-
button.textContent = action.label;
|
|
606
|
-
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
607
|
-
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
608
948
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
inputs.forEach((inputEl) => {
|
|
617
|
-
const fieldId = inputEl.dataset.pluginFieldId;
|
|
618
|
-
const fieldType = inputEl.dataset.pluginFieldType || "text";
|
|
619
|
-
if (!fieldId) {
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
values[fieldId] = toFieldValue({ type: fieldType }, inputEl);
|
|
624
|
-
});
|
|
625
|
-
payload = {
|
|
626
|
-
panelId,
|
|
627
|
-
values
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const actionUrl = buildApiUrl("/api/plugins/" + encodeURIComponent(pluginId) + "/" + encodeURIComponent(action.id));
|
|
632
|
-
const result = await fetchJson(actionUrl, {
|
|
633
|
-
method: "POST",
|
|
634
|
-
headers: { "Content-Type": "application/json" },
|
|
635
|
-
body: JSON.stringify(payload)
|
|
636
|
-
});
|
|
637
|
-
if (result.message) {
|
|
638
|
-
alert(result.message);
|
|
639
|
-
}
|
|
640
|
-
if (result.refresh) {
|
|
641
|
-
await refreshContent();
|
|
642
|
-
}
|
|
643
|
-
} catch (error) {
|
|
644
|
-
alert(error instanceof Error ? error.message : "Action failed");
|
|
645
|
-
} finally {
|
|
646
|
-
button.disabled = false;
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
return button;
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
const renderCards = (cards) => {
|
|
653
|
-
if (!cards.length) {
|
|
654
|
-
el.overviewCards.innerHTML = '<div class="empty">No cards configured yet.</div>';
|
|
655
|
-
return;
|
|
949
|
+
/* Base styles for all floating windows */
|
|
950
|
+
.floating-window {
|
|
951
|
+
background: var(--panel);
|
|
952
|
+
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
|
953
|
+
border: 1px solid var(--border);
|
|
954
|
+
border-radius: 24px;
|
|
955
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
|
656
956
|
}
|
|
657
957
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const shortName = (name) => {
|
|
669
|
-
if (!name) return "?";
|
|
670
|
-
const parts = String(name).trim().split(/s+/).filter(Boolean);
|
|
671
|
-
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
672
|
-
return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
const addAvatarOrFallback = (button, item) => {
|
|
676
|
-
if (!item.avatarUrl) {
|
|
677
|
-
const fallback = document.createElement("span");
|
|
678
|
-
fallback.className = "server-fallback";
|
|
679
|
-
fallback.textContent = item.short;
|
|
680
|
-
button.appendChild(fallback);
|
|
681
|
-
return;
|
|
958
|
+
/* 1. SERVER RAIL */
|
|
959
|
+
.sidebar {
|
|
960
|
+
width: 84px;
|
|
961
|
+
flex-shrink: 0;
|
|
962
|
+
padding: 20px 0;
|
|
963
|
+
z-index: 10;
|
|
964
|
+
overflow-y: auto;
|
|
965
|
+
scrollbar-width: none;
|
|
966
|
+
/* Changed from flex to block to stop height crushing */
|
|
967
|
+
display: block;
|
|
682
968
|
}
|
|
969
|
+
.sidebar::-webkit-scrollbar { display: none; }
|
|
970
|
+
|
|
971
|
+
.server-rail {
|
|
972
|
+
display: flex;
|
|
973
|
+
flex-direction: column;
|
|
974
|
+
align-items: center;
|
|
975
|
+
gap: 16px;
|
|
976
|
+
width: 100%;
|
|
977
|
+
/* This tells the rail it is allowed to be taller than the screen */
|
|
978
|
+
min-height: min-content;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.server-separator {
|
|
982
|
+
width: 32px;
|
|
983
|
+
height: 2px;
|
|
984
|
+
min-height: 2px; /* Lock separator height */
|
|
985
|
+
background: var(--border);
|
|
986
|
+
border-radius: 2px;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.server-item {
|
|
990
|
+
position: relative;
|
|
991
|
+
/* The !important tags force the browser to respect the size */
|
|
992
|
+
width: 54px !important;
|
|
993
|
+
height: 54px !important;
|
|
994
|
+
min-width: 54px !important;
|
|
995
|
+
min-height: 54px !important;
|
|
996
|
+
flex: 0 0 54px !important;
|
|
997
|
+
|
|
998
|
+
border-radius: 16px;
|
|
999
|
+
border: 1px solid transparent;
|
|
1000
|
+
background: var(--panel-2);
|
|
1001
|
+
color: var(--text);
|
|
1002
|
+
font-weight: 700;
|
|
1003
|
+
font-size: 16px;
|
|
1004
|
+
display: flex;
|
|
1005
|
+
align-items: center;
|
|
1006
|
+
justify-content: center;
|
|
1007
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1008
|
+
padding: 0;
|
|
1009
|
+
overflow: visible;
|
|
1010
|
+
}
|
|
1011
|
+
.server-item:hover { border-color: var(--primary); box-shadow: 0 0 15px var(--primary-glow); transform: translateY(-3px); }
|
|
1012
|
+
.server-item.active { background: var(--primary); border-color: var(--primary); box-shadow: 0 0 20px var(--primary-glow); color: #fff; }
|
|
1013
|
+
|
|
1014
|
+
.server-item-indicator { position: absolute; left: -16px; width: 6px; height: 20px; border-radius: 4px; background: var(--primary); opacity: 0; transition: all 0.3s ease; box-shadow: 0 0 10px var(--primary-glow); }
|
|
1015
|
+
.server-item.active .server-item-indicator { opacity: 1; height: 32px; }
|
|
1016
|
+
|
|
1017
|
+
.server-avatar { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
|
|
1018
|
+
.server-fallback { font-weight: 700; font-size: 1rem; }
|
|
1019
|
+
.server-status { position: absolute; right: -4px; bottom: -4px; width: 16px; height: 16px; border-radius: 50%; border: 3px solid #141622; background: var(--success); z-index: 2; box-shadow: 0 0 10px var(--success); }
|
|
1020
|
+
.server-status.offline { background: var(--muted); box-shadow: none; border-color: transparent; }
|
|
1021
|
+
|
|
1022
|
+
/* 2. SECONDARY SIDEBAR */
|
|
1023
|
+
.secondary-sidebar {
|
|
1024
|
+
width: 280px; flex-shrink: 0;
|
|
1025
|
+
display: flex; flex-direction: column; z-index: 9;
|
|
1026
|
+
overflow: hidden; /* Keeps user area inside border radius */
|
|
1027
|
+
}
|
|
1028
|
+
.sidebar-header { height: 72px; padding: 0 24px; display: flex; align-items: center; font-weight: 800; font-size: 18px; letter-spacing: 1px; text-transform: uppercase; background: linear-gradient(90deg, #fff, var(--primary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
1029
|
+
.sidebar-content { flex: 1; overflow-y: auto; padding: 20px 16px; scrollbar-width: none; }
|
|
1030
|
+
.sidebar-content::-webkit-scrollbar { display: none; }
|
|
1031
|
+
|
|
1032
|
+
.category-header { font-size: 11px; font-weight: 800; color: var(--primary); text-transform: uppercase; margin: 24px 8px 8px; letter-spacing: 1.5px; opacity: 0.8; }
|
|
1033
|
+
.category-header:first-child { margin-top: 0; }
|
|
1034
|
+
|
|
1035
|
+
.channel-btn {
|
|
1036
|
+
width: 100%; text-align: left; padding: 12px 16px; border-radius: 12px; background: transparent; border: 1px solid transparent;
|
|
1037
|
+
color: var(--muted); display: flex; align-items: center; gap: 12px; font-size: 14px; font-weight: 600; margin-bottom: 6px; transition: all 0.2s;
|
|
1038
|
+
}
|
|
1039
|
+
.channel-btn:hover { background: var(--panel-2); color: var(--text); transform: translateX(4px); }
|
|
1040
|
+
.channel-btn.active { background: rgba(88, 101, 242, 0.15); border-left: 4px solid var(--primary); color: #fff; transform: translateX(4px); }
|
|
1041
|
+
|
|
1042
|
+
.user-area { height: 80px; background: rgba(0,0,0,0.2); padding: 0 20px; display: flex; align-items: center; flex-shrink: 0; gap: 14px; border-top: 1px solid var(--border); }
|
|
1043
|
+
.user-avatar-small {
|
|
1044
|
+
width: 44px;
|
|
1045
|
+
height: 44px;
|
|
1046
|
+
min-width: 44px;
|
|
1047
|
+
min-height: 44px;
|
|
1048
|
+
flex: 0 0 44px;
|
|
1049
|
+
|
|
1050
|
+
border-radius: 14px;
|
|
1051
|
+
background: var(--primary);
|
|
1052
|
+
border: 2px solid var(--border);
|
|
1053
|
+
padding: 2px;
|
|
1054
|
+
}
|
|
1055
|
+
.user-details { display: flex; flex-direction: column; line-height: 1.4; }
|
|
1056
|
+
.user-details .name { color: #fff; font-size: 15px; font-weight: 700; }
|
|
1057
|
+
.user-details .sub { color: var(--primary); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
683
1058
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1059
|
+
/* 3. MAIN CONTENT */
|
|
1060
|
+
.content { flex: 1; display: flex; flex-direction: column; min-width: 0; gap: 24px; }
|
|
1061
|
+
|
|
1062
|
+
/* Topbar is its own floating island now */
|
|
1063
|
+
.topbar { height: 72px; padding: 0 28px; display: flex; align-items: center; font-weight: 700; font-size: 20px; letter-spacing: 0.5px; z-index: 5; flex-shrink: 0; }
|
|
1064
|
+
|
|
1065
|
+
/* Container background is transparent so inner panels float over the main gradient */
|
|
1066
|
+
.container { flex: 1; padding: 0 8px 0 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--primary) transparent; }
|
|
1067
|
+
.container::-webkit-scrollbar { width: 6px; }
|
|
1068
|
+
.container::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 3px; }
|
|
1069
|
+
|
|
1070
|
+
.section-title { font-size: 26px; font-weight: 800; margin: 0 0 24px 0; color: #fff; letter-spacing: -0.5px; }
|
|
1071
|
+
.subtitle { margin-top: 6px; color: var(--muted); font-size: 14px; line-height: 1.5; }
|
|
1072
|
+
|
|
1073
|
+
.grid { display: grid; gap: 24px; }
|
|
1074
|
+
.cards { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
|
|
1075
|
+
|
|
1076
|
+
/* The content panels are also floating windows */
|
|
1077
|
+
.panel {
|
|
1078
|
+
background: var(--panel); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
|
|
1079
|
+
border: 1px solid var(--border); border-radius: 20px; padding: 28px;
|
|
1080
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); transition: transform 0.3s, box-shadow 0.3s;
|
|
1081
|
+
}
|
|
1082
|
+
.panel:hover { transform: translateY(-4px); box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 20px rgba(88, 101, 242, 0.1); border-color: rgba(88, 101, 242, 0.3); }
|
|
1083
|
+
|
|
1084
|
+
/* Inputs & Forms */
|
|
1085
|
+
.home-sections { display: flex; flex-wrap: wrap; gap: 24px; margin-bottom: 40px; }
|
|
1086
|
+
.home-section-panel { flex: 0 0 100%; max-width: 100%; }
|
|
1087
|
+
.home-width-50 { flex-basis: calc(50% - 12px); max-width: calc(50% - 12px); }
|
|
1088
|
+
@media (max-width: 1300px) { .home-width-50 { flex-basis: 100%; max-width: 100%; } }
|
|
1089
|
+
|
|
1090
|
+
.home-fields { display: grid; gap: 20px; margin-top: 24px; }
|
|
1091
|
+
.home-field { display: grid; gap: 10px; }
|
|
1092
|
+
.home-field label { color: var(--muted); font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; }
|
|
1093
|
+
|
|
1094
|
+
.home-input, .home-textarea, .home-select {
|
|
1095
|
+
width: 100%; border: 1px solid var(--border); background: var(--panel-2); color: #fff;
|
|
1096
|
+
border-radius: 12px; padding: 14px 18px; font-size: 15px; outline: none; transition: all 0.3s ease;
|
|
1097
|
+
}
|
|
1098
|
+
.home-input:focus, .home-textarea:focus, .home-select:focus {
|
|
1099
|
+
border-color: var(--primary); box-shadow: 0 0 15px var(--primary-glow); background: rgba(0,0,0,0.6);
|
|
1100
|
+
}
|
|
1101
|
+
.home-textarea { min-height: 120px; resize: vertical; }
|
|
1102
|
+
|
|
1103
|
+
.actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 28px; }
|
|
1104
|
+
button {
|
|
1105
|
+
border: 1px solid var(--border); background: rgba(255,255,255,0.05); color: #fff;
|
|
1106
|
+
border-radius: 10px; padding: 12px 24px; font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;
|
|
1107
|
+
transition: all 0.2s; backdrop-filter: blur(4px);
|
|
1108
|
+
}
|
|
1109
|
+
button:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }
|
|
1110
|
+
button.primary { background: linear-gradient(135deg, var(--primary), #7c88f9); border: none; box-shadow: 0 4px 15px var(--primary-glow); }
|
|
1111
|
+
button.primary:hover { box-shadow: 0 6px 20px var(--primary-glow); filter: brightness(1.1); }
|
|
1112
|
+
button.danger { background: linear-gradient(135deg, var(--danger), #ff7a59); border: none; box-shadow: 0 4px 15px rgba(255, 61, 0, 0.3); }
|
|
1113
|
+
.cursor-pointer { cursor: pointer; }
|
|
1114
|
+
|
|
1115
|
+
.home-field-row { display: flex; align-items: center; gap: 12px; }
|
|
1116
|
+
.home-checkbox { width: 24px; height: 24px; accent-color: var(--primary); border-radius: 6px; }
|
|
1117
|
+
|
|
1118
|
+
.lookup-wrap { position: relative; }
|
|
1119
|
+
.lookup-results { position: absolute; left: 0; right: 0; top: calc(100% + 8px); z-index: 20; background: #0e101a; border: 1px solid var(--primary); border-radius: 12px; max-height: 220px; overflow: auto; display: none; box-shadow: 0 10px 30px rgba(0,0,0,0.5), 0 0 20px var(--primary-glow); }
|
|
1120
|
+
.lookup-item { width: 100%; text-align: left; padding: 14px; background: transparent; border: none; border-bottom: 1px solid var(--border); color: var(--text); }
|
|
1121
|
+
.lookup-item:hover { background: var(--primary); color: #fff; }
|
|
1122
|
+
|
|
1123
|
+
.value { font-size: 36px; font-weight: 800; background: linear-gradient(to right, #fff, #aab4ff); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-top: 10px; }
|
|
1124
|
+
.title { color: var(--primary); font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: 1.5px; }
|
|
1125
|
+
|
|
1126
|
+
.list-editor { background: var(--panel-2); border: 1px solid var(--border); border-radius: 12px; padding: 16px; display: grid; gap: 12px; }
|
|
1127
|
+
.list-item { display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: center; background: rgba(0,0,0,0.5); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; transition: border-color 0.2s; }
|
|
1128
|
+
.list-item:hover { border-color: var(--primary); }
|
|
1129
|
+
.list-input { width: 100%; border: none; background: transparent; color: #fff; outline: none; font-size: 15px; }
|
|
1130
|
+
.drag-handle { color: var(--primary); font-size: 18px; }
|
|
1131
|
+
|
|
1132
|
+
${design.customCss ?? ""}
|
|
1133
|
+
</style>
|
|
1134
|
+
</head>
|
|
1135
|
+
<body>
|
|
1136
|
+
<div class="layout">
|
|
1137
|
+
<nav class="sidebar floating-window">
|
|
1138
|
+
<div id="serverRail" class="server-rail"></div>
|
|
1139
|
+
</nav>
|
|
1140
|
+
|
|
1141
|
+
<aside class="secondary-sidebar floating-window">
|
|
1142
|
+
<header class="sidebar-header brand">${safeName}</header>
|
|
1143
|
+
<div class="sidebar-content">
|
|
1144
|
+
<div class="category-header">System</div>
|
|
1145
|
+
<button id="tabHome" class="channel-btn cursor-pointer active">
|
|
1146
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
|
|
1147
|
+
Dashboard
|
|
1148
|
+
</button>
|
|
1149
|
+
<button id="tabPlugins" class="channel-btn cursor-pointer">
|
|
1150
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg>
|
|
1151
|
+
Extensions
|
|
1152
|
+
</button>
|
|
1153
|
+
|
|
1154
|
+
<div class="category-header" style="margin-top: 32px;">Modules</div>
|
|
1155
|
+
<div id="homeCategories"></div>
|
|
1156
|
+
</div>
|
|
1157
|
+
|
|
1158
|
+
<div class="user-area" id="userMeta">
|
|
1159
|
+
<div class="user-avatar-small"></div>
|
|
1160
|
+
<div class="user-details">
|
|
1161
|
+
<span class="name">Initializing...</span>
|
|
1162
|
+
<span class="sub">Standby</span>
|
|
1163
|
+
</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
</aside>
|
|
1166
|
+
|
|
1167
|
+
<main class="content">
|
|
1168
|
+
<header class="topbar floating-window">
|
|
1169
|
+
<span id="centerTitle" style="background: linear-gradient(90deg, #fff, var(--muted)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Interface Active</span>
|
|
1170
|
+
</header>
|
|
1171
|
+
|
|
1172
|
+
<div class="container">
|
|
1173
|
+
<section id="homeArea">
|
|
1174
|
+
<div class="section-title">Configuration Matrix</div>
|
|
1175
|
+
<section id="homeSections" class="home-sections"></section>
|
|
1176
|
+
|
|
1177
|
+
<section id="overviewArea">
|
|
1178
|
+
<div class="section-title" style="margin-top: 24px;">Telemetry Data</div>
|
|
1179
|
+
<section id="overviewCards" class="grid cards"></section>
|
|
1180
|
+
</section>
|
|
1181
|
+
</section>
|
|
1182
|
+
|
|
1183
|
+
<section id="pluginsArea" style="display:none;">
|
|
1184
|
+
<div class="section-title">Active Extensions</div>
|
|
1185
|
+
<section id="plugins" class="grid"></section>
|
|
1186
|
+
</section>
|
|
1187
|
+
</div>
|
|
1188
|
+
</main>
|
|
1189
|
+
</div>
|
|
1190
|
+
|
|
1191
|
+
<script>
|
|
1192
|
+
const dashboardConfig = ${scriptData};
|
|
1193
|
+
const state = { session: null, guilds: [], selectedGuildId: null, homeCategories: [], selectedHomeCategoryId: null, activeMainTab: "home" };
|
|
1194
|
+
|
|
1195
|
+
const el = {
|
|
1196
|
+
serverRail: document.getElementById("serverRail"), userMeta: document.getElementById("userMeta"),
|
|
1197
|
+
centerTitle: document.getElementById("centerTitle"), tabHome: document.getElementById("tabHome"),
|
|
1198
|
+
tabPlugins: document.getElementById("tabPlugins"), homeArea: document.getElementById("homeArea"),
|
|
1199
|
+
pluginsArea: document.getElementById("pluginsArea"), homeCategories: document.getElementById("homeCategories"),
|
|
1200
|
+
homeSections: document.getElementById("homeSections"), overviewArea: document.getElementById("overviewArea"),
|
|
1201
|
+
overviewCards: document.getElementById("overviewCards"), plugins: document.getElementById("plugins")
|
|
696
1202
|
};
|
|
697
1203
|
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1204
|
+
const fetchJson = async (url, init) => {
|
|
1205
|
+
const response = await fetch(url, init);
|
|
1206
|
+
if (!response.ok) throw new Error(await response.text());
|
|
1207
|
+
return response.json();
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
const buildApiUrl = (path) => {
|
|
1211
|
+
if (!state.selectedGuildId) return dashboardConfig.basePath + path;
|
|
1212
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
1213
|
+
return dashboardConfig.basePath + path + separator + "guildId=" + encodeURIComponent(state.selectedGuildId);
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
const escapeHtml = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1217
|
+
const normalizeBoxWidth = (value) => [50, 33, 20].includes(Number(value)) ? Number(value) : 100;
|
|
709
1218
|
|
|
710
|
-
|
|
711
|
-
items.forEach((item) => {
|
|
1219
|
+
const makeButton = (action, pluginId, panelId, panelElement) => {
|
|
712
1220
|
const button = document.createElement("button");
|
|
713
|
-
button.
|
|
714
|
-
|
|
1221
|
+
button.textContent = action.label;
|
|
1222
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
1223
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
715
1224
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1225
|
+
button.addEventListener("click", async () => {
|
|
1226
|
+
button.disabled = true;
|
|
1227
|
+
try {
|
|
1228
|
+
let payload = {};
|
|
1229
|
+
if (action.collectFields && panelElement) {
|
|
1230
|
+
const values = {};
|
|
1231
|
+
panelElement.querySelectorAll("[data-plugin-field-id]").forEach((inputEl) => {
|
|
1232
|
+
const fieldId = inputEl.dataset.pluginFieldId;
|
|
1233
|
+
if (fieldId) values[fieldId] = toFieldValue({ type: inputEl.dataset.pluginFieldType || "text" }, inputEl);
|
|
1234
|
+
});
|
|
1235
|
+
payload = { panelId, values };
|
|
1236
|
+
}
|
|
719
1237
|
|
|
720
|
-
|
|
1238
|
+
const result = await fetchJson(buildApiUrl("/api/plugins/" + encodeURIComponent(pluginId) + "/" + encodeURIComponent(action.id)), {
|
|
1239
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload)
|
|
1240
|
+
});
|
|
1241
|
+
if (result.message) alert(result.message);
|
|
1242
|
+
if (result.refresh) await refreshContent();
|
|
1243
|
+
} catch (error) { alert(error instanceof Error ? error.message : "Action failed"); }
|
|
1244
|
+
finally { button.disabled = false; }
|
|
1245
|
+
});
|
|
1246
|
+
return button;
|
|
1247
|
+
};
|
|
721
1248
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1249
|
+
const renderCards = (cards) => {
|
|
1250
|
+
if (!cards.length) { el.overviewCards.innerHTML = '<div style="color:var(--muted)">Awaiting telemetry...</div>'; return; }
|
|
1251
|
+
el.overviewCards.innerHTML = cards.map((card) =>
|
|
1252
|
+
'<article class="panel"><div class="title">' + escapeHtml(card.title) + '</div><div class="value">' + escapeHtml(card.value) + '</div>' +
|
|
1253
|
+
(card.subtitle ? '<div class="subtitle">' + escapeHtml(card.subtitle) + '</div>' : "") + '</article>'
|
|
1254
|
+
).join("");
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const shortName = (name) => {
|
|
1258
|
+
if (!name) return "?";
|
|
1259
|
+
const parts = String(name).trim().split(/\\s+/).filter(Boolean);
|
|
1260
|
+
return parts.length === 1 ? parts[0].slice(0, 2).toUpperCase() : (parts[0][0] + parts[1][0]).toUpperCase();
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
const addAvatarOrFallback = (button, item) => {
|
|
1264
|
+
if (!item.avatarUrl) {
|
|
1265
|
+
const fallback = document.createElement("span");
|
|
1266
|
+
fallback.className = "server-fallback";
|
|
1267
|
+
fallback.textContent = item.short;
|
|
1268
|
+
button.appendChild(fallback);
|
|
1269
|
+
return;
|
|
726
1270
|
}
|
|
1271
|
+
const avatar = document.createElement("img");
|
|
1272
|
+
avatar.className = "server-avatar";
|
|
1273
|
+
avatar.src = item.avatarUrl;
|
|
1274
|
+
avatar.alt = item.name;
|
|
1275
|
+
avatar.addEventListener("error", () => {
|
|
1276
|
+
avatar.remove();
|
|
1277
|
+
const fallback = document.createElement("span");
|
|
1278
|
+
fallback.className = "server-fallback";
|
|
1279
|
+
fallback.textContent = item.short;
|
|
1280
|
+
button.appendChild(fallback);
|
|
1281
|
+
});
|
|
1282
|
+
button.appendChild(avatar);
|
|
1283
|
+
};
|
|
727
1284
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1285
|
+
const renderServerRail = () => {
|
|
1286
|
+
const meItem = { id: null, name: "Global Control", short: "HQ", avatarUrl: state.session?.user?.avatarUrl ?? null, botInGuild: true };
|
|
1287
|
+
el.serverRail.innerHTML = "";
|
|
1288
|
+
|
|
1289
|
+
const meBtn = document.createElement("button");
|
|
1290
|
+
meBtn.className = "server-item cursor-pointer" + (null === state.selectedGuildId ? " active" : "");
|
|
1291
|
+
meBtn.title = meItem.name;
|
|
1292
|
+
|
|
1293
|
+
const meInd = document.createElement("span");
|
|
1294
|
+
meInd.className = "server-item-indicator";
|
|
1295
|
+
meBtn.appendChild(meInd);
|
|
1296
|
+
addAvatarOrFallback(meBtn, meItem);
|
|
1297
|
+
|
|
1298
|
+
meBtn.addEventListener("click", async () => {
|
|
1299
|
+
state.selectedGuildId = null;
|
|
1300
|
+
renderServerRail();
|
|
1301
|
+
updateContextLabel();
|
|
1302
|
+
await refreshContent();
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
el.serverRail.appendChild(meBtn);
|
|
1306
|
+
|
|
1307
|
+
const sep = document.createElement("div");
|
|
1308
|
+
sep.className = "server-separator";
|
|
1309
|
+
el.serverRail.appendChild(sep);
|
|
1310
|
+
|
|
1311
|
+
state.guilds.forEach((guild) => {
|
|
1312
|
+
const item = { id: guild.id, name: guild.name, short: shortName(guild.name), avatarUrl: guild.iconUrl ?? null, botInGuild: guild.botInGuild !== false, inviteUrl: guild.inviteUrl };
|
|
1313
|
+
const button = document.createElement("button");
|
|
1314
|
+
button.className = "server-item cursor-pointer" + (item.id === state.selectedGuildId ? " active" : "");
|
|
1315
|
+
button.title = item.id && !item.botInGuild ? (item.name + " \u2022 Deploy Bot") : item.name;
|
|
1316
|
+
|
|
1317
|
+
const activeIndicator = document.createElement("span");
|
|
1318
|
+
activeIndicator.className = "server-item-indicator";
|
|
1319
|
+
button.appendChild(activeIndicator);
|
|
1320
|
+
addAvatarOrFallback(button, item);
|
|
1321
|
+
|
|
1322
|
+
if (item.id) {
|
|
1323
|
+
const status = document.createElement("span");
|
|
1324
|
+
status.className = "server-status" + (item.botInGuild ? "" : " offline");
|
|
1325
|
+
button.appendChild(status);
|
|
733
1326
|
}
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
1327
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1328
|
+
button.addEventListener("click", async () => {
|
|
1329
|
+
if (item.id && !item.botInGuild && item.inviteUrl) {
|
|
1330
|
+
const opened = window.open(item.inviteUrl, "_blank", "noopener,noreferrer");
|
|
1331
|
+
if (!opened) alert("Popup blocked. Please allow popups to open the deployment page.");
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
state.selectedGuildId = item.id;
|
|
1335
|
+
renderServerRail();
|
|
1336
|
+
updateContextLabel();
|
|
1337
|
+
await refreshContent();
|
|
1338
|
+
});
|
|
1339
|
+
el.serverRail.appendChild(button);
|
|
741
1340
|
});
|
|
742
|
-
el.serverRail.appendChild(button);
|
|
743
|
-
});
|
|
744
1341
|
};
|
|
745
1342
|
|
|
746
1343
|
const applyMainTab = () => {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1344
|
+
const homeActive = state.activeMainTab === "home";
|
|
1345
|
+
el.homeArea.style.display = homeActive ? "block" : "none";
|
|
1346
|
+
el.pluginsArea.style.display = homeActive ? "none" : "block";
|
|
1347
|
+
el.tabHome.classList.toggle("active", homeActive);
|
|
1348
|
+
el.tabPlugins.classList.toggle("active", !homeActive);
|
|
752
1349
|
};
|
|
753
1350
|
|
|
754
1351
|
const updateContextLabel = () => {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1352
|
+
if (!state.selectedGuildId) { el.centerTitle.textContent = "Global Dashboard"; return; }
|
|
1353
|
+
const selectedGuild = state.guilds.find((guild) => guild.id === state.selectedGuildId);
|
|
1354
|
+
el.centerTitle.textContent = selectedGuild ? selectedGuild.name : "System Interface";
|
|
1355
|
+
};
|
|
759
1356
|
|
|
760
|
-
|
|
761
|
-
|
|
1357
|
+
const renderUserBlock = () => {
|
|
1358
|
+
const user = state.session?.user;
|
|
1359
|
+
if (!user) return;
|
|
1360
|
+
const avatarHtml = user.avatarUrl ? '<img src="'+user.avatarUrl+'" style="width:100%;height:100%;border-radius:12px;object-fit:cover;">' : '';
|
|
1361
|
+
el.userMeta.innerHTML =
|
|
1362
|
+
'<div class="user-avatar-small">'+avatarHtml+'</div>' +
|
|
1363
|
+
'<div class="user-details"><span class="name">' + escapeHtml(user.global_name || user.username) + '</span><span class="sub">' + state.guilds.length + ' Nodes</span></div>';
|
|
762
1364
|
};
|
|
763
1365
|
|
|
764
1366
|
const renderHomeCategories = () => {
|
|
765
|
-
if (!state.homeCategories.length) {
|
|
766
1367
|
el.homeCategories.innerHTML = "";
|
|
767
|
-
return;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
button.title = category.description || category.label;
|
|
776
|
-
button.addEventListener("click", async () => {
|
|
777
|
-
state.selectedHomeCategoryId = category.id;
|
|
778
|
-
renderHomeCategories();
|
|
779
|
-
await refreshContent();
|
|
1368
|
+
if (!state.homeCategories.length) return;
|
|
1369
|
+
state.homeCategories.forEach((category) => {
|
|
1370
|
+
const button = document.createElement("button");
|
|
1371
|
+
button.className = "channel-btn cursor-pointer" + (state.selectedHomeCategoryId === category.id ? " active" : "");
|
|
1372
|
+
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg> ' + escapeHtml(category.label);
|
|
1373
|
+
button.title = category.description || category.label;
|
|
1374
|
+
button.addEventListener("click", async () => { state.selectedHomeCategoryId = category.id; renderHomeCategories(); await refreshContent(); });
|
|
1375
|
+
el.homeCategories.appendChild(button);
|
|
780
1376
|
});
|
|
781
|
-
el.homeCategories.appendChild(button);
|
|
782
|
-
});
|
|
783
1377
|
};
|
|
784
1378
|
|
|
785
1379
|
const toFieldValue = (field, element) => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
try {
|
|
793
|
-
const parsed = JSON.parse(raw);
|
|
794
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
795
|
-
} catch {
|
|
796
|
-
return [];
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (field.type === "role-search" || field.type === "channel-search" || field.type === "member-search") {
|
|
801
|
-
const raw = element.dataset.selectedObject;
|
|
802
|
-
if (!raw) {
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
try {
|
|
807
|
-
return JSON.parse(raw);
|
|
808
|
-
} catch {
|
|
809
|
-
return null;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (field.type === "boolean") {
|
|
814
|
-
return Boolean(element.checked);
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if (field.type === "number") {
|
|
818
|
-
if (element.value === "") {
|
|
819
|
-
return null;
|
|
820
|
-
}
|
|
821
|
-
return Number(element.value);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
return element.value;
|
|
1380
|
+
if (field.type === "string-list") { try { return JSON.parse(element.dataset.listValues || "[]"); } catch { return []; } }
|
|
1381
|
+
if (["role-search", "channel-search", "member-search"].includes(field.type)) { try { return JSON.parse(element.dataset.selectedObject || "null"); } catch { return null; } }
|
|
1382
|
+
if (field.type === "boolean") return Boolean(element.checked);
|
|
1383
|
+
if (field.type === "number") return element.value === "" ? null : Number(element.value);
|
|
1384
|
+
return element.value;
|
|
825
1385
|
};
|
|
826
1386
|
|
|
827
1387
|
const setupStringListField = (field, input, fieldWrap) => {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1388
|
+
const editor = document.createElement("div"); editor.className = "list-editor";
|
|
1389
|
+
const itemsWrap = document.createElement("div"); itemsWrap.className = "list-items";
|
|
1390
|
+
const addButton = document.createElement("button"); addButton.type = "button"; addButton.className = "list-add cursor-pointer"; addButton.textContent = "+ Add Sequence";
|
|
1391
|
+
|
|
1392
|
+
const normalizeValues = () => { input.dataset.listValues = JSON.stringify(Array.from(itemsWrap.querySelectorAll(".list-input")).map(i => i.value.trim()).filter(i => i.length > 0)); };
|
|
1393
|
+
|
|
1394
|
+
const makeRow = (value = "") => {
|
|
1395
|
+
const row = document.createElement("div"); row.className = "list-item"; row.draggable = true;
|
|
1396
|
+
const handle = document.createElement("span"); handle.className = "drag-handle cursor-pointer"; handle.innerHTML = "\u28FF";
|
|
1397
|
+
const textInput = document.createElement("input"); textInput.type = "text"; textInput.className = "list-input"; textInput.value = value;
|
|
1398
|
+
textInput.addEventListener("input", normalizeValues);
|
|
1399
|
+
const removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.className = "cursor-pointer"; removeBtn.textContent = "\xD7"; removeBtn.style.padding = "4px 8px"; removeBtn.style.background = "transparent"; removeBtn.style.color = "var(--danger)";
|
|
1400
|
+
removeBtn.addEventListener("click", () => { row.remove(); normalizeValues(); });
|
|
1401
|
+
row.append(handle, textInput, removeBtn);
|
|
1402
|
+
return row;
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
const initialValues = Array.isArray(field.value) ? field.value.map(String) : [];
|
|
1406
|
+
if (!initialValues.length) initialValues.push("Value_01");
|
|
1407
|
+
initialValues.forEach(v => itemsWrap.appendChild(makeRow(v)));
|
|
1408
|
+
|
|
1409
|
+
addButton.addEventListener("click", () => { itemsWrap.appendChild(makeRow("")); normalizeValues(); });
|
|
1410
|
+
editor.append(itemsWrap, addButton); fieldWrap.appendChild(editor); normalizeValues();
|
|
1411
|
+
};
|
|
843
1412
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
handle.textContent = "\u22EE\u22EE";
|
|
852
|
-
|
|
853
|
-
const textInput = document.createElement("input");
|
|
854
|
-
textInput.type = "text";
|
|
855
|
-
textInput.className = "list-input";
|
|
856
|
-
textInput.value = value;
|
|
857
|
-
textInput.placeholder = "Button label";
|
|
858
|
-
textInput.addEventListener("input", normalizeValues);
|
|
859
|
-
|
|
860
|
-
const removeButton = document.createElement("button");
|
|
861
|
-
removeButton.type = "button";
|
|
862
|
-
removeButton.className = "cursor-pointer";
|
|
863
|
-
removeButton.textContent = "\xD7";
|
|
864
|
-
removeButton.addEventListener("click", () => {
|
|
865
|
-
row.remove();
|
|
866
|
-
normalizeValues();
|
|
1413
|
+
const showLookupResults = (container, items, labelResolver, onSelect) => {
|
|
1414
|
+
container.innerHTML = "";
|
|
1415
|
+
if (!items.length) { container.style.display = "none"; return; }
|
|
1416
|
+
items.forEach(item => {
|
|
1417
|
+
const btn = document.createElement("button"); btn.type = "button"; btn.className = "lookup-item cursor-pointer"; btn.textContent = labelResolver(item);
|
|
1418
|
+
btn.addEventListener("click", () => { onSelect(item); container.style.display = "none"; });
|
|
1419
|
+
container.appendChild(btn);
|
|
867
1420
|
});
|
|
1421
|
+
container.style.display = "block";
|
|
1422
|
+
};
|
|
868
1423
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1424
|
+
const setupLookupField = (field, input, fieldWrap) => {
|
|
1425
|
+
const wrap = document.createElement("div"); wrap.className = "lookup-wrap";
|
|
1426
|
+
const results = document.createElement("div"); results.className = "lookup-results";
|
|
1427
|
+
wrap.append(input, results); fieldWrap.appendChild(wrap);
|
|
1428
|
+
|
|
1429
|
+
const runSearch = async () => {
|
|
1430
|
+
const query = String(input.value || "");
|
|
1431
|
+
if (query.length < 1) { results.style.display = "none"; return; }
|
|
1432
|
+
try {
|
|
1433
|
+
const params = new URLSearchParams({ q: query, limit: "10" });
|
|
1434
|
+
const ep = field.type === "role-search" ? "roles" : field.type === "channel-search" ? "channels" : "members";
|
|
1435
|
+
const payload = await fetchJson(buildApiUrl("/api/lookup/" + ep + "?" + params));
|
|
1436
|
+
|
|
1437
|
+
showLookupResults(results, payload[ep] || [],
|
|
1438
|
+
(item) => field.type === "channel-search" ? "#"+item.name : (item.name || item.user?.username),
|
|
1439
|
+
(item) => { input.value = item.name || item.user?.username; input.dataset.selectedObject = JSON.stringify(item); }
|
|
1440
|
+
);
|
|
1441
|
+
} catch { results.style.display = "none"; }
|
|
1442
|
+
};
|
|
1443
|
+
input.addEventListener("input", () => { input.dataset.selectedObject = ""; runSearch(); });
|
|
1444
|
+
input.addEventListener("blur", () => setTimeout(() => { results.style.display = "none"; }, 150));
|
|
1445
|
+
};
|
|
876
1446
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1447
|
+
const renderHomeSections = (sections) => {
|
|
1448
|
+
el.homeSections.innerHTML = "";
|
|
1449
|
+
if (!sections.length) return;
|
|
1450
|
+
|
|
1451
|
+
sections.forEach((section) => {
|
|
1452
|
+
const wrap = document.createElement("article");
|
|
1453
|
+
wrap.className = "panel home-section-panel home-width-" + normalizeBoxWidth(section.width);
|
|
1454
|
+
|
|
1455
|
+
const heading = document.createElement("h3"); heading.textContent = section.title; heading.style.margin = "0"; wrap.appendChild(heading);
|
|
1456
|
+
if (section.description) { const desc = document.createElement("div"); desc.className = "subtitle"; desc.textContent = section.description; wrap.appendChild(desc); }
|
|
1457
|
+
|
|
1458
|
+
if (section.fields?.length) {
|
|
1459
|
+
const fieldsWrap = document.createElement("div"); fieldsWrap.className = "home-fields";
|
|
1460
|
+
|
|
1461
|
+
section.fields.forEach((field) => {
|
|
1462
|
+
const fieldWrap = document.createElement("div"); fieldWrap.className = "home-field";
|
|
1463
|
+
const label = document.createElement("label"); label.textContent = field.label; fieldWrap.appendChild(label);
|
|
1464
|
+
|
|
1465
|
+
let input;
|
|
1466
|
+
if (field.type === "textarea") {
|
|
1467
|
+
input = document.createElement("textarea"); input.className = "home-textarea"; input.value = field.value == null ? "" : String(field.value);
|
|
1468
|
+
} else if (field.type === "select") {
|
|
1469
|
+
input = document.createElement("select"); input.className = "home-select cursor-pointer";
|
|
1470
|
+
(field.options || []).forEach((opt) => {
|
|
1471
|
+
const optionEl = document.createElement("option"); optionEl.value = opt.value; optionEl.textContent = opt.label;
|
|
1472
|
+
if (String(field.value ?? "") === opt.value) optionEl.selected = true; input.appendChild(optionEl);
|
|
1473
|
+
});
|
|
1474
|
+
} else if (field.type === "boolean") {
|
|
1475
|
+
const row = document.createElement("div"); row.className = "home-field-row";
|
|
1476
|
+
input = document.createElement("input"); input.type = "checkbox"; input.className = "home-checkbox cursor-pointer"; input.checked = Boolean(field.value);
|
|
1477
|
+
const stateText = document.createElement("span"); stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
1478
|
+
input.addEventListener("change", () => stateText.textContent = input.checked ? "Enabled" : "Disabled");
|
|
1479
|
+
row.append(input, stateText); fieldWrap.appendChild(row);
|
|
1480
|
+
} else {
|
|
1481
|
+
input = document.createElement("input"); input.className = "home-input"; input.type = field.type === "number" ? "number" : field.type === "url" ? "url" : "text"; input.value = field.value == null ? "" : String(field.value);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (input) {
|
|
1485
|
+
input.dataset.homeFieldId = field.id; input.dataset.homeFieldType = field.type;
|
|
1486
|
+
if (field.placeholder) input.placeholder = field.placeholder;
|
|
1487
|
+
if (field.readOnly) { input.readOnly = true; input.disabled = true; input.style.opacity = "0.6"; }
|
|
1488
|
+
if (["role-search", "channel-search", "member-search"].includes(field.type)) setupLookupField(field, input, fieldWrap);
|
|
1489
|
+
else if (field.type === "string-list") setupStringListField(field, input, fieldWrap);
|
|
1490
|
+
else if (field.type !== "boolean") fieldWrap.appendChild(input);
|
|
1491
|
+
}
|
|
1492
|
+
fieldsWrap.appendChild(fieldWrap);
|
|
1493
|
+
});
|
|
1494
|
+
wrap.appendChild(fieldsWrap);
|
|
1495
|
+
}
|
|
882
1496
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1497
|
+
if (section.actions?.length) {
|
|
1498
|
+
const actions = document.createElement("div"); actions.className = "actions";
|
|
1499
|
+
section.actions.forEach((action) => {
|
|
1500
|
+
const button = document.createElement("button"); button.textContent = action.label;
|
|
1501
|
+
const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
|
|
1502
|
+
button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
|
|
1503
|
+
button.addEventListener("click", async () => {
|
|
1504
|
+
button.disabled = true;
|
|
1505
|
+
try {
|
|
1506
|
+
const values = {};
|
|
1507
|
+
wrap.querySelectorAll("[data-home-field-id]").forEach((el) => { values[el.dataset.homeFieldId] = toFieldValue({ type: el.dataset.homeFieldType }, el); });
|
|
1508
|
+
const result = await fetchJson(buildApiUrl("/api/home/" + encodeURIComponent(action.id)), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sectionId: section.id, values }) });
|
|
1509
|
+
if (result.message) alert(result.message);
|
|
1510
|
+
if (result.refresh) await refreshContent();
|
|
1511
|
+
} catch (error) { alert("Execution Failed"); } finally { button.disabled = false; }
|
|
1512
|
+
});
|
|
1513
|
+
actions.appendChild(button);
|
|
1514
|
+
});
|
|
1515
|
+
wrap.appendChild(actions);
|
|
1516
|
+
}
|
|
1517
|
+
el.homeSections.appendChild(wrap);
|
|
1518
|
+
});
|
|
1519
|
+
};
|
|
889
1520
|
|
|
890
|
-
|
|
891
|
-
|
|
1521
|
+
const renderPlugins = (plugins) => {
|
|
1522
|
+
el.plugins.innerHTML = "";
|
|
1523
|
+
if (!plugins.length) { el.plugins.innerHTML = '<div style="color:var(--muted)">No modules online.</div>'; return; }
|
|
1524
|
+
|
|
1525
|
+
plugins.forEach((plugin) => {
|
|
1526
|
+
const wrap = document.createElement("article"); wrap.className = "panel";
|
|
1527
|
+
const heading = document.createElement("h3"); heading.textContent = plugin.name; heading.style.margin="0"; wrap.appendChild(heading);
|
|
1528
|
+
if (plugin.description) { const desc = document.createElement("div"); desc.className = "subtitle"; desc.textContent = plugin.description; wrap.appendChild(desc); }
|
|
1529
|
+
|
|
1530
|
+
(plugin.panels || []).forEach((panel) => {
|
|
1531
|
+
const pBody = document.createElement("div"); pBody.style.marginTop = "24px";
|
|
1532
|
+
const pTitle = document.createElement("h4"); pTitle.textContent = panel.title; pTitle.style.margin = "0 0 12px 0"; pTitle.style.color = "var(--primary)"; pTitle.style.textTransform = "uppercase"; pBody.appendChild(pTitle);
|
|
1533
|
+
|
|
1534
|
+
if (panel.fields?.length) {
|
|
1535
|
+
const fWrap = document.createElement("div"); fWrap.className = "home-fields";
|
|
1536
|
+
panel.fields.forEach((field) => {
|
|
1537
|
+
const fw = document.createElement("div"); fw.className = "home-field";
|
|
1538
|
+
if (!field.editable) {
|
|
1539
|
+
fw.innerHTML = '<label>' + escapeHtml(field.label) + '</label><div style="padding:14px 18px;background:var(--panel-2);border-radius:12px;border:1px solid var(--border);">' + escapeHtml(field.value) + '</div>';
|
|
1540
|
+
} else {
|
|
1541
|
+
fw.innerHTML = '<label>' + escapeHtml(field.label) + '</label><input type="text" class="home-input" data-plugin-field-id="'+field.id+'" value="'+escapeHtml(field.value)+'">';
|
|
1542
|
+
}
|
|
1543
|
+
fWrap.appendChild(fw);
|
|
1544
|
+
});
|
|
1545
|
+
pBody.appendChild(fWrap);
|
|
1546
|
+
}
|
|
892
1547
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1548
|
+
if (panel.actions?.length) {
|
|
1549
|
+
const actions = document.createElement("div"); actions.className = "actions";
|
|
1550
|
+
panel.actions.forEach((act) => actions.appendChild(makeButton(act, plugin.id, panel.id, pBody)));
|
|
1551
|
+
pBody.appendChild(actions);
|
|
1552
|
+
}
|
|
1553
|
+
wrap.appendChild(pBody);
|
|
1554
|
+
});
|
|
1555
|
+
el.plugins.appendChild(wrap);
|
|
1556
|
+
});
|
|
1557
|
+
};
|
|
901
1558
|
|
|
902
|
-
|
|
903
|
-
|
|
1559
|
+
const refreshContent = async () => {
|
|
1560
|
+
const categoriesPayload = await fetchJson(buildApiUrl("/api/home/categories"));
|
|
1561
|
+
state.homeCategories = categoriesPayload.categories || [];
|
|
1562
|
+
if (!state.selectedHomeCategoryId || !state.homeCategories.some(i => i.id === state.selectedHomeCategoryId)) {
|
|
1563
|
+
state.selectedHomeCategoryId = state.homeCategories.find(i => i.id === "overview")?.id || state.homeCategories[0]?.id || null;
|
|
904
1564
|
}
|
|
905
|
-
|
|
1565
|
+
renderHomeCategories();
|
|
906
1566
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
: [];
|
|
910
|
-
if (initialValues.length === 0) {
|
|
911
|
-
initialValues.push("Yes", "No");
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
initialValues.forEach((value) => {
|
|
915
|
-
itemsWrap.appendChild(makeRow(value));
|
|
916
|
-
});
|
|
1567
|
+
const homePath = state.selectedHomeCategoryId ? "/api/home?categoryId=" + encodeURIComponent(state.selectedHomeCategoryId) : "/api/home";
|
|
1568
|
+
const [home, overview, plugins] = await Promise.all([ fetchJson(buildApiUrl(homePath)), fetchJson(buildApiUrl("/api/overview")), fetchJson(buildApiUrl("/api/plugins")) ]);
|
|
917
1569
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
editor.appendChild(itemsWrap);
|
|
924
|
-
editor.appendChild(addButton);
|
|
925
|
-
fieldWrap.appendChild(editor);
|
|
926
|
-
normalizeValues();
|
|
1570
|
+
renderHomeSections(home.sections || []);
|
|
1571
|
+
el.overviewArea.style.display = state.selectedHomeCategoryId === "overview" ? "block" : "none";
|
|
1572
|
+
renderCards(overview.cards || []);
|
|
1573
|
+
renderPlugins(plugins.plugins || []);
|
|
927
1574
|
};
|
|
928
1575
|
|
|
929
|
-
const
|
|
930
|
-
|
|
1576
|
+
const loadInitialData = async () => {
|
|
1577
|
+
const session = await fetchJson(dashboardConfig.basePath + "/api/session");
|
|
1578
|
+
if (!session.authenticated) { window.location.href = dashboardConfig.basePath + "/login"; return; }
|
|
931
1579
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1580
|
+
state.session = session;
|
|
1581
|
+
const guilds = await fetchJson(dashboardConfig.basePath + "/api/guilds");
|
|
1582
|
+
state.guilds = guilds.guilds || [];
|
|
1583
|
+
|
|
1584
|
+
renderUserBlock();
|
|
1585
|
+
renderServerRail();
|
|
1586
|
+
updateContextLabel();
|
|
936
1587
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
btn.type = "button";
|
|
940
|
-
btn.className = "lookup-item cursor-pointer";
|
|
941
|
-
btn.textContent = labelResolver(item);
|
|
942
|
-
btn.addEventListener("click", () => {
|
|
943
|
-
onSelect(item);
|
|
944
|
-
container.style.display = "none";
|
|
945
|
-
});
|
|
946
|
-
container.appendChild(btn);
|
|
947
|
-
});
|
|
1588
|
+
el.tabHome.addEventListener("click", () => { state.activeMainTab = "home"; applyMainTab(); });
|
|
1589
|
+
el.tabPlugins.addEventListener("click", () => { state.activeMainTab = "plugins"; applyMainTab(); });
|
|
948
1590
|
|
|
949
|
-
|
|
1591
|
+
applyMainTab();
|
|
1592
|
+
await refreshContent();
|
|
950
1593
|
};
|
|
951
1594
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
const selected = document.createElement("div");
|
|
958
|
-
selected.className = "lookup-selected";
|
|
959
|
-
selected.textContent = "No selection";
|
|
960
|
-
|
|
961
|
-
wrap.appendChild(input);
|
|
962
|
-
wrap.appendChild(results);
|
|
963
|
-
fieldWrap.appendChild(wrap);
|
|
964
|
-
fieldWrap.appendChild(selected);
|
|
965
|
-
|
|
966
|
-
const minQueryLength = Math.max(0, field.lookup?.minQueryLength ?? 1);
|
|
967
|
-
const limit = Math.max(1, Math.min(field.lookup?.limit ?? 10, 50));
|
|
968
|
-
|
|
969
|
-
const runSearch = async () => {
|
|
970
|
-
const query = String(input.value || "");
|
|
971
|
-
if (query.length < minQueryLength) {
|
|
972
|
-
results.style.display = "none";
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
1595
|
+
loadInitialData().catch(console.error);
|
|
1596
|
+
</script>
|
|
1597
|
+
</body>
|
|
1598
|
+
</html>`;
|
|
1599
|
+
}
|
|
975
1600
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1601
|
+
// src/templates/layouts/shadcn-magic.ts
|
|
1602
|
+
function escapeHtml3(value) {
|
|
1603
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1604
|
+
}
|
|
1605
|
+
function renderShadcnMagicLayout(context) {
|
|
1606
|
+
const safeName = escapeHtml3(context.dashboardName);
|
|
1607
|
+
const design = context.setupDesign ?? {};
|
|
1608
|
+
const clientScript = getClientScript(context.basePath, design);
|
|
1609
|
+
return `<!DOCTYPE html>
|
|
1610
|
+
<html lang="en">
|
|
1611
|
+
<head>
|
|
1612
|
+
<meta charset="UTF-8" />
|
|
1613
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1614
|
+
<title>${safeName}</title>
|
|
1615
|
+
<style>
|
|
1616
|
+
:root {
|
|
1617
|
+
color-scheme: dark;
|
|
1618
|
+
--bg: ${design.bg ?? "#06070b"};
|
|
1619
|
+
--surface: ${design.surface ?? "rgba(10, 12, 18, 0.84)"};
|
|
1620
|
+
--card: ${design.card ?? "rgba(16, 20, 30, 0.82)"};
|
|
1621
|
+
--card-2: ${design.card2 ?? "rgba(23, 29, 42, 0.86)"};
|
|
1622
|
+
--text: ${design.text ?? "#f8fafc"};
|
|
1623
|
+
--muted: ${design.muted ?? "#94a3b8"};
|
|
1624
|
+
--primary: ${design.primary ?? "#c084fc"};
|
|
1625
|
+
--accent: ${design.accent ?? "#22d3ee"};
|
|
1626
|
+
--border: ${design.border ?? "rgba(148, 163, 184, 0.26)"};
|
|
1627
|
+
--radius-lg: ${design.radiusLg ?? "18px"};
|
|
1628
|
+
--radius-md: ${design.radiusMd ?? "12px"};
|
|
1629
|
+
}
|
|
982
1630
|
|
|
983
|
-
|
|
984
|
-
showLookupResults(
|
|
985
|
-
results,
|
|
986
|
-
payload.roles || [],
|
|
987
|
-
(item) => "@" + item.name,
|
|
988
|
-
(item) => {
|
|
989
|
-
input.value = item.name;
|
|
990
|
-
input.dataset.selectedObject = JSON.stringify(item);
|
|
991
|
-
selected.textContent = "Selected role: @" + item.name + " (" + item.id + ")";
|
|
992
|
-
}
|
|
993
|
-
);
|
|
994
|
-
} else if (field.type === "channel-search") {
|
|
995
|
-
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
996
|
-
if (field.lookup?.nsfw !== undefined) {
|
|
997
|
-
params.set("nsfw", String(Boolean(field.lookup.nsfw)));
|
|
998
|
-
}
|
|
999
|
-
if (field.lookup?.channelTypes && field.lookup.channelTypes.length > 0) {
|
|
1000
|
-
params.set("channelTypes", field.lookup.channelTypes.join(","));
|
|
1001
|
-
}
|
|
1631
|
+
* { box-sizing: border-box; }
|
|
1002
1632
|
|
|
1003
|
-
|
|
1004
|
-
showLookupResults(
|
|
1005
|
-
results,
|
|
1006
|
-
payload.channels || [],
|
|
1007
|
-
(item) => "#" + item.name,
|
|
1008
|
-
(item) => {
|
|
1009
|
-
input.value = item.name;
|
|
1010
|
-
input.dataset.selectedObject = JSON.stringify(item);
|
|
1011
|
-
selected.textContent = "Selected channel: #" + item.name + " (" + item.id + ")";
|
|
1012
|
-
}
|
|
1013
|
-
);
|
|
1014
|
-
} else if (field.type === "member-search") {
|
|
1015
|
-
const params = new URLSearchParams({ q: query, limit: String(limit) });
|
|
1016
|
-
const payload = await fetchJson(buildApiUrl("/api/lookup/members?" + params.toString()));
|
|
1017
|
-
showLookupResults(
|
|
1018
|
-
results,
|
|
1019
|
-
payload.members || [],
|
|
1020
|
-
(item) => {
|
|
1021
|
-
const username = item?.user?.username || "unknown";
|
|
1022
|
-
const nick = item?.nick ? " (" + item.nick + ")" : "";
|
|
1023
|
-
return username + nick;
|
|
1024
|
-
},
|
|
1025
|
-
(item) => {
|
|
1026
|
-
const username = item?.user?.username || "unknown";
|
|
1027
|
-
input.value = username;
|
|
1028
|
-
input.dataset.selectedObject = JSON.stringify(item);
|
|
1029
|
-
selected.textContent = "Selected member: " + username + " (" + (item?.user?.id || "unknown") + ")";
|
|
1030
|
-
}
|
|
1031
|
-
);
|
|
1032
|
-
}
|
|
1033
|
-
} catch {
|
|
1034
|
-
results.style.display = "none";
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1633
|
+
html, body { min-height: 100%; }
|
|
1037
1634
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1635
|
+
body {
|
|
1636
|
+
margin: 0;
|
|
1637
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
|
1638
|
+
color: var(--text);
|
|
1639
|
+
background:
|
|
1640
|
+
radial-gradient(circle at 8% 12%, rgba(192,132,252,.22), transparent 34%),
|
|
1641
|
+
radial-gradient(circle at 92% 8%, rgba(34,211,238,.18), transparent 36%),
|
|
1642
|
+
radial-gradient(circle at 76% 86%, rgba(244,114,182,.14), transparent 34%),
|
|
1643
|
+
var(--bg);
|
|
1644
|
+
}
|
|
1043
1645
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1646
|
+
body::before {
|
|
1647
|
+
content: "";
|
|
1648
|
+
position: fixed;
|
|
1649
|
+
inset: 0;
|
|
1650
|
+
pointer-events: none;
|
|
1651
|
+
background-image:
|
|
1652
|
+
linear-gradient(rgba(148,163,184,.06) 1px, transparent 1px),
|
|
1653
|
+
linear-gradient(90deg, rgba(148,163,184,.06) 1px, transparent 1px);
|
|
1654
|
+
background-size: 24px 24px;
|
|
1655
|
+
mask-image: radial-gradient(circle at center, rgba(0,0,0,.9), transparent 72%);
|
|
1656
|
+
}
|
|
1050
1657
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1658
|
+
.page {
|
|
1659
|
+
min-height: 100vh;
|
|
1660
|
+
width: 100%;
|
|
1661
|
+
padding: 0;
|
|
1662
|
+
}
|
|
1056
1663
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (section.description) {
|
|
1068
|
-
const desc = document.createElement("div");
|
|
1069
|
-
desc.className = "subtitle";
|
|
1070
|
-
desc.textContent = section.description;
|
|
1071
|
-
wrap.appendChild(desc);
|
|
1072
|
-
}
|
|
1664
|
+
.shell {
|
|
1665
|
+
border: 1px solid var(--border);
|
|
1666
|
+
border-radius: 0;
|
|
1667
|
+
background: linear-gradient(180deg, rgba(8, 10, 14, .78), rgba(6, 8, 12, .72));
|
|
1668
|
+
backdrop-filter: blur(10px);
|
|
1669
|
+
min-height: 100vh;
|
|
1670
|
+
width: 100%;
|
|
1671
|
+
overflow: hidden;
|
|
1672
|
+
}
|
|
1073
1673
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
fieldWrap.className = "home-field";
|
|
1084
|
-
|
|
1085
|
-
const label = document.createElement("label");
|
|
1086
|
-
label.textContent = field.label;
|
|
1087
|
-
fieldWrap.appendChild(label);
|
|
1088
|
-
|
|
1089
|
-
let input;
|
|
1090
|
-
if (field.type === "textarea") {
|
|
1091
|
-
input = document.createElement("textarea");
|
|
1092
|
-
input.className = "home-textarea";
|
|
1093
|
-
input.value = field.value == null ? "" : String(field.value);
|
|
1094
|
-
} else if (field.type === "select") {
|
|
1095
|
-
input = document.createElement("select");
|
|
1096
|
-
input.className = "home-select cursor-pointer";
|
|
1097
|
-
(field.options || []).forEach((option) => {
|
|
1098
|
-
const optionEl = document.createElement("option");
|
|
1099
|
-
optionEl.value = option.value;
|
|
1100
|
-
optionEl.textContent = option.label;
|
|
1101
|
-
if (String(field.value ?? "") === option.value) {
|
|
1102
|
-
optionEl.selected = true;
|
|
1103
|
-
}
|
|
1104
|
-
input.appendChild(optionEl);
|
|
1105
|
-
});
|
|
1106
|
-
} else if (field.type === "boolean") {
|
|
1107
|
-
const row = document.createElement("div");
|
|
1108
|
-
row.className = "home-field-row";
|
|
1109
|
-
input = document.createElement("input");
|
|
1110
|
-
input.type = "checkbox";
|
|
1111
|
-
input.className = "home-checkbox cursor-pointer";
|
|
1112
|
-
input.checked = Boolean(field.value);
|
|
1113
|
-
const stateText = document.createElement("span");
|
|
1114
|
-
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
1115
|
-
input.addEventListener("change", () => {
|
|
1116
|
-
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
1117
|
-
});
|
|
1118
|
-
row.appendChild(input);
|
|
1119
|
-
row.appendChild(stateText);
|
|
1120
|
-
fieldWrap.appendChild(row);
|
|
1121
|
-
} else {
|
|
1122
|
-
input = document.createElement("input");
|
|
1123
|
-
input.className = "home-input";
|
|
1124
|
-
input.type = field.type === "number" ? "number" : "text";
|
|
1125
|
-
input.value = field.value == null ? "" : String(field.value);
|
|
1126
|
-
}
|
|
1674
|
+
.topbar {
|
|
1675
|
+
display: grid;
|
|
1676
|
+
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
1677
|
+
gap: 12px;
|
|
1678
|
+
align-items: center;
|
|
1679
|
+
padding: 14px 16px;
|
|
1680
|
+
border-bottom: 1px solid var(--border);
|
|
1681
|
+
background: rgba(6, 8, 12, .66);
|
|
1682
|
+
}
|
|
1127
1683
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
}
|
|
1134
|
-
if (field.required && "required" in input) {
|
|
1135
|
-
input.required = true;
|
|
1136
|
-
}
|
|
1137
|
-
if (field.readOnly) {
|
|
1138
|
-
if ("readOnly" in input) {
|
|
1139
|
-
input.readOnly = true;
|
|
1140
|
-
}
|
|
1141
|
-
if ("disabled" in input) {
|
|
1142
|
-
input.disabled = true;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1684
|
+
.brand {
|
|
1685
|
+
font-weight: 700;
|
|
1686
|
+
font-size: 1rem;
|
|
1687
|
+
letter-spacing: .2px;
|
|
1688
|
+
}
|
|
1145
1689
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1690
|
+
.center-title {
|
|
1691
|
+
border: 1px solid var(--border);
|
|
1692
|
+
background: rgba(15, 23, 42, .44);
|
|
1693
|
+
border-radius: 999px;
|
|
1694
|
+
padding: 6px 12px;
|
|
1695
|
+
font-size: .82rem;
|
|
1696
|
+
color: #e2e8f0;
|
|
1697
|
+
}
|
|
1152
1698
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1699
|
+
.pill {
|
|
1700
|
+
border: 1px solid var(--border);
|
|
1701
|
+
border-radius: 999px;
|
|
1702
|
+
padding: 6px 12px;
|
|
1703
|
+
font-size: .78rem;
|
|
1704
|
+
color: var(--muted);
|
|
1705
|
+
background: rgba(15, 23, 42, .52);
|
|
1706
|
+
}
|
|
1155
1707
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1708
|
+
.server-strip {
|
|
1709
|
+
padding: 12px 16px;
|
|
1710
|
+
border-bottom: 1px solid var(--border);
|
|
1711
|
+
background: rgba(8, 12, 18, .48);
|
|
1712
|
+
}
|
|
1158
1713
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1714
|
+
.server-rail {
|
|
1715
|
+
display: flex;
|
|
1716
|
+
gap: 10px;
|
|
1717
|
+
overflow-x: auto;
|
|
1718
|
+
padding-bottom: 2px;
|
|
1719
|
+
scrollbar-width: thin;
|
|
1720
|
+
}
|
|
1162
1721
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1722
|
+
.server-item {
|
|
1723
|
+
position: relative;
|
|
1724
|
+
min-width: 210px;
|
|
1725
|
+
height: 60px;
|
|
1726
|
+
border-radius: var(--radius-md);
|
|
1727
|
+
border: 1px solid var(--border);
|
|
1728
|
+
background: linear-gradient(180deg, rgba(30, 41, 59, .74), rgba(15, 23, 42, .7));
|
|
1729
|
+
color: var(--text);
|
|
1730
|
+
font-weight: 600;
|
|
1731
|
+
display: flex;
|
|
1732
|
+
align-items: center;
|
|
1733
|
+
justify-content: flex-start;
|
|
1734
|
+
padding: 8px 72px 8px 56px;
|
|
1735
|
+
transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
|
|
1736
|
+
}
|
|
1180
1737
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1738
|
+
.server-item::before {
|
|
1739
|
+
content: attr(title);
|
|
1740
|
+
display: block;
|
|
1741
|
+
max-width: 100%;
|
|
1742
|
+
font-size: .82rem;
|
|
1743
|
+
font-weight: 600;
|
|
1744
|
+
line-height: 1.2;
|
|
1745
|
+
letter-spacing: .1px;
|
|
1746
|
+
color: #e2e8f0;
|
|
1747
|
+
white-space: nowrap;
|
|
1748
|
+
overflow: hidden;
|
|
1749
|
+
text-overflow: ellipsis;
|
|
1750
|
+
}
|
|
1183
1751
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
sectionId: section.id,
|
|
1189
|
-
values
|
|
1190
|
-
})
|
|
1191
|
-
});
|
|
1752
|
+
.server-item:hover {
|
|
1753
|
+
transform: translateY(-2px);
|
|
1754
|
+
border-color: rgba(192,132,252,.72);
|
|
1755
|
+
}
|
|
1192
1756
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
} catch (error) {
|
|
1198
|
-
message.textContent = error instanceof Error ? error.message : "Save failed";
|
|
1199
|
-
} finally {
|
|
1200
|
-
button.disabled = false;
|
|
1201
|
-
}
|
|
1202
|
-
});
|
|
1203
|
-
actions.appendChild(button);
|
|
1204
|
-
});
|
|
1757
|
+
.server-item.active {
|
|
1758
|
+
border-color: rgba(34,211,238,.86);
|
|
1759
|
+
box-shadow: 0 0 0 1px rgba(34,211,238,.3), 0 10px 26px rgba(34,211,238,.17);
|
|
1760
|
+
}
|
|
1205
1761
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1762
|
+
.server-item-indicator {
|
|
1763
|
+
position: absolute;
|
|
1764
|
+
left: 8px;
|
|
1765
|
+
top: 50%;
|
|
1766
|
+
transform: translateY(-50%);
|
|
1767
|
+
width: 4px;
|
|
1768
|
+
height: 0;
|
|
1769
|
+
border-radius: 999px;
|
|
1770
|
+
background: linear-gradient(180deg, var(--primary), var(--accent));
|
|
1771
|
+
opacity: 0;
|
|
1772
|
+
transition: opacity .15s ease, height .15s ease;
|
|
1773
|
+
}
|
|
1208
1774
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1775
|
+
.server-item.active .server-item-indicator {
|
|
1776
|
+
opacity: 1;
|
|
1777
|
+
height: 32px;
|
|
1778
|
+
}
|
|
1213
1779
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1780
|
+
.server-avatar {
|
|
1781
|
+
position: absolute;
|
|
1782
|
+
left: 16px;
|
|
1783
|
+
top: 50%;
|
|
1784
|
+
transform: translateY(-50%);
|
|
1785
|
+
width: 26px;
|
|
1786
|
+
height: 26px;
|
|
1787
|
+
border-radius: 8px;
|
|
1788
|
+
object-fit: cover;
|
|
1789
|
+
}
|
|
1219
1790
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
desc.textContent = plugin.description;
|
|
1234
|
-
wrap.appendChild(desc);
|
|
1235
|
-
}
|
|
1791
|
+
.server-fallback {
|
|
1792
|
+
position: absolute;
|
|
1793
|
+
left: 16px;
|
|
1794
|
+
top: 50%;
|
|
1795
|
+
transform: translateY(-50%);
|
|
1796
|
+
width: 26px;
|
|
1797
|
+
height: 26px;
|
|
1798
|
+
border-radius: 8px;
|
|
1799
|
+
display: grid;
|
|
1800
|
+
place-items: center;
|
|
1801
|
+
background: rgba(148,163,184,.18);
|
|
1802
|
+
font-size: .75rem;
|
|
1803
|
+
}
|
|
1236
1804
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1805
|
+
.server-status {
|
|
1806
|
+
position: absolute;
|
|
1807
|
+
right: 12px;
|
|
1808
|
+
top: 50%;
|
|
1809
|
+
transform: translateY(-50%);
|
|
1810
|
+
width: 9px;
|
|
1811
|
+
height: 9px;
|
|
1812
|
+
border-radius: 999px;
|
|
1813
|
+
background: #22c55e;
|
|
1814
|
+
}
|
|
1239
1815
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1816
|
+
.server-status::after {
|
|
1817
|
+
position: absolute;
|
|
1818
|
+
right: 16px;
|
|
1819
|
+
top: 50%;
|
|
1820
|
+
transform: translateY(-50%);
|
|
1821
|
+
font-size: .7rem;
|
|
1822
|
+
font-weight: 500;
|
|
1823
|
+
letter-spacing: .15px;
|
|
1824
|
+
color: #86efac;
|
|
1825
|
+
white-space: nowrap;
|
|
1826
|
+
}
|
|
1244
1827
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
p.textContent = panel.description;
|
|
1249
|
-
panelBody.appendChild(p);
|
|
1250
|
-
}
|
|
1828
|
+
.server-status.offline {
|
|
1829
|
+
background: #94a3b8;
|
|
1830
|
+
}
|
|
1251
1831
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1832
|
+
.server-status.offline::after {
|
|
1833
|
+
content: "Invite Bot";
|
|
1834
|
+
color: #fda4af;
|
|
1835
|
+
}
|
|
1255
1836
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
if (!field.editable) {
|
|
1261
|
-
const display = field.value == null
|
|
1262
|
-
? ""
|
|
1263
|
-
: typeof field.value === "object"
|
|
1264
|
-
? JSON.stringify(field.value)
|
|
1265
|
-
: String(field.value);
|
|
1266
|
-
fieldWrap.innerHTML = '<strong>' + escapeHtml(field.label) + '</strong><span>' + escapeHtml(display) + '</span>';
|
|
1267
|
-
fieldsWrap.appendChild(fieldWrap);
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1837
|
+
.content {
|
|
1838
|
+
padding: 16px;
|
|
1839
|
+
}
|
|
1270
1840
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1841
|
+
.container {
|
|
1842
|
+
border: 1px solid var(--border);
|
|
1843
|
+
border-radius: var(--radius-lg);
|
|
1844
|
+
background: var(--surface);
|
|
1845
|
+
padding: 14px;
|
|
1846
|
+
}
|
|
1274
1847
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const optionEl = document.createElement("option");
|
|
1285
|
-
optionEl.value = option.value;
|
|
1286
|
-
optionEl.textContent = option.label;
|
|
1287
|
-
if (String(field.value ?? "") === option.value) {
|
|
1288
|
-
optionEl.selected = true;
|
|
1289
|
-
}
|
|
1290
|
-
input.appendChild(optionEl);
|
|
1291
|
-
});
|
|
1292
|
-
} else if (field.type === "boolean") {
|
|
1293
|
-
const row = document.createElement("div");
|
|
1294
|
-
row.className = "home-field-row";
|
|
1295
|
-
input = document.createElement("input");
|
|
1296
|
-
input.type = "checkbox";
|
|
1297
|
-
input.className = "home-checkbox cursor-pointer";
|
|
1298
|
-
input.checked = Boolean(field.value);
|
|
1299
|
-
const stateText = document.createElement("span");
|
|
1300
|
-
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
1301
|
-
input.addEventListener("change", () => {
|
|
1302
|
-
stateText.textContent = input.checked ? "Enabled" : "Disabled";
|
|
1303
|
-
});
|
|
1304
|
-
row.appendChild(input);
|
|
1305
|
-
row.appendChild(stateText);
|
|
1306
|
-
fieldWrap.appendChild(row);
|
|
1307
|
-
} else {
|
|
1308
|
-
input = document.createElement("input");
|
|
1309
|
-
input.className = "home-input";
|
|
1310
|
-
input.type = field.type === "number" ? "number" : field.type === "url" ? "url" : "text";
|
|
1311
|
-
input.value = field.value == null ? "" : String(field.value);
|
|
1312
|
-
}
|
|
1848
|
+
.main-tabs {
|
|
1849
|
+
display: inline-flex;
|
|
1850
|
+
gap: 8px;
|
|
1851
|
+
padding: 5px;
|
|
1852
|
+
border: 1px solid var(--border);
|
|
1853
|
+
border-radius: 999px;
|
|
1854
|
+
background: rgba(15, 23, 42, .4);
|
|
1855
|
+
margin-bottom: 14px;
|
|
1856
|
+
}
|
|
1313
1857
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
input.required = true;
|
|
1322
|
-
}
|
|
1858
|
+
button {
|
|
1859
|
+
border: 1px solid var(--border);
|
|
1860
|
+
background: var(--card-2);
|
|
1861
|
+
color: var(--text);
|
|
1862
|
+
border-radius: 999px;
|
|
1863
|
+
padding: 7px 13px;
|
|
1864
|
+
}
|
|
1323
1865
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
fieldWrap.appendChild(input);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1866
|
+
button.primary {
|
|
1867
|
+
border: none;
|
|
1868
|
+
color: #0b1020;
|
|
1869
|
+
font-weight: 700;
|
|
1870
|
+
background: linear-gradient(90deg, rgba(192,132,252,.95), rgba(34,211,238,.95));
|
|
1871
|
+
}
|
|
1333
1872
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1873
|
+
button.danger {
|
|
1874
|
+
background: rgba(190,24,93,.2);
|
|
1875
|
+
border-color: rgba(244,114,182,.45);
|
|
1876
|
+
}
|
|
1338
1877
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
});
|
|
1345
|
-
panelBody.appendChild(actions);
|
|
1346
|
-
}
|
|
1878
|
+
.main-tab.active,
|
|
1879
|
+
.home-category-btn.active {
|
|
1880
|
+
background: rgba(34,211,238,.2);
|
|
1881
|
+
border-color: rgba(34,211,238,.62);
|
|
1882
|
+
}
|
|
1347
1883
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1884
|
+
.section-title {
|
|
1885
|
+
margin: 10px 0 9px;
|
|
1886
|
+
font-size: .92rem;
|
|
1887
|
+
color: #e2e8f0;
|
|
1888
|
+
letter-spacing: .15px;
|
|
1889
|
+
}
|
|
1350
1890
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
};
|
|
1891
|
+
.grid { display: grid; gap: 10px; }
|
|
1892
|
+
.cards { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
|
1354
1893
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
}
|
|
1362
|
-
renderHomeCategories();
|
|
1363
|
-
|
|
1364
|
-
const homePath = state.selectedHomeCategoryId
|
|
1365
|
-
? "/api/home?categoryId=" + encodeURIComponent(state.selectedHomeCategoryId)
|
|
1366
|
-
: "/api/home";
|
|
1367
|
-
|
|
1368
|
-
const [home, overview, plugins] = await Promise.all([
|
|
1369
|
-
fetchJson(buildApiUrl(homePath)),
|
|
1370
|
-
fetchJson(buildApiUrl("/api/overview")),
|
|
1371
|
-
fetchJson(buildApiUrl("/api/plugins"))
|
|
1372
|
-
]);
|
|
1373
|
-
|
|
1374
|
-
renderHomeSections(home.sections || []);
|
|
1375
|
-
const showOverviewArea = state.selectedHomeCategoryId === "overview";
|
|
1376
|
-
el.overviewArea.style.display = showOverviewArea ? "block" : "none";
|
|
1377
|
-
renderCards(overview.cards || []);
|
|
1378
|
-
renderPlugins(plugins.plugins || []);
|
|
1379
|
-
};
|
|
1894
|
+
.panel {
|
|
1895
|
+
border: 1px solid var(--border);
|
|
1896
|
+
border-radius: var(--radius-md);
|
|
1897
|
+
background: var(--card);
|
|
1898
|
+
padding: 12px;
|
|
1899
|
+
}
|
|
1380
1900
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1901
|
+
.title { color: var(--muted); font-size: .82rem; }
|
|
1902
|
+
.value { margin-top: 6px; font-size: 1.34rem; font-weight: 700; }
|
|
1903
|
+
.subtitle { margin-top: 6px; color: var(--muted); font-size: .8rem; }
|
|
1383
1904
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1905
|
+
.home-categories,
|
|
1906
|
+
.home-sections {
|
|
1907
|
+
display: flex;
|
|
1908
|
+
gap: 10px;
|
|
1909
|
+
flex-wrap: wrap;
|
|
1910
|
+
margin-bottom: 10px;
|
|
1911
|
+
}
|
|
1389
1912
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1913
|
+
.home-section-panel { flex: 0 0 100%; max-width: 100%; }
|
|
1914
|
+
.home-width-50 { flex-basis: calc(50% - 5px); max-width: calc(50% - 5px); }
|
|
1915
|
+
.home-width-33 { flex-basis: calc(33.333333% - 6.67px); max-width: calc(33.333333% - 6.67px); }
|
|
1916
|
+
.home-width-20 { flex-basis: calc(20% - 8px); max-width: calc(20% - 8px); }
|
|
1917
|
+
|
|
1918
|
+
.home-fields,
|
|
1919
|
+
.plugin-fields { display: grid; gap: 9px; margin-top: 8px; }
|
|
1920
|
+
.home-field,
|
|
1921
|
+
.plugin-field { display: grid; gap: 5px; }
|
|
1922
|
+
.home-field label,
|
|
1923
|
+
.plugin-field > label { color: var(--muted); font-size: .8rem; }
|
|
1924
|
+
|
|
1925
|
+
.home-input,
|
|
1926
|
+
.home-textarea,
|
|
1927
|
+
.home-select {
|
|
1928
|
+
width: 100%;
|
|
1929
|
+
border: 1px solid var(--border);
|
|
1930
|
+
background: var(--card-2);
|
|
1931
|
+
color: var(--text);
|
|
1932
|
+
border-radius: 10px;
|
|
1933
|
+
padding: 8px 10px;
|
|
1934
|
+
}
|
|
1397
1935
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1936
|
+
.home-textarea { min-height: 90px; resize: vertical; }
|
|
1937
|
+
.home-checkbox { width: 17px; height: 17px; }
|
|
1938
|
+
.home-field-row { display: flex; align-items: center; gap: 8px; }
|
|
1939
|
+
.home-message { margin-top: 6px; color: var(--muted); font-size: .8rem; }
|
|
1940
|
+
|
|
1941
|
+
.lookup-wrap { position: relative; }
|
|
1942
|
+
.lookup-results {
|
|
1943
|
+
position: absolute;
|
|
1944
|
+
left: 0;
|
|
1945
|
+
right: 0;
|
|
1946
|
+
top: calc(100% + 6px);
|
|
1947
|
+
z-index: 20;
|
|
1948
|
+
border: 1px solid var(--border);
|
|
1949
|
+
background: rgba(15, 23, 42, .96);
|
|
1950
|
+
border-radius: 10px;
|
|
1951
|
+
max-height: 220px;
|
|
1952
|
+
overflow: auto;
|
|
1953
|
+
display: none;
|
|
1954
|
+
}
|
|
1402
1955
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1956
|
+
.lookup-item {
|
|
1957
|
+
width: 100%;
|
|
1958
|
+
border: none;
|
|
1959
|
+
border-radius: 0;
|
|
1960
|
+
text-align: left;
|
|
1961
|
+
padding: 8px 10px;
|
|
1962
|
+
background: transparent;
|
|
1963
|
+
}
|
|
1407
1964
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
}
|
|
1965
|
+
.lookup-item:hover { background: rgba(51,65,85,.56); }
|
|
1966
|
+
.lookup-selected { margin-top: 5px; font-size: .8rem; color: var(--muted); }
|
|
1967
|
+
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
|
1968
|
+
|
|
1969
|
+
.kv-item {
|
|
1970
|
+
display: flex;
|
|
1971
|
+
justify-content: space-between;
|
|
1972
|
+
border: 1px solid var(--border);
|
|
1973
|
+
border-radius: 10px;
|
|
1974
|
+
padding: 7px 10px;
|
|
1975
|
+
background: var(--card-2);
|
|
1976
|
+
}
|
|
1411
1977
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
}
|
|
1978
|
+
.list-editor {
|
|
1979
|
+
border: 1px solid var(--border);
|
|
1980
|
+
border-radius: 10px;
|
|
1981
|
+
background: var(--card-2);
|
|
1982
|
+
padding: 8px;
|
|
1983
|
+
display: grid;
|
|
1984
|
+
gap: 8px;
|
|
1985
|
+
}
|
|
1420
1986
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
}
|
|
1449
|
-
* { box-sizing: border-box; }
|
|
1450
|
-
body {
|
|
1451
|
-
margin: 0;
|
|
1452
|
-
font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
|
1453
|
-
background: radial-gradient(circle at 0% 0%, #1b2140 0%, var(--bg) 45%);
|
|
1454
|
-
color: var(--text);
|
|
1455
|
-
}
|
|
1456
|
-
.shell {
|
|
1457
|
-
min-height: 100vh;
|
|
1458
|
-
display: grid;
|
|
1459
|
-
grid-template-rows: auto 1fr;
|
|
1460
|
-
}
|
|
1461
|
-
.topbar {
|
|
1462
|
-
display: grid;
|
|
1463
|
-
grid-template-columns: auto 1fr auto;
|
|
1464
|
-
align-items: center;
|
|
1465
|
-
gap: 10px;
|
|
1466
|
-
padding: 10px 14px;
|
|
1467
|
-
border-bottom: 1px solid var(--border);
|
|
1468
|
-
background: rgba(15, 20, 38, 0.7);
|
|
1469
|
-
backdrop-filter: blur(8px);
|
|
1470
|
-
}
|
|
1471
|
-
.brand { font-weight: 800; letter-spacing: .2px; }
|
|
1472
|
-
.center-title { text-align: center; font-weight: 700; color: #d8defc; }
|
|
1473
|
-
.pill {
|
|
1474
|
-
justify-self: end;
|
|
1475
|
-
padding: 4px 8px;
|
|
1476
|
-
border-radius: 999px;
|
|
1477
|
-
border: 1px solid var(--border);
|
|
1478
|
-
color: var(--muted);
|
|
1479
|
-
font-size: .75rem;
|
|
1480
|
-
}
|
|
1481
|
-
.layout {
|
|
1482
|
-
display: grid;
|
|
1483
|
-
grid-template-columns: 80px 1fr;
|
|
1484
|
-
min-height: 0;
|
|
1485
|
-
}
|
|
1486
|
-
.sidebar {
|
|
1487
|
-
border-right: 1px solid var(--border);
|
|
1488
|
-
background: linear-gradient(180deg, var(--rail), #121528);
|
|
1489
|
-
padding: 10px 0;
|
|
1490
|
-
}
|
|
1491
|
-
.server-rail {
|
|
1492
|
-
display: flex;
|
|
1493
|
-
flex-direction: column;
|
|
1494
|
-
align-items: center;
|
|
1495
|
-
gap: 10px;
|
|
1496
|
-
}
|
|
1497
|
-
.server-item {
|
|
1498
|
-
position: relative;
|
|
1499
|
-
width: 46px;
|
|
1500
|
-
height: 46px;
|
|
1501
|
-
border: none;
|
|
1502
|
-
border-radius: 14px;
|
|
1503
|
-
overflow: visible;
|
|
1504
|
-
background: var(--panel);
|
|
1505
|
-
color: #fff;
|
|
1506
|
-
font-weight: 700;
|
|
1507
|
-
transition: transform .15s ease, background .15s ease;
|
|
1508
|
-
}
|
|
1509
|
-
.server-item:hover { transform: translateY(-1px); background: #323b5f; }
|
|
1510
|
-
.server-item.active { background: var(--primary); transform: translateY(-2px); }
|
|
1511
|
-
.server-item-indicator {
|
|
1512
|
-
position: absolute;
|
|
1513
|
-
left: -8px;
|
|
1514
|
-
top: 50%;
|
|
1515
|
-
transform: translateY(-50%) scaleY(.5);
|
|
1516
|
-
width: 3px;
|
|
1517
|
-
height: 18px;
|
|
1518
|
-
background: #fff;
|
|
1519
|
-
border-radius: 999px;
|
|
1520
|
-
opacity: 0;
|
|
1521
|
-
transition: opacity .15s ease, transform .15s ease;
|
|
1522
|
-
}
|
|
1523
|
-
.server-item.active .server-item-indicator {
|
|
1524
|
-
opacity: 1;
|
|
1525
|
-
transform: translateY(-50%) scaleY(1);
|
|
1526
|
-
}
|
|
1527
|
-
.server-avatar { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
|
|
1528
|
-
.server-fallback { display: grid; place-items: center; width: 100%; height: 100%; }
|
|
1529
|
-
.server-status {
|
|
1530
|
-
position: absolute;
|
|
1531
|
-
right: -3px;
|
|
1532
|
-
bottom: -3px;
|
|
1533
|
-
width: 11px;
|
|
1534
|
-
height: 11px;
|
|
1535
|
-
border-radius: 999px;
|
|
1536
|
-
border: 2px solid var(--rail);
|
|
1537
|
-
background: #35d489;
|
|
1538
|
-
}
|
|
1539
|
-
.server-status.offline { background: #7f8bb3; }
|
|
1540
|
-
.content {
|
|
1541
|
-
min-width: 0;
|
|
1542
|
-
padding: 12px;
|
|
1543
|
-
}
|
|
1544
|
-
.container {
|
|
1545
|
-
background: rgba(23, 28, 48, 0.6);
|
|
1546
|
-
border: 1px solid var(--border);
|
|
1547
|
-
border-radius: 12px;
|
|
1548
|
-
padding: 12px;
|
|
1549
|
-
}
|
|
1550
|
-
.main-tabs { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
1551
|
-
button {
|
|
1552
|
-
border: 1px solid var(--border);
|
|
1553
|
-
background: var(--panel-2);
|
|
1554
|
-
color: var(--text);
|
|
1555
|
-
border-radius: 8px;
|
|
1556
|
-
padding: 7px 10px;
|
|
1557
|
-
}
|
|
1558
|
-
button.primary { background: var(--primary); border: none; }
|
|
1559
|
-
button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
|
|
1560
|
-
.main-tab.active, .home-category-btn.active { background: var(--primary); border-color: transparent; }
|
|
1561
|
-
.section-title { margin: 12px 0 8px; color: #dce3ff; font-size: .95rem; }
|
|
1562
|
-
.grid { display: grid; gap: 10px; }
|
|
1563
|
-
.cards { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
|
|
1564
|
-
.panel {
|
|
1565
|
-
background: linear-gradient(180deg, rgba(42,49,78,.7), rgba(31,36,59,.85));
|
|
1566
|
-
border: 1px solid var(--border);
|
|
1567
|
-
border-radius: 10px;
|
|
1568
|
-
padding: 12px;
|
|
1569
|
-
}
|
|
1570
|
-
.title { color: var(--muted); font-size: .83rem; }
|
|
1571
|
-
.value { font-size: 1.25rem; font-weight: 800; margin-top: 5px; }
|
|
1572
|
-
.subtitle { margin-top: 5px; color: var(--muted); font-size: .8rem; }
|
|
1573
|
-
.home-categories { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
1574
|
-
.home-sections { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
|
|
1575
|
-
.home-section-panel { flex: 0 0 100%; max-width: 100%; }
|
|
1576
|
-
.home-width-50 { flex-basis: calc(50% - 5px); max-width: calc(50% - 5px); }
|
|
1577
|
-
.home-width-33 { flex-basis: calc(33.333333% - 6.67px); max-width: calc(33.333333% - 6.67px); }
|
|
1578
|
-
.home-width-20 { flex-basis: calc(20% - 8px); max-width: calc(20% - 8px); }
|
|
1579
|
-
.home-fields, .plugin-fields { display: grid; gap: 8px; margin-top: 8px; }
|
|
1580
|
-
.home-field, .plugin-field { display: grid; gap: 5px; }
|
|
1581
|
-
.home-field label, .plugin-field > label { color: var(--muted); font-size: .8rem; }
|
|
1582
|
-
.home-input, .home-textarea, .home-select {
|
|
1583
|
-
width: 100%;
|
|
1584
|
-
border: 1px solid var(--border);
|
|
1585
|
-
background: var(--panel-2);
|
|
1586
|
-
color: var(--text);
|
|
1587
|
-
border-radius: 8px;
|
|
1588
|
-
padding: 7px 9px;
|
|
1589
|
-
}
|
|
1590
|
-
.home-textarea { min-height: 88px; resize: vertical; }
|
|
1591
|
-
.home-checkbox { width: 17px; height: 17px; }
|
|
1592
|
-
.home-field-row { display: flex; align-items: center; gap: 8px; }
|
|
1593
|
-
.home-message { margin-top: 6px; color: var(--muted); font-size: .8rem; }
|
|
1594
|
-
.lookup-wrap { position: relative; }
|
|
1595
|
-
.lookup-results {
|
|
1596
|
-
position: absolute;
|
|
1597
|
-
left: 0;
|
|
1598
|
-
right: 0;
|
|
1599
|
-
top: calc(100% + 5px);
|
|
1600
|
-
z-index: 20;
|
|
1601
|
-
border: 1px solid var(--border);
|
|
1602
|
-
background: #1f2742;
|
|
1603
|
-
border-radius: 8px;
|
|
1604
|
-
max-height: 220px;
|
|
1605
|
-
overflow: auto;
|
|
1606
|
-
display: none;
|
|
1607
|
-
}
|
|
1608
|
-
.lookup-item {
|
|
1609
|
-
width: 100%;
|
|
1610
|
-
border: none;
|
|
1611
|
-
border-radius: 0;
|
|
1612
|
-
text-align: left;
|
|
1613
|
-
padding: 8px 10px;
|
|
1614
|
-
background: transparent;
|
|
1615
|
-
}
|
|
1616
|
-
.lookup-item:hover { background: #2d3658; }
|
|
1617
|
-
.lookup-selected { margin-top: 5px; font-size: .8rem; color: var(--muted); }
|
|
1618
|
-
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
|
1619
|
-
.kv-item {
|
|
1620
|
-
display: flex;
|
|
1621
|
-
justify-content: space-between;
|
|
1622
|
-
border: 1px solid var(--border);
|
|
1623
|
-
border-radius: 8px;
|
|
1624
|
-
padding: 7px 9px;
|
|
1625
|
-
background: var(--panel-2);
|
|
1626
|
-
}
|
|
1627
|
-
.list-editor {
|
|
1628
|
-
border: 1px solid var(--border);
|
|
1629
|
-
border-radius: 8px;
|
|
1630
|
-
background: var(--panel-2);
|
|
1631
|
-
padding: 8px;
|
|
1632
|
-
display: grid;
|
|
1633
|
-
gap: 8px;
|
|
1634
|
-
}
|
|
1635
|
-
.list-items { display: grid; gap: 6px; }
|
|
1636
|
-
.list-item {
|
|
1637
|
-
display: grid;
|
|
1638
|
-
grid-template-columns: auto 1fr auto;
|
|
1639
|
-
gap: 8px;
|
|
1640
|
-
align-items: center;
|
|
1641
|
-
border: 1px solid var(--border);
|
|
1642
|
-
border-radius: 8px;
|
|
1643
|
-
padding: 6px 8px;
|
|
1644
|
-
background: var(--panel);
|
|
1645
|
-
}
|
|
1646
|
-
.list-item.dragging { opacity: .6; }
|
|
1647
|
-
.drag-handle { color: var(--muted); user-select: none; font-size: .9rem; }
|
|
1648
|
-
.list-input { width: 100%; border: none; outline: none; background: transparent; color: var(--text); }
|
|
1649
|
-
.list-add { justify-self: start; }
|
|
1650
|
-
.empty { color: var(--muted); font-size: .9rem; }
|
|
1651
|
-
.cursor-pointer { cursor: pointer; }
|
|
1652
|
-
@media (max-width: 980px) {
|
|
1653
|
-
.layout { grid-template-columns: 70px 1fr; }
|
|
1654
|
-
.home-width-50, .home-width-33, .home-width-20 { flex-basis: 100%; max-width: 100%; }
|
|
1655
|
-
}
|
|
1656
|
-
`;
|
|
1657
|
-
var compactDashboardTemplateRenderer = ({ dashboardName, basePath, setupDesign }) => {
|
|
1658
|
-
const script = extractDashboardScript(renderDashboardHtml(dashboardName, basePath, setupDesign));
|
|
1659
|
-
const safeName = escapeHtml2(dashboardName);
|
|
1660
|
-
const customCssBlock = setupDesign?.customCss ? `
|
|
1661
|
-
<style>${setupDesign.customCss}</style>` : "";
|
|
1662
|
-
return `<!DOCTYPE html>
|
|
1663
|
-
<html lang="en">
|
|
1664
|
-
<head>
|
|
1665
|
-
<meta charset="UTF-8" />
|
|
1666
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1667
|
-
<title>${safeName}</title>
|
|
1668
|
-
<style>${compactCss}</style>${customCssBlock}
|
|
1669
|
-
</head>
|
|
1670
|
-
<body>
|
|
1671
|
-
<div class="shell">
|
|
1672
|
-
<header class="topbar">
|
|
1673
|
-
<div class="brand">${safeName}</div>
|
|
1674
|
-
<div id="centerTitle" class="center-title">User Dashboard</div>
|
|
1675
|
-
<div id="userMeta" class="pill">Loading...</div>
|
|
1676
|
-
</header>
|
|
1987
|
+
.list-items { display: grid; gap: 6px; }
|
|
1988
|
+
|
|
1989
|
+
.list-item {
|
|
1990
|
+
display: grid;
|
|
1991
|
+
grid-template-columns: auto 1fr auto;
|
|
1992
|
+
gap: 8px;
|
|
1993
|
+
align-items: center;
|
|
1994
|
+
border: 1px solid var(--border);
|
|
1995
|
+
border-radius: 8px;
|
|
1996
|
+
padding: 6px 8px;
|
|
1997
|
+
background: rgba(23, 29, 42, .78);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
.list-item.dragging { opacity: .6; }
|
|
2001
|
+
.drag-handle { color: var(--muted); user-select: none; font-size: .9rem; }
|
|
2002
|
+
.list-input { width: 100%; border: none; outline: none; background: transparent; color: var(--text); }
|
|
2003
|
+
.list-add { justify-self: start; }
|
|
2004
|
+
.empty { color: var(--muted); font-size: .9rem; }
|
|
2005
|
+
.cursor-pointer { cursor: pointer; }
|
|
2006
|
+
|
|
2007
|
+
@media (max-width: 980px) {
|
|
2008
|
+
.topbar { grid-template-columns: 1fr auto; }
|
|
2009
|
+
.center-title { display: none; }
|
|
2010
|
+
.server-item { min-width: 180px; }
|
|
2011
|
+
.home-width-50,
|
|
2012
|
+
.home-width-33,
|
|
2013
|
+
.home-width-20 { flex-basis: 100%; max-width: 100%; }
|
|
2014
|
+
}
|
|
1677
2015
|
|
|
1678
|
-
|
|
1679
|
-
|
|
2016
|
+
/* Inject User Custom CSS Here */
|
|
2017
|
+
${design.customCss ?? ""}
|
|
2018
|
+
</style>
|
|
2019
|
+
</head>
|
|
2020
|
+
<body>
|
|
2021
|
+
<div class="page">
|
|
2022
|
+
<div class="shell">
|
|
2023
|
+
<header class="topbar">
|
|
2024
|
+
<div class="brand">${safeName}</div>
|
|
2025
|
+
<div id="centerTitle" class="center-title">Workspace Hub</div>
|
|
2026
|
+
<div id="userMeta" class="pill">Loading...</div>
|
|
2027
|
+
</header>
|
|
2028
|
+
|
|
2029
|
+
<section class="server-strip">
|
|
1680
2030
|
<div id="serverRail" class="server-rail"></div>
|
|
1681
|
-
</
|
|
2031
|
+
</section>
|
|
1682
2032
|
|
|
1683
2033
|
<main class="content">
|
|
1684
|
-
<
|
|
2034
|
+
<section class="container">
|
|
1685
2035
|
<div class="main-tabs">
|
|
1686
2036
|
<button id="tabHome" class="main-tab active cursor-pointer">Home</button>
|
|
1687
2037
|
<button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
|
|
1688
2038
|
</div>
|
|
1689
2039
|
|
|
1690
2040
|
<section id="homeArea">
|
|
1691
|
-
<div class="section-title">
|
|
2041
|
+
<div class="section-title">Dashboard Workspace</div>
|
|
1692
2042
|
<section id="homeCategories" class="home-categories"></section>
|
|
1693
2043
|
<section id="homeSections" class="home-sections"></section>
|
|
1694
2044
|
|
|
1695
2045
|
<section id="overviewArea">
|
|
1696
|
-
<div class="section-title">
|
|
2046
|
+
<div class="section-title">Overview Metrics</div>
|
|
1697
2047
|
<section id="overviewCards" class="grid cards"></section>
|
|
1698
2048
|
</section>
|
|
1699
2049
|
</section>
|
|
1700
2050
|
|
|
1701
2051
|
<section id="pluginsArea" style="display:none;">
|
|
1702
|
-
<div class="section-title">
|
|
2052
|
+
<div class="section-title">Plugin Center</div>
|
|
1703
2053
|
<section id="plugins" class="grid"></section>
|
|
1704
2054
|
</section>
|
|
1705
|
-
</
|
|
2055
|
+
</section>
|
|
1706
2056
|
</main>
|
|
1707
2057
|
</div>
|
|
1708
2058
|
</div>
|
|
1709
2059
|
|
|
1710
|
-
<script>${
|
|
2060
|
+
<script>${clientScript}</script>
|
|
1711
2061
|
</body>
|
|
1712
2062
|
</html>`;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/templates/index.ts
|
|
2066
|
+
var BuiltinLayouts = {
|
|
2067
|
+
default: renderDefaultLayout,
|
|
2068
|
+
compact: renderCompactLayout,
|
|
2069
|
+
"shadcn-magic": renderShadcnMagicLayout
|
|
1713
2070
|
};
|
|
1714
2071
|
|
|
1715
|
-
// src/templates/
|
|
1716
|
-
var
|
|
2072
|
+
// src/templates/themes/index.ts
|
|
2073
|
+
var BuiltinThemes = {
|
|
2074
|
+
default: {
|
|
2075
|
+
bg: "#08090f",
|
|
2076
|
+
rail: "#0e101a",
|
|
2077
|
+
contentBg: "radial-gradient(circle at top right, #171a2f, #08090f)",
|
|
2078
|
+
panel: "rgba(20, 23, 43, 0.6)",
|
|
2079
|
+
panel2: "rgba(0, 0, 0, 0.3)",
|
|
2080
|
+
text: "#e0e6ff",
|
|
2081
|
+
muted: "#8a93bc",
|
|
2082
|
+
primary: "#5865F2",
|
|
2083
|
+
success: "#00E676",
|
|
2084
|
+
warning: "#FFD600",
|
|
2085
|
+
danger: "#FF3D00",
|
|
2086
|
+
border: "rgba(88, 101, 242, 0.2)"
|
|
2087
|
+
},
|
|
2088
|
+
compact: {
|
|
2089
|
+
bg: "#0f1221",
|
|
2090
|
+
rail: "#171a2d",
|
|
2091
|
+
contentBg: "#0f1426",
|
|
2092
|
+
panel: "#1f243b",
|
|
2093
|
+
panel2: "#2a314e",
|
|
2094
|
+
text: "#f5f7ff",
|
|
2095
|
+
muted: "#aab1d6",
|
|
2096
|
+
primary: "#7c87ff",
|
|
2097
|
+
success: "#2bd4a6",
|
|
2098
|
+
warning: "#ffd166",
|
|
2099
|
+
danger: "#ff6f91",
|
|
2100
|
+
info: "#66d9ff",
|
|
2101
|
+
border: "rgba(255, 255, 255, 0.12)"
|
|
2102
|
+
},
|
|
2103
|
+
"shadcn-magic": {
|
|
2104
|
+
bg: "#06070b",
|
|
2105
|
+
surface: "rgba(10, 12, 18, 0.84)",
|
|
2106
|
+
card: "rgba(16, 20, 30, 0.82)",
|
|
2107
|
+
card2: "rgba(23, 29, 42, 0.86)",
|
|
2108
|
+
text: "#f8fafc",
|
|
2109
|
+
muted: "#94a3b8",
|
|
2110
|
+
primary: "#c084fc",
|
|
2111
|
+
accent: "#22d3ee",
|
|
2112
|
+
border: "rgba(148, 163, 184, 0.26)",
|
|
2113
|
+
radiusLg: "18px",
|
|
2114
|
+
radiusMd: "12px"
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
1717
2117
|
|
|
1718
|
-
// src/
|
|
1719
|
-
var
|
|
1720
|
-
|
|
1721
|
-
|
|
2118
|
+
// src/handlers/TemplateManager.ts
|
|
2119
|
+
var TemplateManager = class {
|
|
2120
|
+
layoutRenderer;
|
|
2121
|
+
resolvedDesign;
|
|
2122
|
+
constructor(options) {
|
|
2123
|
+
this.layoutRenderer = this.resolveLayout(options.uiTemplate);
|
|
2124
|
+
this.resolvedDesign = this.resolveTheme(options.uiTheme, options.setupDesign);
|
|
2125
|
+
}
|
|
2126
|
+
resolveLayout(layoutInput) {
|
|
2127
|
+
if (typeof layoutInput === "function") {
|
|
2128
|
+
return layoutInput;
|
|
2129
|
+
}
|
|
2130
|
+
if (typeof layoutInput === "string" && BuiltinLayouts[layoutInput]) {
|
|
2131
|
+
return BuiltinLayouts[layoutInput];
|
|
2132
|
+
}
|
|
2133
|
+
return BuiltinLayouts["default"];
|
|
2134
|
+
}
|
|
2135
|
+
resolveTheme(themeInput, customOverrides) {
|
|
2136
|
+
let baseTheme = {};
|
|
2137
|
+
if (typeof themeInput === "string" && BuiltinThemes[themeInput]) {
|
|
2138
|
+
baseTheme = BuiltinThemes[themeInput];
|
|
2139
|
+
} else if (typeof themeInput === "object") {
|
|
2140
|
+
baseTheme = themeInput;
|
|
2141
|
+
}
|
|
2142
|
+
const merged = {
|
|
2143
|
+
...baseTheme,
|
|
2144
|
+
...customOverrides
|
|
2145
|
+
};
|
|
2146
|
+
const combinedCss = [baseTheme.customCss, customOverrides?.customCss].filter((css) => css && css.trim().length > 0).join("\n\n");
|
|
2147
|
+
if (combinedCss) {
|
|
2148
|
+
merged.customCss = combinedCss;
|
|
2149
|
+
}
|
|
2150
|
+
return merged;
|
|
2151
|
+
}
|
|
2152
|
+
render(contextBase) {
|
|
2153
|
+
const finalContext = {
|
|
2154
|
+
...contextBase,
|
|
2155
|
+
setupDesign: this.resolvedDesign
|
|
2156
|
+
};
|
|
2157
|
+
return this.layoutRenderer(finalContext);
|
|
2158
|
+
}
|
|
1722
2159
|
};
|
|
1723
|
-
function getBuiltinTemplateRenderer(templateId) {
|
|
1724
|
-
return builtinTemplateRenderers[templateId];
|
|
1725
|
-
}
|
|
1726
2160
|
|
|
1727
|
-
// src/
|
|
1728
|
-
var
|
|
1729
|
-
constructor(
|
|
1730
|
-
this.
|
|
1731
|
-
this.
|
|
1732
|
-
this.
|
|
2161
|
+
// src/core/DashboardEngine.ts
|
|
2162
|
+
var DashboardEngine = class {
|
|
2163
|
+
constructor(options) {
|
|
2164
|
+
this.options = options;
|
|
2165
|
+
this.helpers = new DiscordHelpers(options.botToken);
|
|
2166
|
+
this.templateManager = new TemplateManager(options);
|
|
1733
2167
|
}
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
scope
|
|
1744
|
-
|
|
2168
|
+
helpers;
|
|
2169
|
+
templateManager;
|
|
2170
|
+
DISCORD_API = "https://discord.com/api/v10";
|
|
2171
|
+
getAuthUrl(state) {
|
|
2172
|
+
const scope = this.options.scopes && this.options.scopes.length > 0 ? this.options.scopes.join(" ") : ["identify", "guilds"].join(" ");
|
|
2173
|
+
const query = new URLSearchParams({
|
|
2174
|
+
client_id: this.options.clientId,
|
|
2175
|
+
redirect_uri: this.options.redirectUri,
|
|
2176
|
+
response_type: "code",
|
|
2177
|
+
scope,
|
|
2178
|
+
state,
|
|
2179
|
+
prompt: "none"
|
|
1745
2180
|
});
|
|
1746
|
-
return
|
|
2181
|
+
return `https://discord.com/oauth2/authorize?${query.toString()}`;
|
|
2182
|
+
}
|
|
2183
|
+
async exchangeCode(code) {
|
|
2184
|
+
const response = await fetch(`${this.DISCORD_API}/oauth2/token`, {
|
|
2185
|
+
method: "POST",
|
|
2186
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2187
|
+
body: new URLSearchParams({
|
|
2188
|
+
client_id: this.options.clientId,
|
|
2189
|
+
client_secret: this.options.clientSecret,
|
|
2190
|
+
grant_type: "authorization_code",
|
|
2191
|
+
code,
|
|
2192
|
+
redirect_uri: this.options.redirectUri
|
|
2193
|
+
})
|
|
2194
|
+
});
|
|
2195
|
+
if (!response.ok) throw new Error(`Failed token exchange: ${response.status} ${await response.text()}`);
|
|
2196
|
+
return await response.json();
|
|
2197
|
+
}
|
|
2198
|
+
async fetchUser(token) {
|
|
2199
|
+
const res = await fetch(`${this.DISCORD_API}/users/@me`, {
|
|
2200
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
2201
|
+
});
|
|
2202
|
+
if (!res.ok) throw new Error(`Failed to fetch user: ${res.status} ${await res.text()}`);
|
|
2203
|
+
return await res.json();
|
|
1747
2204
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
2205
|
+
async fetchGuilds(token) {
|
|
2206
|
+
const res = await fetch(`${this.DISCORD_API}/users/@me/guilds`, {
|
|
2207
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
2208
|
+
});
|
|
2209
|
+
if (!res.ok) throw new Error(`Failed to fetch guilds: ${res.status} ${await res.text()}`);
|
|
2210
|
+
return await res.json();
|
|
1750
2211
|
}
|
|
1751
|
-
|
|
1752
|
-
return
|
|
2212
|
+
render(basePath) {
|
|
2213
|
+
return this.templateManager.render({
|
|
2214
|
+
dashboardName: this.options.dashboardName ?? "Discord Dashboard",
|
|
2215
|
+
basePath
|
|
2216
|
+
});
|
|
1753
2217
|
}
|
|
1754
2218
|
};
|
|
2219
|
+
|
|
2220
|
+
// src/handlers/DashboardDesigner.ts
|
|
1755
2221
|
var DashboardDesigner = class {
|
|
1756
|
-
|
|
2222
|
+
config = {
|
|
2223
|
+
setupDesign: {},
|
|
2224
|
+
uiTemplates: {}
|
|
2225
|
+
};
|
|
1757
2226
|
categories = [];
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
setup(input) {
|
|
1766
|
-
Object.assign(this.partialOptions, input);
|
|
1767
|
-
return this;
|
|
1768
|
-
}
|
|
1769
|
-
useTemplate(templateId) {
|
|
1770
|
-
this.partialOptions.uiTemplate = templateId;
|
|
1771
|
-
return this;
|
|
1772
|
-
}
|
|
1773
|
-
addTemplate(templateId, renderer) {
|
|
1774
|
-
this.partialOptions.uiTemplates = {
|
|
1775
|
-
...this.partialOptions.uiTemplates ?? {},
|
|
1776
|
-
[templateId]: renderer
|
|
1777
|
-
};
|
|
1778
|
-
return this;
|
|
2227
|
+
sections = [];
|
|
2228
|
+
plugins = [];
|
|
2229
|
+
overviewCards = [];
|
|
2230
|
+
constructor(dashboardName) {
|
|
2231
|
+
if (dashboardName) {
|
|
2232
|
+
this.config.dashboardName = dashboardName;
|
|
2233
|
+
}
|
|
1779
2234
|
}
|
|
1780
|
-
|
|
1781
|
-
this.
|
|
1782
|
-
...this.partialOptions.setupDesign ?? {},
|
|
1783
|
-
...input
|
|
1784
|
-
};
|
|
2235
|
+
setLayout(template) {
|
|
2236
|
+
this.config.uiTemplate = template;
|
|
1785
2237
|
return this;
|
|
1786
2238
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
build(builder);
|
|
1790
|
-
this.categories.push(builder);
|
|
2239
|
+
setTheme(theme) {
|
|
2240
|
+
this.config.uiTheme = theme;
|
|
1791
2241
|
return this;
|
|
1792
2242
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
build(builder);
|
|
1796
|
-
this.categories.push(builder);
|
|
2243
|
+
setColors(colors) {
|
|
2244
|
+
this.config.setupDesign = { ...this.config.setupDesign, ...colors };
|
|
1797
2245
|
return this;
|
|
1798
2246
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
build(builder);
|
|
1802
|
-
this.categories.push(builder);
|
|
2247
|
+
setCustomCss(css) {
|
|
2248
|
+
this.config.setupDesign.customCss = css;
|
|
1803
2249
|
return this;
|
|
1804
2250
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
const categoryId = input.categoryId ?? input.id;
|
|
1808
|
-
this.pages.push({
|
|
1809
|
-
pageId: input.id,
|
|
1810
|
-
category: { id: categoryId, label: input.label ?? input.title, scope },
|
|
1811
|
-
section: {
|
|
1812
|
-
id: input.id,
|
|
1813
|
-
title: input.title,
|
|
1814
|
-
description: input.description,
|
|
1815
|
-
width: input.width,
|
|
1816
|
-
scope,
|
|
1817
|
-
categoryId,
|
|
1818
|
-
fields: input.fields ?? [],
|
|
1819
|
-
actions: input.actions ?? []
|
|
1820
|
-
}
|
|
1821
|
-
});
|
|
2251
|
+
registerCustomLayout(name, renderer) {
|
|
2252
|
+
this.config.uiTemplates[name] = renderer;
|
|
1822
2253
|
return this;
|
|
1823
2254
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
2255
|
+
// --- Content Building Methods ---
|
|
2256
|
+
addOverviewCard(title, value, subtitle) {
|
|
2257
|
+
this.overviewCards.push({ title, value, subtitle });
|
|
1826
2258
|
return this;
|
|
1827
2259
|
}
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
}
|
|
1831
|
-
onSave(pageId, handler) {
|
|
1832
|
-
this.saveHandlers[pageId] = handler;
|
|
2260
|
+
addCategory(category) {
|
|
2261
|
+
this.categories.push(category.data);
|
|
1833
2262
|
return this;
|
|
1834
2263
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
}
|
|
1838
|
-
onHomeAction(actionId, handler) {
|
|
1839
|
-
this.homeActions[actionId] = handler;
|
|
2264
|
+
addSection(section) {
|
|
2265
|
+
this.sections.push(section.data);
|
|
1840
2266
|
return this;
|
|
1841
2267
|
}
|
|
1842
|
-
|
|
1843
|
-
this.
|
|
1844
|
-
...this.partialOptions.setupDesign ?? {},
|
|
1845
|
-
customCss: cssString
|
|
1846
|
-
};
|
|
2268
|
+
addPlugin(plugin) {
|
|
2269
|
+
this.plugins.push(plugin.data);
|
|
1847
2270
|
return this;
|
|
1848
2271
|
}
|
|
1849
2272
|
build() {
|
|
1850
|
-
const staticCategories = this.categories.map((item) => item.buildCategory());
|
|
1851
|
-
const staticSections = this.categories.flatMap((item) => item.buildSections());
|
|
1852
|
-
const pageCategories = this.pages.map((item) => item.category);
|
|
1853
|
-
const baseSections = [...staticSections, ...this.pages.map((item) => item.section)];
|
|
1854
|
-
const categoryMap = /* @__PURE__ */ new Map();
|
|
1855
|
-
for (const category of [...staticCategories, ...pageCategories]) {
|
|
1856
|
-
const key = `${category.scope}:${category.id}`;
|
|
1857
|
-
if (!categoryMap.has(key)) categoryMap.set(key, category);
|
|
1858
|
-
}
|
|
1859
|
-
const categories = [...categoryMap.values()];
|
|
1860
|
-
const saveActionIds = {};
|
|
1861
|
-
for (const section of baseSections) {
|
|
1862
|
-
if (this.saveHandlers[section.id]) saveActionIds[section.id] = `save:${section.id}`;
|
|
1863
|
-
}
|
|
1864
|
-
const resolvedActions = { ...this.homeActions };
|
|
1865
|
-
for (const [sectionId, handler] of Object.entries(this.saveHandlers)) {
|
|
1866
|
-
resolvedActions[saveActionIds[sectionId]] = handler;
|
|
1867
|
-
}
|
|
1868
|
-
const home = {
|
|
1869
|
-
getCategories: () => categories,
|
|
1870
|
-
getSections: async (context) => {
|
|
1871
|
-
const sections = [];
|
|
1872
|
-
for (const sourceSection of baseSections) {
|
|
1873
|
-
let section = {
|
|
1874
|
-
...sourceSection,
|
|
1875
|
-
fields: sourceSection.fields ? [...sourceSection.fields] : [],
|
|
1876
|
-
actions: sourceSection.actions ? [...sourceSection.actions] : []
|
|
1877
|
-
};
|
|
1878
|
-
const saveActionId = saveActionIds[section.id];
|
|
1879
|
-
if (saveActionId && !section.actions?.some((action) => action.id === saveActionId)) {
|
|
1880
|
-
section.actions = [...section.actions ?? [], { id: saveActionId, label: "Save", variant: "primary" }];
|
|
1881
|
-
}
|
|
1882
|
-
const loadHandler = this.loadHandlers[section.id];
|
|
1883
|
-
if (loadHandler) {
|
|
1884
|
-
const loaded = await loadHandler(context, section);
|
|
1885
|
-
if (loaded) {
|
|
1886
|
-
section = {
|
|
1887
|
-
...section,
|
|
1888
|
-
...loaded,
|
|
1889
|
-
fields: loaded.fields ?? section.fields,
|
|
1890
|
-
actions: loaded.actions ?? section.actions
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
sections.push(section);
|
|
1895
|
-
}
|
|
1896
|
-
return sections;
|
|
1897
|
-
},
|
|
1898
|
-
actions: resolvedActions
|
|
1899
|
-
};
|
|
1900
2273
|
return {
|
|
1901
|
-
...this.
|
|
1902
|
-
|
|
2274
|
+
...this.config,
|
|
2275
|
+
dashboardData: {
|
|
2276
|
+
categories: this.categories,
|
|
2277
|
+
sections: this.sections,
|
|
2278
|
+
plugins: this.plugins,
|
|
2279
|
+
overviewCards: this.overviewCards
|
|
2280
|
+
}
|
|
1903
2281
|
};
|
|
1904
2282
|
}
|
|
1905
|
-
/**
|
|
1906
|
-
* Builds the configuration and immediately instantiates the Dashboard.
|
|
1907
|
-
*/
|
|
1908
|
-
createDashboard() {
|
|
1909
|
-
const options = this.build();
|
|
1910
|
-
return new DiscordDashboard(options);
|
|
1911
|
-
}
|
|
1912
2283
|
};
|
|
1913
2284
|
|
|
1914
|
-
// src/
|
|
1915
|
-
var
|
|
1916
|
-
var
|
|
1917
|
-
var
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2285
|
+
// src/adapters/express.ts
|
|
2286
|
+
var import_compression = __toESM(require("compression"), 1);
|
|
2287
|
+
var import_express = __toESM(require("express"), 1);
|
|
2288
|
+
var import_express_session = __toESM(require("express-session"), 1);
|
|
2289
|
+
var import_helmet = __toESM(require("helmet"), 1);
|
|
2290
|
+
var import_node_crypto = require("crypto");
|
|
2291
|
+
function createExpressAdapter(options) {
|
|
2292
|
+
const engine = new DashboardEngine(options);
|
|
2293
|
+
const app = options.app ?? (0, import_express.default)();
|
|
2294
|
+
const router = (0, import_express.Router)();
|
|
2295
|
+
const basePath = options.basePath ?? "/dashboard";
|
|
2296
|
+
router.use((0, import_compression.default)());
|
|
2297
|
+
router.use((0, import_helmet.default)({ contentSecurityPolicy: false }));
|
|
2298
|
+
router.use(import_express.default.json());
|
|
2299
|
+
router.use(
|
|
2300
|
+
(0, import_express_session.default)({
|
|
2301
|
+
secret: options.sessionSecret,
|
|
2302
|
+
resave: false,
|
|
2303
|
+
saveUninitialized: false,
|
|
2304
|
+
name: options.sessionName ?? "discord_dashboard.sid"
|
|
2305
|
+
})
|
|
2306
|
+
);
|
|
2307
|
+
const buildContext = (req) => ({
|
|
2308
|
+
user: req.session.discordAuth.user,
|
|
2309
|
+
guilds: req.session.discordAuth.guilds || [],
|
|
2310
|
+
accessToken: req.session.discordAuth.accessToken || "",
|
|
2311
|
+
selectedGuildId: req.query.guildId || void 0,
|
|
2312
|
+
helpers: engine.helpers
|
|
1936
2313
|
});
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
const ext = user.avatar.startsWith("a_") ? "gif" : "png";
|
|
1943
|
-
return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=256`;
|
|
1944
|
-
}
|
|
1945
|
-
const fallbackIndex = Number((BigInt(user.id) >> 22n) % 6n);
|
|
1946
|
-
return `https://cdn.discordapp.com/embed/avatars/${fallbackIndex}.png`;
|
|
1947
|
-
}
|
|
1948
|
-
function getGuildIconUrl(guild) {
|
|
1949
|
-
if (!guild.icon) return null;
|
|
1950
|
-
const ext = guild.icon.startsWith("a_") ? "gif" : "png";
|
|
1951
|
-
return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${ext}?size=128`;
|
|
1952
|
-
}
|
|
1953
|
-
var DiscordDashboard = class {
|
|
1954
|
-
app;
|
|
1955
|
-
options;
|
|
1956
|
-
helpers;
|
|
1957
|
-
router;
|
|
1958
|
-
server;
|
|
1959
|
-
basePath;
|
|
1960
|
-
templateRenderer;
|
|
1961
|
-
plugins;
|
|
1962
|
-
constructor(options) {
|
|
1963
|
-
this.validateOptions(options);
|
|
1964
|
-
this.options = options;
|
|
1965
|
-
this.app = options.app ?? (0, import_express.default)();
|
|
1966
|
-
this.router = import_express.default.Router();
|
|
1967
|
-
this.helpers = new DiscordHelpers(options.botToken);
|
|
1968
|
-
this.basePath = normalizeBasePath(options.basePath);
|
|
1969
|
-
this.templateRenderer = this.resolveTemplateRenderer();
|
|
1970
|
-
this.plugins = options.plugins ?? [];
|
|
1971
|
-
if (!options.app && options.trustProxy !== void 0) {
|
|
1972
|
-
this.app.set("trust proxy", options.trustProxy);
|
|
1973
|
-
}
|
|
1974
|
-
this.setupMiddleware();
|
|
1975
|
-
this.setupRoutes();
|
|
1976
|
-
this.app.use(this.basePath, this.router);
|
|
1977
|
-
}
|
|
1978
|
-
validateOptions(options) {
|
|
1979
|
-
if (!options.botToken) throw new Error("botToken is required");
|
|
1980
|
-
if (!options.clientId) throw new Error("clientId is required");
|
|
1981
|
-
if (!options.clientSecret) throw new Error("clientSecret is required");
|
|
1982
|
-
if (!options.redirectUri) throw new Error("redirectUri is required");
|
|
1983
|
-
if (!options.sessionSecret) throw new Error("sessionSecret is required");
|
|
1984
|
-
}
|
|
1985
|
-
setupMiddleware() {
|
|
1986
|
-
this.router.use((0, import_compression.default)());
|
|
1987
|
-
this.router.use((0, import_helmet.default)({ contentSecurityPolicy: false }));
|
|
1988
|
-
this.router.use(import_express.default.json());
|
|
1989
|
-
this.router.use(
|
|
1990
|
-
(0, import_express_session.default)({
|
|
1991
|
-
name: this.options.sessionName ?? "discord_dashboard.sid",
|
|
1992
|
-
secret: this.options.sessionSecret,
|
|
1993
|
-
resave: false,
|
|
1994
|
-
saveUninitialized: false,
|
|
1995
|
-
cookie: {
|
|
1996
|
-
httpOnly: true,
|
|
1997
|
-
sameSite: "lax",
|
|
1998
|
-
maxAge: this.options.sessionMaxAgeMs ?? 1e3 * 60 * 60 * 24 * 7
|
|
1999
|
-
}
|
|
2000
|
-
})
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
setupRoutes() {
|
|
2004
|
-
this.router.get("/", this.handleRoot.bind(this));
|
|
2005
|
-
this.router.get("/login", this.handleLogin.bind(this));
|
|
2006
|
-
this.router.get("/callback", this.handleCallback.bind(this));
|
|
2007
|
-
this.router.post("/logout", this.handleLogout.bind(this));
|
|
2008
|
-
this.router.get("/api/session", this.handleSession.bind(this));
|
|
2009
|
-
this.router.get("/api/guilds", this.ensureAuthenticated, this.handleGuilds.bind(this));
|
|
2010
|
-
this.router.get("/api/overview", this.ensureAuthenticated, this.handleOverview.bind(this));
|
|
2011
|
-
this.router.get("/api/home/categories", this.ensureAuthenticated, this.handleHomeCategories.bind(this));
|
|
2012
|
-
this.router.get("/api/home", this.ensureAuthenticated, this.handleHome.bind(this));
|
|
2013
|
-
this.router.get("/api/lookup/roles", this.ensureAuthenticated, this.handleLookupRoles.bind(this));
|
|
2014
|
-
this.router.get("/api/lookup/channels", this.ensureAuthenticated, this.handleLookupChannels.bind(this));
|
|
2015
|
-
this.router.get("/api/lookup/members", this.ensureAuthenticated, this.handleLookupMembers.bind(this));
|
|
2016
|
-
this.router.post("/api/home/:actionId", this.ensureAuthenticated, this.handleHomeAction.bind(this));
|
|
2017
|
-
this.router.get("/api/plugins", this.ensureAuthenticated, this.handlePlugins.bind(this));
|
|
2018
|
-
this.router.post("/api/plugins/:pluginId/:actionId", this.ensureAuthenticated, this.handlePluginAction.bind(this));
|
|
2019
|
-
}
|
|
2020
|
-
// --- Start/Stop Methods ---
|
|
2021
|
-
async start() {
|
|
2022
|
-
if (this.options.app || this.server) return;
|
|
2023
|
-
const port = this.options.port ?? 3e3;
|
|
2024
|
-
const host = this.options.host ?? "0.0.0.0";
|
|
2025
|
-
this.server = (0, import_node_http.createServer)(this.app);
|
|
2026
|
-
return new Promise((resolve) => {
|
|
2027
|
-
this.server.listen(port, host, () => resolve());
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
async stop() {
|
|
2031
|
-
if (!this.server) return;
|
|
2032
|
-
return new Promise((resolve, reject) => {
|
|
2033
|
-
this.server.close((error) => {
|
|
2034
|
-
if (error) return reject(error);
|
|
2035
|
-
resolve();
|
|
2036
|
-
});
|
|
2037
|
-
this.server = void 0;
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
// --- Route Handlers ---
|
|
2041
|
-
handleRoot(req, res) {
|
|
2042
|
-
if (!req.session.discordAuth) {
|
|
2043
|
-
res.redirect(`${this.basePath}/login`);
|
|
2044
|
-
return;
|
|
2045
|
-
}
|
|
2046
|
-
res.setHeader("Cache-Control", "no-store");
|
|
2047
|
-
res.type("html").send(
|
|
2048
|
-
this.templateRenderer({
|
|
2049
|
-
dashboardName: this.options.dashboardName ?? "Discord Dashboard",
|
|
2050
|
-
basePath: this.basePath,
|
|
2051
|
-
setupDesign: this.options.setupDesign
|
|
2052
|
-
})
|
|
2053
|
-
);
|
|
2054
|
-
}
|
|
2055
|
-
handleLogin(req, res) {
|
|
2314
|
+
router.get("/", (req, res) => {
|
|
2315
|
+
if (!req.session.discordAuth) return res.redirect(`${basePath}/login`);
|
|
2316
|
+
res.type("html").send(engine.render(basePath));
|
|
2317
|
+
});
|
|
2318
|
+
router.get("/login", (req, res) => {
|
|
2056
2319
|
const state = (0, import_node_crypto.randomBytes)(16).toString("hex");
|
|
2057
2320
|
req.session.oauthState = state;
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
scope,
|
|
2064
|
-
state,
|
|
2065
|
-
prompt: "none"
|
|
2066
|
-
});
|
|
2067
|
-
res.redirect(`https://discord.com/oauth2/authorize?${query}`);
|
|
2068
|
-
}
|
|
2069
|
-
async handleCallback(req, res) {
|
|
2321
|
+
res.redirect(engine.getAuthUrl(state));
|
|
2322
|
+
});
|
|
2323
|
+
router.get("/callback", async (req, res) => {
|
|
2324
|
+
const { code, state } = req.query;
|
|
2325
|
+
if (state !== req.session.oauthState) return res.status(403).send("Invalid OAuth2 state");
|
|
2070
2326
|
try {
|
|
2071
|
-
const
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2327
|
+
const tokens = await engine.exchangeCode(code);
|
|
2328
|
+
const [user, rawGuilds] = await Promise.all([engine.fetchUser(tokens.access_token), engine.fetchGuilds(tokens.access_token)]);
|
|
2329
|
+
const ADMIN = 8n;
|
|
2330
|
+
const MANAGE_GUILD = 32n;
|
|
2331
|
+
const processedGuilds = rawGuilds.filter((guild) => {
|
|
2332
|
+
const perms = BigInt(guild.permissions || "0");
|
|
2333
|
+
return (perms & ADMIN) === ADMIN || (perms & MANAGE_GUILD) === MANAGE_GUILD;
|
|
2334
|
+
}).map((guild) => ({
|
|
2335
|
+
...guild,
|
|
2336
|
+
iconUrl: engine.helpers.getGuildIconUrl(guild.id, guild.icon),
|
|
2337
|
+
botInGuild: options.client.guilds.cache.has(guild.id)
|
|
2338
|
+
}));
|
|
2077
2339
|
req.session.discordAuth = {
|
|
2078
|
-
accessToken:
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2340
|
+
accessToken: tokens.access_token,
|
|
2341
|
+
user: {
|
|
2342
|
+
...user,
|
|
2343
|
+
avatarUrl: engine.helpers.getUserAvatarUrl(user.id, user.avatar)
|
|
2344
|
+
},
|
|
2345
|
+
guilds: processedGuilds
|
|
2083
2346
|
};
|
|
2084
|
-
|
|
2085
|
-
res.redirect(this.basePath);
|
|
2347
|
+
res.redirect(basePath);
|
|
2086
2348
|
} catch (error) {
|
|
2087
|
-
|
|
2088
|
-
res.
|
|
2349
|
+
console.error("Dashboard Auth Error:", error);
|
|
2350
|
+
res.redirect(`${basePath}/login`);
|
|
2089
2351
|
}
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
req.session.
|
|
2093
|
-
if (sessionError) return res.status(500).json({ ok: false, message: "Failed to destroy session" });
|
|
2094
|
-
res.clearCookie(this.options.sessionName ?? "discord_dashboard.sid");
|
|
2095
|
-
res.json({ ok: true });
|
|
2096
|
-
});
|
|
2097
|
-
}
|
|
2098
|
-
handleSession(req, res) {
|
|
2099
|
-
const auth = req.session.discordAuth;
|
|
2100
|
-
if (!auth) return res.status(200).json({ authenticated: false });
|
|
2101
|
-
const manageableGuildCount = auth.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
|
|
2352
|
+
});
|
|
2353
|
+
router.get("/api/session", (req, res) => {
|
|
2354
|
+
if (!req.session.discordAuth) return res.json({ authenticated: false });
|
|
2102
2355
|
res.json({
|
|
2103
2356
|
authenticated: true,
|
|
2104
|
-
user:
|
|
2105
|
-
guildCount:
|
|
2106
|
-
expiresAt: auth.expiresAt
|
|
2107
|
-
});
|
|
2108
|
-
}
|
|
2109
|
-
async handleGuilds(req, res) {
|
|
2110
|
-
const context = this.createContext(req);
|
|
2111
|
-
if (this.options.ownerIds && this.options.ownerIds.length > 0 && !this.options.ownerIds.includes(context.user.id)) {
|
|
2112
|
-
return res.status(403).json({ message: "You are not allowed to access this dashboard." });
|
|
2113
|
-
}
|
|
2114
|
-
let manageableGuilds = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions));
|
|
2115
|
-
if (this.options.guildFilter) {
|
|
2116
|
-
const filtered = [];
|
|
2117
|
-
for (const guild of manageableGuilds) {
|
|
2118
|
-
if (await this.options.guildFilter(guild, context)) filtered.push(guild);
|
|
2119
|
-
}
|
|
2120
|
-
manageableGuilds = filtered;
|
|
2121
|
-
}
|
|
2122
|
-
const botGuildIds = await this.fetchBotGuildIds();
|
|
2123
|
-
const enrichedGuilds = manageableGuilds.map((guild) => {
|
|
2124
|
-
const botInGuild = botGuildIds.has(guild.id);
|
|
2125
|
-
return {
|
|
2126
|
-
...guild,
|
|
2127
|
-
iconUrl: getGuildIconUrl(guild),
|
|
2128
|
-
botInGuild,
|
|
2129
|
-
inviteUrl: botInGuild ? void 0 : this.createGuildInviteUrl(guild.id)
|
|
2130
|
-
};
|
|
2357
|
+
user: req.session.discordAuth.user,
|
|
2358
|
+
guildCount: req.session.discordAuth.guilds.length
|
|
2131
2359
|
});
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2360
|
+
});
|
|
2361
|
+
router.get("/api/guilds", (req, res) => {
|
|
2362
|
+
if (!req.session.discordAuth) return res.status(401).json({ error: "Unauthorized" });
|
|
2363
|
+
res.json({ guilds: req.session.discordAuth.guilds });
|
|
2364
|
+
});
|
|
2365
|
+
router.get("/api/overview", async (req, res) => {
|
|
2366
|
+
if (!req.session.discordAuth) return res.status(401).json({ error: "Unauthorized" });
|
|
2367
|
+
const cards = options.getOverviewCards ? await options.getOverviewCards(buildContext(req)) : [];
|
|
2137
2368
|
res.json({ cards });
|
|
2138
|
-
}
|
|
2139
|
-
async
|
|
2140
|
-
|
|
2141
|
-
const
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
const
|
|
2148
|
-
const
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
});
|
|
2157
|
-
res.json({ sections, activeScope });
|
|
2158
|
-
}
|
|
2159
|
-
async handleLookupRoles(req, res) {
|
|
2160
|
-
const context = this.createContext(req);
|
|
2161
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2162
|
-
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2163
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2164
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2165
|
-
const includeManaged = typeof req.query.includeManaged === "string" ? req.query.includeManaged === "true" : void 0;
|
|
2166
|
-
const roles = await context.helpers.searchGuildRoles(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, includeManaged });
|
|
2167
|
-
res.json({ roles });
|
|
2168
|
-
}
|
|
2169
|
-
async handleLookupChannels(req, res) {
|
|
2170
|
-
const context = this.createContext(req);
|
|
2171
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2172
|
-
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2173
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2174
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2175
|
-
const nsfw = typeof req.query.nsfw === "string" ? req.query.nsfw === "true" : void 0;
|
|
2176
|
-
const channelTypes = typeof req.query.channelTypes === "string" ? req.query.channelTypes.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item)) : void 0;
|
|
2177
|
-
const channels = await context.helpers.searchGuildChannels(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, nsfw, channelTypes });
|
|
2178
|
-
res.json({ channels });
|
|
2179
|
-
}
|
|
2180
|
-
async handleLookupMembers(req, res) {
|
|
2181
|
-
const context = this.createContext(req);
|
|
2182
|
-
const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
|
|
2183
|
-
if (!guildId) return res.status(400).json({ message: "guildId is required" });
|
|
2184
|
-
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
2185
|
-
const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
|
|
2186
|
-
const members = await context.helpers.searchGuildMembers(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0 });
|
|
2187
|
-
res.json({ members });
|
|
2188
|
-
}
|
|
2189
|
-
async handleHomeAction(req, res) {
|
|
2190
|
-
const context = this.createContext(req);
|
|
2191
|
-
const action = this.options.home?.actions?.[req.params.actionId];
|
|
2192
|
-
if (!action) return res.status(404).json({ ok: false, message: "Home action not found" });
|
|
2193
|
-
const payload = req.body;
|
|
2194
|
-
if (!payload || typeof payload.sectionId !== "string" || !payload.values || typeof payload.values !== "object") {
|
|
2195
|
-
return res.status(400).json({ ok: false, message: "Invalid home action payload" });
|
|
2196
|
-
}
|
|
2369
|
+
});
|
|
2370
|
+
router.get("/api/home/categories", async (req, res) => {
|
|
2371
|
+
if (!req.session.discordAuth) return res.status(401).json({ error: "Unauthorized" });
|
|
2372
|
+
const categories = options.home?.getCategories ? await options.home.getCategories(buildContext(req)) : [];
|
|
2373
|
+
res.json({ categories });
|
|
2374
|
+
});
|
|
2375
|
+
router.get("/api/home", async (req, res) => {
|
|
2376
|
+
if (!req.session.discordAuth) return res.status(401).json({ error: "Unauthorized" });
|
|
2377
|
+
const sections = options.home?.getSections ? await options.home.getSections(buildContext(req)) : [];
|
|
2378
|
+
const categoryId = req.query.categoryId;
|
|
2379
|
+
const filtered = categoryId ? sections.filter((s) => s.categoryId === categoryId) : sections;
|
|
2380
|
+
res.json({ sections: filtered });
|
|
2381
|
+
});
|
|
2382
|
+
router.post("/api/home/:actionId", async (req, res) => {
|
|
2383
|
+
if (!req.session.discordAuth) return res.status(401).json({ error: "Unauthorized" });
|
|
2384
|
+
const actionId = req.params.actionId;
|
|
2385
|
+
const actionFn = options.home?.actions?.[actionId];
|
|
2386
|
+
if (!actionFn) return res.status(404).json({ error: "Action not found" });
|
|
2197
2387
|
try {
|
|
2198
|
-
const result = await
|
|
2388
|
+
const result = await actionFn(buildContext(req), req.body);
|
|
2199
2389
|
res.json(result);
|
|
2200
2390
|
} catch (error) {
|
|
2201
|
-
res.status(500).json({
|
|
2391
|
+
res.status(500).json({ error: error instanceof Error ? error.message : "Action failed" });
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
router.get("/api/plugins", async (req, res) => {
|
|
2395
|
+
if (!req.session.discordAuth) return res.status(401).json({ error: "Unauthorized" });
|
|
2396
|
+
const context = buildContext(req);
|
|
2397
|
+
const resolvedPlugins = await Promise.all(
|
|
2398
|
+
(options.plugins || []).map(async (p) => {
|
|
2399
|
+
return {
|
|
2400
|
+
id: p.id,
|
|
2401
|
+
name: p.name,
|
|
2402
|
+
description: p.description,
|
|
2403
|
+
panels: p.getPanels ? await p.getPanels(context) : p.panels || []
|
|
2404
|
+
};
|
|
2405
|
+
})
|
|
2406
|
+
);
|
|
2407
|
+
res.json({ plugins: resolvedPlugins });
|
|
2408
|
+
});
|
|
2409
|
+
router.post("/api/plugins/:pluginId/:actionId", async (req, res) => {
|
|
2410
|
+
if (!req.session.discordAuth) return res.status(401).json({ error: "Unauthorized" });
|
|
2411
|
+
const { pluginId, actionId } = req.params;
|
|
2412
|
+
const plugin = options.plugins?.find((p) => p.id === pluginId);
|
|
2413
|
+
if (!plugin || !plugin.actions?.[actionId]) {
|
|
2414
|
+
return res.status(404).json({ error: "Plugin or action not found" });
|
|
2202
2415
|
}
|
|
2203
|
-
}
|
|
2204
|
-
async handlePlugins(req, res) {
|
|
2205
|
-
const context = this.createContext(req);
|
|
2206
|
-
const activeScope = context.selectedGuildId ? "guild" : "user";
|
|
2207
|
-
const payload = [];
|
|
2208
|
-
for (const plugin of this.plugins) {
|
|
2209
|
-
const pluginScope = plugin.scope ?? "both";
|
|
2210
|
-
if (pluginScope !== "both" && pluginScope !== activeScope) continue;
|
|
2211
|
-
const panels = await plugin.getPanels(context);
|
|
2212
|
-
payload.push({ id: plugin.id, name: plugin.name, description: plugin.description, panels });
|
|
2213
|
-
}
|
|
2214
|
-
res.json({ plugins: payload });
|
|
2215
|
-
}
|
|
2216
|
-
async handlePluginAction(req, res) {
|
|
2217
|
-
const context = this.createContext(req);
|
|
2218
|
-
const plugin = this.plugins.find((item) => item.id === req.params.pluginId);
|
|
2219
|
-
if (!plugin) return res.status(404).json({ ok: false, message: "Plugin not found" });
|
|
2220
|
-
const action = plugin.actions?.[req.params.actionId];
|
|
2221
|
-
if (!action) return res.status(404).json({ ok: false, message: "Action not found" });
|
|
2222
2416
|
try {
|
|
2223
|
-
const
|
|
2417
|
+
const context = buildContext(req);
|
|
2418
|
+
const result = await plugin.actions[actionId](context, req.body);
|
|
2224
2419
|
res.json(result);
|
|
2225
2420
|
} catch (error) {
|
|
2226
|
-
|
|
2421
|
+
console.error(`Action Error (${pluginId}/${actionId}):`, error);
|
|
2422
|
+
res.status(500).json({ error: "Internal Server Error" });
|
|
2227
2423
|
}
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2424
|
+
});
|
|
2425
|
+
app.use(basePath, router);
|
|
2426
|
+
return { app, engine };
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/adapters/elysia.ts
|
|
2430
|
+
var import_jwt = __toESM(require("@elysiajs/jwt"), 1);
|
|
2431
|
+
var import_node = __toESM(require("@elysiajs/node"), 1);
|
|
2432
|
+
var import_elysia2 = require("elysia");
|
|
2433
|
+
var import_node_crypto2 = require("crypto");
|
|
2434
|
+
function createElysiaAdapter(options) {
|
|
2435
|
+
const engine = new DashboardEngine(options);
|
|
2436
|
+
const basePath = options.basePath ?? "/dashboard";
|
|
2437
|
+
const sessionName = options.sessionName ?? "discord_dashboard.sid";
|
|
2438
|
+
const app = options.app ?? new import_elysia2.Elysia({ adapter: (0, import_node.default)() });
|
|
2439
|
+
const getSession = async (sessionSigner, cookie) => {
|
|
2440
|
+
const cookieValue = cookie[sessionName]?.value;
|
|
2441
|
+
if (!cookieValue) return null;
|
|
2442
|
+
const session2 = await sessionSigner.verify(cookieValue);
|
|
2443
|
+
return session2 ?? null;
|
|
2236
2444
|
};
|
|
2237
|
-
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
selectedGuildId: typeof req.query.guildId === "string" ? req.query.guildId : void 0,
|
|
2245
|
-
helpers: this.helpers
|
|
2246
|
-
};
|
|
2247
|
-
}
|
|
2248
|
-
async exchangeCodeForToken(code) {
|
|
2249
|
-
const response = await fetch(`${DISCORD_API}/oauth2/token`, {
|
|
2250
|
-
method: "POST",
|
|
2251
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
2252
|
-
body: toQuery({
|
|
2253
|
-
client_id: this.options.clientId,
|
|
2254
|
-
client_secret: this.options.clientSecret,
|
|
2255
|
-
grant_type: "authorization_code",
|
|
2256
|
-
code,
|
|
2257
|
-
redirect_uri: this.options.redirectUri
|
|
2258
|
-
})
|
|
2445
|
+
const setSession = async (sessionSigner, cookie, session2) => {
|
|
2446
|
+
const token = await sessionSigner.sign(session2);
|
|
2447
|
+
cookie[sessionName].set({
|
|
2448
|
+
value: token,
|
|
2449
|
+
httpOnly: true,
|
|
2450
|
+
sameSite: "lax",
|
|
2451
|
+
path: "/"
|
|
2259
2452
|
});
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
const
|
|
2280
|
-
return
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
description: "Core dashboard setup information",
|
|
2285
|
-
scope: "setup",
|
|
2286
|
-
categoryId: "setup",
|
|
2287
|
-
fields: [
|
|
2288
|
-
{ id: "dashboardName", label: "Dashboard Name", type: "text", value: this.options.dashboardName ?? "Discord Dashboard", readOnly: true },
|
|
2289
|
-
{ id: "basePath", label: "Base Path", type: "text", value: this.basePath, readOnly: true }
|
|
2290
|
-
]
|
|
2291
|
-
},
|
|
2292
|
-
{
|
|
2293
|
-
id: "context",
|
|
2294
|
-
title: "Dashboard Context",
|
|
2295
|
-
description: selectedGuild ? `Managing ${selectedGuild.name}` : "Managing user dashboard",
|
|
2296
|
-
scope: this.resolveScope(context),
|
|
2297
|
-
categoryId: "overview",
|
|
2298
|
-
fields: [
|
|
2299
|
-
{ id: "mode", label: "Mode", type: "text", value: selectedGuild ? "Guild" : "User", readOnly: true },
|
|
2300
|
-
{ id: "target", label: "Target", type: "text", value: selectedGuild ? selectedGuild.name : context.user.username, readOnly: true }
|
|
2301
|
-
]
|
|
2453
|
+
};
|
|
2454
|
+
const buildContext = (session2, query) => ({
|
|
2455
|
+
user: session2.discordAuth.user,
|
|
2456
|
+
guilds: (session2.discordAuth.guilds || []).map((guild) => ({
|
|
2457
|
+
...guild,
|
|
2458
|
+
botInGuild: guild.botInGuild ?? void 0,
|
|
2459
|
+
inviteUrl: guild.inviteUrl ?? void 0
|
|
2460
|
+
})),
|
|
2461
|
+
accessToken: session2.discordAuth.accessToken || "",
|
|
2462
|
+
selectedGuildId: typeof query?.guildId === "string" ? query.guildId : void 0,
|
|
2463
|
+
helpers: engine.helpers
|
|
2464
|
+
});
|
|
2465
|
+
const router = new import_elysia2.Elysia({ prefix: basePath }).use(
|
|
2466
|
+
(0, import_jwt.default)({
|
|
2467
|
+
name: "sessionSigner",
|
|
2468
|
+
secret: options.sessionSecret,
|
|
2469
|
+
schema: SessionSchema
|
|
2470
|
+
})
|
|
2471
|
+
).get("/", async ({ sessionSigner, cookie, redirect }) => {
|
|
2472
|
+
const sessionData = await getSession(sessionSigner, cookie);
|
|
2473
|
+
if (!sessionData || !sessionData.discordAuth) return redirect(`${basePath}/login`);
|
|
2474
|
+
return new Response(engine.render(basePath), {
|
|
2475
|
+
headers: {
|
|
2476
|
+
"content-type": "text/html; charset=utf-8"
|
|
2302
2477
|
}
|
|
2303
|
-
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
async
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
}
|
|
2478
|
+
});
|
|
2479
|
+
}).get("/login", async ({ sessionSigner, cookie, redirect }) => {
|
|
2480
|
+
const state = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
2481
|
+
await setSession(sessionSigner, cookie, { oauthState: state });
|
|
2482
|
+
return redirect(engine.getAuthUrl(state));
|
|
2483
|
+
}).get("/callback", async ({ query, sessionSigner, cookie, redirect }) => {
|
|
2484
|
+
const { code, state } = query;
|
|
2485
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2486
|
+
if (!code || !state || !session2 || state !== session2.oauthState) return redirect(`${basePath}/login`);
|
|
2487
|
+
try {
|
|
2488
|
+
const tokens = await engine.exchangeCode(code);
|
|
2489
|
+
const [user, rawGuilds] = await Promise.all([engine.fetchUser(tokens.access_token), engine.fetchGuilds(tokens.access_token)]);
|
|
2490
|
+
const ADMIN = 8n;
|
|
2491
|
+
const MANAGE_GUILD = 32n;
|
|
2492
|
+
const processedGuilds = rawGuilds.filter((guild) => {
|
|
2493
|
+
const perms = BigInt(guild.permissions || "0");
|
|
2494
|
+
return (perms & ADMIN) === ADMIN || (perms & MANAGE_GUILD) === MANAGE_GUILD;
|
|
2495
|
+
}).map((guild) => ({
|
|
2496
|
+
id: guild.id,
|
|
2497
|
+
name: guild.name,
|
|
2498
|
+
icon: guild.icon,
|
|
2499
|
+
owner: guild.owner,
|
|
2500
|
+
permissions: guild.permissions,
|
|
2501
|
+
iconUrl: engine.helpers.getGuildIconUrl(guild.id, guild.icon),
|
|
2502
|
+
botInGuild: options.client.guilds.cache.has(guild.id)
|
|
2503
|
+
}));
|
|
2504
|
+
await setSession(sessionSigner, cookie, {
|
|
2505
|
+
discordAuth: {
|
|
2506
|
+
accessToken: tokens.access_token,
|
|
2507
|
+
user: {
|
|
2508
|
+
id: user.id,
|
|
2509
|
+
username: user.username,
|
|
2510
|
+
discriminator: user.discriminator,
|
|
2511
|
+
avatar: user.avatar,
|
|
2512
|
+
global_name: user.global_name,
|
|
2513
|
+
avatarUrl: engine.helpers.getUserAvatarUrl(user.id, user.avatar)
|
|
2514
|
+
},
|
|
2515
|
+
guilds: processedGuilds
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
return redirect(basePath);
|
|
2519
|
+
} catch (error) {
|
|
2520
|
+
console.error("Dashboard Auth Error:", error);
|
|
2521
|
+
return redirect(`${basePath}/login`);
|
|
2522
|
+
}
|
|
2523
|
+
}).get("/api/session", async ({ sessionSigner, cookie }) => {
|
|
2524
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2525
|
+
if (!session2?.discordAuth) return { authenticated: false };
|
|
2526
|
+
return {
|
|
2527
|
+
authenticated: true,
|
|
2528
|
+
user: session2.discordAuth.user,
|
|
2529
|
+
guildCount: session2.discordAuth.guilds.length
|
|
2530
|
+
};
|
|
2531
|
+
}).get("/api/guilds", async ({ sessionSigner, cookie, set }) => {
|
|
2532
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2533
|
+
if (!session2?.discordAuth) {
|
|
2534
|
+
set.status = 401;
|
|
2535
|
+
return { error: "Unauthorized" };
|
|
2536
|
+
}
|
|
2537
|
+
return { guilds: session2.discordAuth.guilds };
|
|
2538
|
+
}).get("/api/overview", async ({ sessionSigner, cookie, query, set }) => {
|
|
2539
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2540
|
+
if (!session2?.discordAuth) {
|
|
2541
|
+
set.status = 401;
|
|
2542
|
+
return { error: "Unauthorized" };
|
|
2543
|
+
}
|
|
2544
|
+
const cards = options.getOverviewCards ? await options.getOverviewCards(buildContext(session2, query)) : [];
|
|
2545
|
+
return { cards };
|
|
2546
|
+
}).get("/api/home/categories", async ({ sessionSigner, cookie, query, set }) => {
|
|
2547
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2548
|
+
if (!session2?.discordAuth) {
|
|
2549
|
+
set.status = 401;
|
|
2550
|
+
return { error: "Unauthorized" };
|
|
2551
|
+
}
|
|
2552
|
+
const categories = options.home?.getCategories ? await options.home.getCategories(buildContext(session2, query)) : [];
|
|
2553
|
+
return { categories };
|
|
2554
|
+
}).get("/api/home", async ({ sessionSigner, cookie, query, set }) => {
|
|
2555
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2556
|
+
if (!session2?.discordAuth) {
|
|
2557
|
+
set.status = 401;
|
|
2558
|
+
return { error: "Unauthorized" };
|
|
2559
|
+
}
|
|
2560
|
+
const context = buildContext(session2, query);
|
|
2561
|
+
const sections = options.home?.getSections ? await options.home.getSections(context) : [];
|
|
2562
|
+
const categoryId = typeof query.categoryId === "string" ? query.categoryId : void 0;
|
|
2563
|
+
const filteredSections = categoryId ? sections.filter((section) => section.categoryId === categoryId) : sections;
|
|
2564
|
+
return { sections: filteredSections };
|
|
2565
|
+
}).post("/api/home/:actionId", async ({ params, body, sessionSigner, cookie, query, set }) => {
|
|
2566
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2567
|
+
if (!session2?.discordAuth) {
|
|
2568
|
+
set.status = 401;
|
|
2569
|
+
return { error: "Unauthorized" };
|
|
2570
|
+
}
|
|
2571
|
+
const actionFn = options.home?.actions?.[params.actionId];
|
|
2572
|
+
if (!actionFn) {
|
|
2573
|
+
set.status = 404;
|
|
2574
|
+
return { error: "Action not found" };
|
|
2575
|
+
}
|
|
2576
|
+
try {
|
|
2577
|
+
return await actionFn(buildContext(session2, query), body);
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
set.status = 500;
|
|
2580
|
+
return { error: error instanceof Error ? error.message : "Action failed" };
|
|
2581
|
+
}
|
|
2582
|
+
}).get("/api/plugins", async ({ sessionSigner, cookie, query, set }) => {
|
|
2583
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2584
|
+
if (!session2?.discordAuth) {
|
|
2585
|
+
set.status = 401;
|
|
2586
|
+
return { error: "Unauthorized" };
|
|
2587
|
+
}
|
|
2588
|
+
const context = buildContext(session2, query);
|
|
2589
|
+
const resolvedPlugins = await Promise.all(
|
|
2590
|
+
(options.plugins || []).map(async (plugin) => ({
|
|
2591
|
+
id: plugin.id,
|
|
2592
|
+
name: plugin.name,
|
|
2593
|
+
description: plugin.description,
|
|
2594
|
+
panels: plugin.getPanels ? await plugin.getPanels(context) : plugin.panels || []
|
|
2595
|
+
}))
|
|
2596
|
+
);
|
|
2597
|
+
return { plugins: resolvedPlugins };
|
|
2598
|
+
}).post("/api/plugins/:pluginId/:actionId", async ({ params, body, sessionSigner, cookie, query, set }) => {
|
|
2599
|
+
const session2 = await getSession(sessionSigner, cookie);
|
|
2600
|
+
if (!session2?.discordAuth) {
|
|
2601
|
+
set.status = 401;
|
|
2602
|
+
return { error: "Unauthorized" };
|
|
2603
|
+
}
|
|
2604
|
+
const plugin = options.plugins?.find((item) => item.id === params.pluginId);
|
|
2605
|
+
if (!plugin || !plugin.actions?.[params.actionId]) {
|
|
2606
|
+
set.status = 404;
|
|
2607
|
+
return { error: "Plugin or action not found" };
|
|
2608
|
+
}
|
|
2609
|
+
try {
|
|
2610
|
+
return await plugin.actions[params.actionId](buildContext(session2, query), body);
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
console.error(`Action Error (${params.pluginId}/${params.actionId}):`, error);
|
|
2613
|
+
set.status = 500;
|
|
2614
|
+
return { error: "Internal Server Error" };
|
|
2615
|
+
}
|
|
2616
|
+
});
|
|
2617
|
+
app.use(router);
|
|
2618
|
+
return { app, engine };
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// src/adapters/fastify.ts
|
|
2622
|
+
var import_crypto = require("crypto");
|
|
2623
|
+
function createFastifyAdapter(fastify, options) {
|
|
2624
|
+
const engine = new DashboardEngine(options);
|
|
2625
|
+
const basePath = options.basePath ?? "/dashboard";
|
|
2626
|
+
const buildContext = (request) => {
|
|
2627
|
+
const query = request.query;
|
|
2628
|
+
return {
|
|
2629
|
+
user: request.session.discordAuth.user,
|
|
2630
|
+
guilds: request.session.discordAuth.guilds || [],
|
|
2631
|
+
accessToken: request.session.discordAuth.accessToken || "",
|
|
2632
|
+
selectedGuildId: typeof query.guildId === "string" ? query.guildId : void 0,
|
|
2633
|
+
helpers: engine.helpers
|
|
2634
|
+
};
|
|
2635
|
+
};
|
|
2636
|
+
fastify.register(
|
|
2637
|
+
async (instance) => {
|
|
2638
|
+
instance.get("/", async (request, reply) => {
|
|
2639
|
+
if (!request.session.discordAuth) return reply.redirect(`${basePath}/login`);
|
|
2640
|
+
return reply.type("html").send(engine.render(basePath));
|
|
2641
|
+
});
|
|
2642
|
+
instance.get("/login", async (request, reply) => {
|
|
2643
|
+
const state = (0, import_crypto.randomBytes)(16).toString("hex");
|
|
2644
|
+
request.session.oauthState = state;
|
|
2645
|
+
return reply.redirect(engine.getAuthUrl(state));
|
|
2646
|
+
});
|
|
2647
|
+
instance.get("/callback", async (request, reply) => {
|
|
2648
|
+
const { code, state } = request.query;
|
|
2649
|
+
if (state !== request.session.oauthState) return reply.status(403).send("Invalid OAuth2 state");
|
|
2650
|
+
try {
|
|
2651
|
+
const tokens = await engine.exchangeCode(code);
|
|
2652
|
+
const [user, rawGuilds] = await Promise.all([engine.fetchUser(tokens.access_token), engine.fetchGuilds(tokens.access_token)]);
|
|
2653
|
+
const ADMIN = 8n;
|
|
2654
|
+
const MANAGE_GUILD = 32n;
|
|
2655
|
+
const processedGuilds = rawGuilds.filter((guild) => {
|
|
2656
|
+
const perms = BigInt(guild.permissions || "0");
|
|
2657
|
+
return (perms & ADMIN) === ADMIN || (perms & MANAGE_GUILD) === MANAGE_GUILD;
|
|
2658
|
+
}).map((guild) => ({
|
|
2659
|
+
...guild,
|
|
2660
|
+
iconUrl: engine.helpers.getGuildIconUrl(guild.id, guild.icon),
|
|
2661
|
+
botInGuild: options.client.guilds.cache.has(guild.id)
|
|
2662
|
+
}));
|
|
2663
|
+
request.session.discordAuth = {
|
|
2664
|
+
accessToken: tokens.access_token,
|
|
2665
|
+
user: {
|
|
2666
|
+
...user,
|
|
2667
|
+
avatarUrl: engine.helpers.getUserAvatarUrl(user.id, user.avatar)
|
|
2668
|
+
},
|
|
2669
|
+
guilds: processedGuilds
|
|
2670
|
+
};
|
|
2671
|
+
return reply.redirect(basePath);
|
|
2672
|
+
} catch (error) {
|
|
2673
|
+
console.error("Dashboard Auth Error:", error);
|
|
2674
|
+
return reply.redirect(`${basePath}/login`);
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
instance.get("/api/session", async (request, reply) => {
|
|
2678
|
+
if (!request.session.discordAuth) {
|
|
2679
|
+
return reply.send({ authenticated: false });
|
|
2680
|
+
}
|
|
2681
|
+
return reply.send({
|
|
2682
|
+
authenticated: true,
|
|
2683
|
+
user: request.session.discordAuth.user,
|
|
2684
|
+
guildCount: request.session.discordAuth.guilds.length
|
|
2685
|
+
});
|
|
2686
|
+
});
|
|
2687
|
+
instance.get("/api/guilds", async (request, reply) => {
|
|
2688
|
+
if (!request.session.discordAuth) {
|
|
2689
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2690
|
+
}
|
|
2691
|
+
return reply.send({ guilds: request.session.discordAuth.guilds });
|
|
2692
|
+
});
|
|
2693
|
+
instance.get("/api/overview", async (request, reply) => {
|
|
2694
|
+
if (!request.session.discordAuth) {
|
|
2695
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2696
|
+
}
|
|
2697
|
+
const cards = options.getOverviewCards ? await options.getOverviewCards(buildContext(request)) : [];
|
|
2698
|
+
return reply.send({ cards });
|
|
2699
|
+
});
|
|
2700
|
+
instance.get("/api/home/categories", async (request, reply) => {
|
|
2701
|
+
if (!request.session.discordAuth) {
|
|
2702
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2703
|
+
}
|
|
2704
|
+
const categories = options.home?.getCategories ? await options.home.getCategories(buildContext(request)) : [];
|
|
2705
|
+
return reply.send({ categories });
|
|
2706
|
+
});
|
|
2707
|
+
instance.get("/api/home", async (request, reply) => {
|
|
2708
|
+
if (!request.session.discordAuth) {
|
|
2709
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2710
|
+
}
|
|
2711
|
+
const sections = options.home?.getSections ? await options.home.getSections(buildContext(request)) : [];
|
|
2712
|
+
const query = request.query;
|
|
2713
|
+
const categoryId = typeof query.categoryId === "string" ? query.categoryId : void 0;
|
|
2714
|
+
const filteredSections = categoryId ? sections.filter((section) => section.categoryId === categoryId) : sections;
|
|
2715
|
+
return reply.send({ sections: filteredSections });
|
|
2716
|
+
});
|
|
2717
|
+
instance.post("/api/home/:actionId", async (request, reply) => {
|
|
2718
|
+
if (!request.session.discordAuth) {
|
|
2719
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2720
|
+
}
|
|
2721
|
+
const actionFn = options.home?.actions?.[request.params.actionId];
|
|
2722
|
+
if (!actionFn) {
|
|
2723
|
+
return reply.status(404).send({ error: "Action not found" });
|
|
2724
|
+
}
|
|
2725
|
+
try {
|
|
2726
|
+
const result = await actionFn(buildContext(request), request.body);
|
|
2727
|
+
return reply.send(result);
|
|
2728
|
+
} catch (error) {
|
|
2729
|
+
return reply.status(500).send({ error: error instanceof Error ? error.message : "Action failed" });
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2732
|
+
instance.get("/api/plugins", async (request, reply) => {
|
|
2733
|
+
if (!request.session.discordAuth) {
|
|
2734
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2735
|
+
}
|
|
2736
|
+
const context = buildContext(request);
|
|
2737
|
+
const resolvedPlugins = await Promise.all(
|
|
2738
|
+
(options.plugins || []).map(async (plugin) => ({
|
|
2739
|
+
id: plugin.id,
|
|
2740
|
+
name: plugin.name,
|
|
2741
|
+
description: plugin.description,
|
|
2742
|
+
panels: plugin.getPanels ? await plugin.getPanels(context) : plugin.panels || []
|
|
2743
|
+
}))
|
|
2744
|
+
);
|
|
2745
|
+
return reply.send({ plugins: resolvedPlugins });
|
|
2746
|
+
});
|
|
2747
|
+
instance.post(
|
|
2748
|
+
"/api/plugins/:pluginId/:actionId",
|
|
2749
|
+
async (request, reply) => {
|
|
2750
|
+
if (!request.session.discordAuth) {
|
|
2751
|
+
return reply.status(401).send({ error: "Unauthorized" });
|
|
2752
|
+
}
|
|
2753
|
+
const { pluginId, actionId } = request.params;
|
|
2754
|
+
const plugin = options.plugins?.find((item) => item.id === pluginId);
|
|
2755
|
+
if (!plugin || !plugin.actions?.[actionId]) {
|
|
2756
|
+
return reply.status(404).send({ error: "Plugin or action not found" });
|
|
2757
|
+
}
|
|
2758
|
+
try {
|
|
2759
|
+
const result = await plugin.actions[actionId](buildContext(request), request.body);
|
|
2760
|
+
return reply.send(result);
|
|
2761
|
+
} catch (error) {
|
|
2762
|
+
console.error(`Action Error (${pluginId}/${actionId}):`, error);
|
|
2763
|
+
return reply.status(500).send({ error: "Internal Server Error" });
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
);
|
|
2767
|
+
},
|
|
2768
|
+
{ prefix: basePath }
|
|
2769
|
+
);
|
|
2770
|
+
return { engine };
|
|
2771
|
+
}
|
|
2345
2772
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2346
2773
|
0 && (module.exports = {
|
|
2347
2774
|
DashboardDesigner,
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2775
|
+
DashboardEngine,
|
|
2776
|
+
SessionSchema,
|
|
2777
|
+
createElysiaAdapter,
|
|
2778
|
+
createExpressAdapter,
|
|
2779
|
+
createFastifyAdapter
|
|
2352
2780
|
});
|
|
2353
2781
|
//# sourceMappingURL=index.cjs.map
|