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