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