@sonicjs-cms/core 2.0.0 → 2.0.1

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.
Files changed (63) hide show
  1. package/dist/{chunk-QUMBDPNJ.cjs → chunk-5FDDDD4J.cjs} +4414 -381
  2. package/dist/chunk-5FDDDD4J.cjs.map +1 -0
  3. package/dist/chunk-5XTB4FE5.js +1030 -0
  4. package/dist/chunk-5XTB4FE5.js.map +1 -0
  5. package/dist/{chunk-ET5I4GBD.cjs → chunk-ALOS2CBJ.cjs} +194 -4
  6. package/dist/chunk-ALOS2CBJ.cjs.map +1 -0
  7. package/dist/{chunk-7N3HK7ZK.js → chunk-CDBVZEWR.js} +7 -904
  8. package/dist/chunk-CDBVZEWR.js.map +1 -0
  9. package/dist/chunk-EGFHFM4N.cjs +76 -0
  10. package/dist/chunk-EGFHFM4N.cjs.map +1 -0
  11. package/dist/chunk-KM4AJFXI.cjs +101 -0
  12. package/dist/chunk-KM4AJFXI.cjs.map +1 -0
  13. package/dist/{chunk-RNR4HA23.cjs → chunk-LEG4KNFP.cjs} +6 -945
  14. package/dist/chunk-LEG4KNFP.cjs.map +1 -0
  15. package/dist/{chunk-RGCQSFKC.cjs → chunk-NK6FN5R5.cjs} +10 -2
  16. package/dist/chunk-NK6FN5R5.cjs.map +1 -0
  17. package/dist/{chunk-P3VS4DV3.js → chunk-O46XKBFM.js} +193 -5
  18. package/dist/chunk-O46XKBFM.js.map +1 -0
  19. package/dist/{chunk-JIINOD2W.js → chunk-OL2OE3VJ.js} +9 -3
  20. package/dist/chunk-OL2OE3VJ.js.map +1 -0
  21. package/dist/chunk-P2PTTBO5.js +74 -0
  22. package/dist/chunk-P2PTTBO5.js.map +1 -0
  23. package/dist/{chunk-JETM2U2D.js → chunk-QSF34IYQ.js} +4244 -214
  24. package/dist/chunk-QSF34IYQ.js.map +1 -0
  25. package/dist/chunk-SRCY43RN.cjs +1076 -0
  26. package/dist/chunk-SRCY43RN.cjs.map +1 -0
  27. package/dist/chunk-TY3NHEBN.js +80 -0
  28. package/dist/chunk-TY3NHEBN.js.map +1 -0
  29. package/dist/index.cjs +215 -207
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +12 -11
  32. package/dist/index.js.map +1 -1
  33. package/dist/middleware.cjs +22 -22
  34. package/dist/middleware.js +2 -2
  35. package/dist/routes.cjs +35 -23
  36. package/dist/routes.js +6 -6
  37. package/dist/services.cjs +28 -28
  38. package/dist/services.js +2 -2
  39. package/dist/templates.cjs +24 -24
  40. package/dist/templates.js +2 -2
  41. package/dist/utils.cjs +18 -10
  42. package/dist/utils.js +1 -1
  43. package/package.json +2 -16
  44. package/dist/chunk-3MNMOLSA.js +0 -133
  45. package/dist/chunk-3MNMOLSA.js.map +0 -1
  46. package/dist/chunk-4XI3YBKU.cjs +0 -266
  47. package/dist/chunk-4XI3YBKU.cjs.map +0 -1
  48. package/dist/chunk-7N3HK7ZK.js.map +0 -1
  49. package/dist/chunk-AGOE25LF.cjs +0 -137
  50. package/dist/chunk-AGOE25LF.cjs.map +0 -1
  51. package/dist/chunk-BUKT6HP5.cjs +0 -776
  52. package/dist/chunk-BUKT6HP5.cjs.map +0 -1
  53. package/dist/chunk-ET5I4GBD.cjs.map +0 -1
  54. package/dist/chunk-JETM2U2D.js.map +0 -1
  55. package/dist/chunk-JIINOD2W.js.map +0 -1
  56. package/dist/chunk-LU6J53IX.js +0 -262
  57. package/dist/chunk-LU6J53IX.js.map +0 -1
  58. package/dist/chunk-P3VS4DV3.js.map +0 -1
  59. package/dist/chunk-QUMBDPNJ.cjs.map +0 -1
  60. package/dist/chunk-RGCQSFKC.cjs.map +0 -1
  61. package/dist/chunk-RNR4HA23.cjs.map +0 -1
  62. package/dist/chunk-WESS2U3K.js +0 -755
  63. package/dist/chunk-WESS2U3K.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import { getCacheService, CACHE_CONFIGS } from './chunk-3MNMOLSA.js';
2
- import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity, requirePermission } from './chunk-WESS2U3K.js';
3
- import { PluginService, getLogger } from './chunk-7N3HK7ZK.js';
4
- import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2 } from './chunk-P3VS4DV3.js';
5
- import { QueryFilterBuilder, sanitizeInput, escapeHtml } from './chunk-JIINOD2W.js';
1
+ import { getCacheService, CACHE_CONFIGS, getLogger } from './chunk-5XTB4FE5.js';
2
+ import { requireAuth, isPluginActive, requireRole, AuthManager, logActivity, requirePermission } from './chunk-TY3NHEBN.js';
3
+ import { PluginService, MigrationService } from './chunk-CDBVZEWR.js';
4
+ import { init_admin_layout_catalyst_template, renderDesignPage, renderCheckboxPage, renderFAQList, renderTestimonialsList, renderCodeExamplesList, renderAlert, renderTable, renderPagination, renderConfirmationDialog, getConfirmationDialogScript, renderAdminLayoutCatalyst, renderAdminLayout, adminLayoutV2, renderForm } from './chunk-O46XKBFM.js';
5
+ import { QueryFilterBuilder, sanitizeInput, getCoreVersion, escapeHtml } from './chunk-OL2OE3VJ.js';
6
6
  import { Hono } from 'hono';
7
7
  import { cors } from 'hono/cors';
8
8
  import { z } from 'zod';
@@ -2030,209 +2030,9 @@ function renderRegisterPage(data) {
2030
2030
  </html>
2031
2031
  `;
2032
2032
  }
2033
- var AuthValidationService = class _AuthValidationService {
2034
- static instance;
2035
- cachedSettings = null;
2036
- cacheExpiry = 0;
2037
- CACHE_TTL = 5 * 60 * 1e3;
2038
- // 5 minutes
2039
- static getInstance() {
2040
- if (!_AuthValidationService.instance) {
2041
- _AuthValidationService.instance = new _AuthValidationService();
2042
- }
2043
- return _AuthValidationService.instance;
2044
- }
2045
- /**
2046
- * Get authentication settings from core-auth plugin
2047
- */
2048
- async getAuthSettings(db) {
2049
- if (this.cachedSettings && Date.now() < this.cacheExpiry) {
2050
- return this.cachedSettings;
2051
- }
2052
- try {
2053
- const plugin = await db.prepare("SELECT settings FROM plugins WHERE id = ? AND status = ?").bind("core-auth", "active").first();
2054
- if (!plugin || !plugin.settings) {
2055
- console.warn("[AuthValidation] Core-auth plugin not found or not active, using defaults");
2056
- return this.getDefaultSettings();
2057
- }
2058
- const settings = typeof plugin.settings === "string" ? JSON.parse(plugin.settings) : plugin.settings;
2059
- this.cachedSettings = settings;
2060
- this.cacheExpiry = Date.now() + this.CACHE_TTL;
2061
- return settings;
2062
- } catch (error) {
2063
- console.error("[AuthValidation] Error loading auth settings:", error);
2064
- return this.getDefaultSettings();
2065
- }
2066
- }
2067
- /**
2068
- * Get default authentication settings
2069
- */
2070
- getDefaultSettings() {
2071
- return {
2072
- requiredFields: {
2073
- email: { required: true, minLength: 5, label: "Email", type: "email" },
2074
- password: { required: true, minLength: 8, label: "Password", type: "password" },
2075
- username: { required: true, minLength: 3, label: "Username", type: "text" },
2076
- firstName: { required: true, minLength: 1, label: "First Name", type: "text" },
2077
- lastName: { required: true, minLength: 1, label: "Last Name", type: "text" }
2078
- },
2079
- validation: {
2080
- emailFormat: true,
2081
- allowDuplicateUsernames: false,
2082
- passwordRequirements: {
2083
- requireUppercase: false,
2084
- requireLowercase: false,
2085
- requireNumbers: false,
2086
- requireSpecialChars: false
2087
- }
2088
- },
2089
- registration: {
2090
- enabled: true,
2091
- requireEmailVerification: false,
2092
- defaultRole: "viewer"
2093
- }
2094
- };
2095
- }
2096
- /**
2097
- * Build dynamic Zod schema based on settings
2098
- */
2099
- async buildRegistrationSchema(db) {
2100
- const settings = await this.getAuthSettings(db);
2101
- const fields = settings.requiredFields;
2102
- const validation = settings.validation;
2103
- const schemaFields = {};
2104
- if (fields.email.required) {
2105
- let emailSchema = z.string();
2106
- if (validation.emailFormat) {
2107
- emailSchema = emailSchema.email("Valid email is required");
2108
- }
2109
- if (fields.email.minLength > 0) {
2110
- emailSchema = emailSchema.min(
2111
- fields.email.minLength,
2112
- `Email must be at least ${fields.email.minLength} characters`
2113
- );
2114
- }
2115
- schemaFields.email = emailSchema;
2116
- } else {
2117
- schemaFields.email = z.string().email().optional();
2118
- }
2119
- if (fields.password.required) {
2120
- let passwordSchema = z.string().min(
2121
- fields.password.minLength,
2122
- `Password must be at least ${fields.password.minLength} characters`
2123
- );
2124
- if (validation.passwordRequirements.requireUppercase) {
2125
- passwordSchema = passwordSchema.regex(
2126
- /[A-Z]/,
2127
- "Password must contain at least one uppercase letter"
2128
- );
2129
- }
2130
- if (validation.passwordRequirements.requireLowercase) {
2131
- passwordSchema = passwordSchema.regex(
2132
- /[a-z]/,
2133
- "Password must contain at least one lowercase letter"
2134
- );
2135
- }
2136
- if (validation.passwordRequirements.requireNumbers) {
2137
- passwordSchema = passwordSchema.regex(
2138
- /[0-9]/,
2139
- "Password must contain at least one number"
2140
- );
2141
- }
2142
- if (validation.passwordRequirements.requireSpecialChars) {
2143
- passwordSchema = passwordSchema.regex(
2144
- /[!@#$%^&*(),.?":{}|<>]/,
2145
- "Password must contain at least one special character"
2146
- );
2147
- }
2148
- schemaFields.password = passwordSchema;
2149
- } else {
2150
- schemaFields.password = z.string().min(fields.password.minLength).optional();
2151
- }
2152
- if (fields.username.required) {
2153
- schemaFields.username = z.string().min(
2154
- fields.username.minLength,
2155
- `Username must be at least ${fields.username.minLength} characters`
2156
- );
2157
- } else {
2158
- schemaFields.username = z.string().min(fields.username.minLength).optional();
2159
- }
2160
- if (fields.firstName.required) {
2161
- schemaFields.firstName = z.string().min(
2162
- fields.firstName.minLength,
2163
- `First name must be at least ${fields.firstName.minLength} characters`
2164
- );
2165
- } else {
2166
- schemaFields.firstName = z.string().optional();
2167
- }
2168
- if (fields.lastName.required) {
2169
- schemaFields.lastName = z.string().min(
2170
- fields.lastName.minLength,
2171
- `Last name must be at least ${fields.lastName.minLength} characters`
2172
- );
2173
- } else {
2174
- schemaFields.lastName = z.string().optional();
2175
- }
2176
- return z.object(schemaFields);
2177
- }
2178
- /**
2179
- * Validate registration data against settings
2180
- */
2181
- async validateRegistration(db, data) {
2182
- try {
2183
- const schema = await this.buildRegistrationSchema(db);
2184
- await schema.parseAsync(data);
2185
- return { valid: true, errors: [] };
2186
- } catch (error) {
2187
- if (error instanceof z.ZodError) {
2188
- return {
2189
- valid: false,
2190
- errors: error.errors.map((e) => e.message)
2191
- };
2192
- }
2193
- return {
2194
- valid: false,
2195
- errors: ["Validation failed"]
2196
- };
2197
- }
2198
- }
2199
- /**
2200
- * Clear cached settings (call after updating plugin settings)
2201
- */
2202
- clearCache() {
2203
- this.cachedSettings = null;
2204
- this.cacheExpiry = 0;
2205
- }
2206
- /**
2207
- * Get required field names for database insertion
2208
- */
2209
- async getRequiredFieldNames(db) {
2210
- const settings = await this.getAuthSettings(db);
2211
- const requiredFields = [];
2212
- Object.entries(settings.requiredFields).forEach(([key, config]) => {
2213
- if (config.required) {
2214
- requiredFields.push(key);
2215
- }
2216
- });
2217
- return requiredFields;
2218
- }
2219
- /**
2220
- * Generate auto-fill values for optional fields
2221
- */
2222
- generateDefaultValue(fieldName, data) {
2223
- switch (fieldName) {
2224
- case "username":
2225
- return data.email ? data.email.split("@")[0] : `user_${Date.now()}`;
2226
- case "firstName":
2227
- return data.firstName || "User";
2228
- case "lastName":
2229
- return data.lastName || "";
2230
- default:
2231
- return "";
2232
- }
2233
- }
2234
- };
2235
- var authValidationService = AuthValidationService.getInstance();
2033
+
2034
+ // src/services/auth-validation.ts
2035
+ var authValidationService = {};
2236
2036
 
2237
2037
  // src/routes/auth.ts
2238
2038
  var authRoutes = new Hono();
@@ -4899,6 +4699,9 @@ function escapeHtml3(text) {
4899
4699
  })[char] || char);
4900
4700
  }
4901
4701
 
4702
+ // src/middleware/plugin-middleware.ts
4703
+ var isPluginActive2 = () => false;
4704
+
4902
4705
  // src/routes/admin-content.ts
4903
4706
  var adminContentRoutes = new Hono();
4904
4707
  async function getCollectionFields(db, collectionId) {
@@ -5147,7 +4950,7 @@ adminContentRoutes.get("/new", async (c) => {
5147
4950
  return c.html(renderContentFormPage(formData2));
5148
4951
  }
5149
4952
  const fields = await getCollectionFields(db, collectionId);
5150
- const workflowEnabled = await isPluginActive(db, "workflow");
4953
+ const workflowEnabled = await isPluginActive2(db, "workflow");
5151
4954
  const formData = {
5152
4955
  collection,
5153
4956
  fields,
@@ -5219,7 +5022,7 @@ adminContentRoutes.get("/:id/edit", async (c) => {
5219
5022
  };
5220
5023
  const fields = await getCollectionFields(db, content.collection_id);
5221
5024
  const contentData = content.data ? JSON.parse(content.data) : {};
5222
- const workflowEnabled = await isPluginActive(db, "workflow");
5025
+ const workflowEnabled = await isPluginActive2(db, "workflow");
5223
5026
  const formData = {
5224
5027
  id: content.id,
5225
5028
  title: content.title,
@@ -15753,6 +15556,4230 @@ adminCodeExamplesRoutes.delete("/:id", async (c) => {
15753
15556
  });
15754
15557
  var admin_code_examples_default = adminCodeExamplesRoutes;
15755
15558
 
15559
+ // src/templates/pages/admin-dashboard.template.ts
15560
+ function renderDashboardPage(data) {
15561
+ const pageContent = `
15562
+ <div class="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between">
15563
+ <div>
15564
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Dashboard</h1>
15565
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Welcome to your SonicJS AI admin dashboard</p>
15566
+ </div>
15567
+ <div class="mt-4 sm:mt-0 flex items-center gap-x-3">
15568
+ <a href="/docs/getting-started" target="_blank" class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-lime-600 dark:bg-lime-700 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-lime-700 dark:hover:bg-lime-600 transition-colors shadow-sm">
15569
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
15570
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.436 60.436 0 00-.491 6.347A48.627 48.627 0 0112 20.904a48.627 48.627 0 018.232-4.41 60.46 60.46 0 00-.491-6.347m-15.482 0a50.57 50.57 0 00-2.658-.813A59.905 59.905 0 0112 3.493a59.902 59.902 0 0110.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.697 50.697 0 0112 13.489a50.702 50.702 0 017.74-3.342M6.75 15a.75.75 0 100-1.5.75.75 0 000 1.5zm0 0v-3.675A55.378 55.378 0 0112 8.443m-7.007 11.55A5.981 5.981 0 006.75 15.75v-1.5"/>
15571
+ </svg>
15572
+ Developer Docs
15573
+ </a>
15574
+ <a href="/admin/api-reference" class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm">
15575
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
15576
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"/>
15577
+ </svg>
15578
+ API Docs
15579
+ </a>
15580
+ <a href="/api" target="_blank" class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
15581
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
15582
+ <path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/>
15583
+ </svg>
15584
+ OpenAPI
15585
+ </a>
15586
+ </div>
15587
+ </div>
15588
+
15589
+ <!-- Stats Cards -->
15590
+ <div
15591
+ id="stats-container"
15592
+ class="mb-8"
15593
+ hx-get="/admin/dashboard/stats"
15594
+ hx-trigger="load"
15595
+ hx-swap="innerHTML"
15596
+ >
15597
+ ${renderStatsCardsSkeleton()}
15598
+ </div>
15599
+
15600
+ <!-- Dashboard Grid -->
15601
+ <div class="grid grid-cols-1 gap-6 xl:grid-cols-3 mb-8">
15602
+ <!-- Analytics Chart -->
15603
+ <div class="xl:col-span-2">
15604
+ ${renderAnalyticsChart()}
15605
+ </div>
15606
+
15607
+ <!-- Recent Activity -->
15608
+ <div
15609
+ class="xl:col-span-1"
15610
+ id="recent-activity-container"
15611
+ hx-get="/admin/dashboard/recent-activity"
15612
+ hx-trigger="load"
15613
+ hx-swap="innerHTML"
15614
+ >
15615
+ ${renderRecentActivitySkeleton()}
15616
+ </div>
15617
+ </div>
15618
+
15619
+ <!-- Secondary Grid -->
15620
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
15621
+ <!-- Quick Actions -->
15622
+ ${renderQuickActions()}
15623
+
15624
+ <!-- System Status -->
15625
+ ${renderSystemStatus()}
15626
+
15627
+ <!-- Storage Usage -->
15628
+ <div id="storage-usage-container" hx-get="/admin/dashboard/storage" hx-trigger="load" hx-swap="innerHTML">
15629
+ ${renderStorageUsage()}
15630
+ </div>
15631
+ </div>
15632
+
15633
+ <script>
15634
+ function refreshDashboard() {
15635
+ htmx.trigger('#stats-container', 'htmx:load');
15636
+ showNotification('Dashboard refreshed', 'success');
15637
+ }
15638
+ </script>
15639
+ `;
15640
+ const layoutData = {
15641
+ title: "Dashboard",
15642
+ pageTitle: "Dashboard",
15643
+ currentPath: "/admin",
15644
+ user: data.user,
15645
+ version: data.version,
15646
+ content: pageContent
15647
+ };
15648
+ return renderAdminLayout(layoutData);
15649
+ }
15650
+ function renderStatsCards(stats) {
15651
+ const cards = [
15652
+ {
15653
+ title: "Total Collections",
15654
+ value: stats.collections.toString(),
15655
+ change: "12.5",
15656
+ isPositive: true
15657
+ },
15658
+ {
15659
+ title: "Content Items",
15660
+ value: stats.contentItems.toString(),
15661
+ change: "8.2",
15662
+ isPositive: true
15663
+ },
15664
+ {
15665
+ title: "Media Files",
15666
+ value: stats.mediaFiles.toString(),
15667
+ change: "15.3",
15668
+ isPositive: true
15669
+ },
15670
+ {
15671
+ title: "Active Users",
15672
+ value: stats.users.toString(),
15673
+ change: "2.4",
15674
+ isPositive: false
15675
+ }
15676
+ ];
15677
+ const cardColors = ["text-cyan-400", "text-lime-400", "text-pink-400", "text-purple-400"];
15678
+ return `
15679
+ <div>
15680
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white">Last 30 days</h3>
15681
+ <dl class="mt-5 grid grid-cols-1 divide-zinc-950/5 dark:divide-white/10 overflow-hidden rounded-lg bg-zinc-800/75 dark:bg-zinc-800/75 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 md:grid-cols-4 md:divide-x md:divide-y-0">
15682
+ ${cards.map((card, index) => `
15683
+ <div class="px-4 py-5 sm:p-6">
15684
+ <dt class="text-base font-normal text-zinc-700 dark:text-zinc-100">${card.title}</dt>
15685
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
15686
+ <div class="flex items-baseline text-2xl font-semibold ${cardColors[index]}">
15687
+ ${card.value}
15688
+ </div>
15689
+ <div class="inline-flex items-baseline rounded-full ${card.isPositive ? "bg-lime-400/10 text-lime-600 dark:text-lime-400" : "bg-pink-400/10 text-pink-600 dark:text-pink-400"} px-2.5 py-0.5 text-sm font-medium md:mt-2 lg:mt-0">
15690
+ <svg viewBox="0 0 20 20" fill="currentColor" class="-ml-1 mr-0.5 size-5 shrink-0 self-center">
15691
+ ${card.isPositive ? '<path d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" fill-rule="evenodd" />' : '<path d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" fill-rule="evenodd" />'}
15692
+ </svg>
15693
+ <span class="sr-only">${card.isPositive ? "Increased" : "Decreased"} by</span>
15694
+ ${card.change}%
15695
+ </div>
15696
+ </dd>
15697
+ </div>
15698
+ `).join("")}
15699
+ </dl>
15700
+ </div>
15701
+ `;
15702
+ }
15703
+ function renderStatsCardsSkeleton() {
15704
+ return `
15705
+ <div>
15706
+ <div class="h-6 w-32 bg-zinc-200 dark:bg-zinc-700 rounded animate-pulse mb-5"></div>
15707
+ <div class="grid grid-cols-1 divide-zinc-950/5 dark:divide-white/10 overflow-hidden rounded-lg bg-zinc-800/75 dark:bg-zinc-800/75 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 md:grid-cols-4 md:divide-x md:divide-y-0">
15708
+ ${Array(4).fill(0).map(
15709
+ () => `
15710
+ <div class="px-4 py-5 sm:p-6 animate-pulse">
15711
+ <div class="h-4 w-24 bg-zinc-200 dark:bg-zinc-700 rounded mb-3"></div>
15712
+ <div class="h-8 w-16 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
15713
+ </div>
15714
+ `
15715
+ ).join("")}
15716
+ </div>
15717
+ </div>
15718
+ `;
15719
+ }
15720
+ function renderAnalyticsChart() {
15721
+ return `
15722
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
15723
+ <div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
15724
+ <div class="flex flex-wrap items-start justify-between gap-3 sm:flex-nowrap">
15725
+ <div>
15726
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Real-Time Analytics</h3>
15727
+ <p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Requests per second (live)</p>
15728
+ </div>
15729
+ <div class="flex items-center gap-2">
15730
+ <div class="h-2 w-2 rounded-full bg-lime-500 animate-pulse"></div>
15731
+ <span class="text-xs text-zinc-500 dark:text-zinc-400">Live</span>
15732
+ </div>
15733
+ </div>
15734
+ <div class="mt-4 flex items-baseline gap-2">
15735
+ <span id="current-rps" class="text-4xl font-bold text-cyan-500 dark:text-cyan-400">0</span>
15736
+ <span class="text-sm text-zinc-500 dark:text-zinc-400">req/s</span>
15737
+ </div>
15738
+ </div>
15739
+
15740
+ <div class="px-6 py-6">
15741
+ <canvas id="requestsChart" class="w-full" style="height: 300px;"></canvas>
15742
+ </div>
15743
+
15744
+ <!-- Hidden div to trigger HTMX polling -->
15745
+ <div
15746
+ hx-get="/admin/api/metrics"
15747
+ hx-trigger="every 1s"
15748
+ hx-swap="none"
15749
+ style="display: none;"
15750
+ ></div>
15751
+ </div>
15752
+
15753
+ <script>
15754
+ // Initialize Chart.js for Real-time Requests
15755
+ (function() {
15756
+ const ctx = document.getElementById('requestsChart');
15757
+ if (!ctx) return;
15758
+
15759
+ // Initialize with last 60 seconds of data (1 data point per second)
15760
+ const maxDataPoints = 60;
15761
+ const labels = [];
15762
+ const data = [];
15763
+
15764
+ for (let i = maxDataPoints - 1; i >= 0; i--) {
15765
+ labels.push(\`-\${i}s\`);
15766
+ data.push(0);
15767
+ }
15768
+
15769
+ const isDark = document.documentElement.classList.contains('dark');
15770
+
15771
+ const chart = new Chart(ctx, {
15772
+ type: 'line',
15773
+ data: {
15774
+ labels: labels,
15775
+ datasets: [{
15776
+ label: 'Requests/sec',
15777
+ data: data,
15778
+ borderColor: isDark ? 'rgb(34, 211, 238)' : 'rgb(6, 182, 212)',
15779
+ backgroundColor: isDark ? 'rgba(34, 211, 238, 0.1)' : 'rgba(6, 182, 212, 0.1)',
15780
+ borderWidth: 2,
15781
+ fill: true,
15782
+ tension: 0.4,
15783
+ pointRadius: 0,
15784
+ pointHoverRadius: 4,
15785
+ pointBackgroundColor: isDark ? 'rgb(34, 211, 238)' : 'rgb(6, 182, 212)',
15786
+ pointBorderColor: isDark ? 'rgb(17, 24, 39)' : 'rgb(255, 255, 255)',
15787
+ pointBorderWidth: 2
15788
+ }]
15789
+ },
15790
+ options: {
15791
+ responsive: true,
15792
+ maintainAspectRatio: false,
15793
+ plugins: {
15794
+ legend: {
15795
+ display: false
15796
+ },
15797
+ tooltip: {
15798
+ backgroundColor: isDark ? 'rgb(39, 39, 42)' : 'rgb(255, 255, 255)',
15799
+ titleColor: isDark ? 'rgb(255, 255, 255)' : 'rgb(9, 9, 11)',
15800
+ bodyColor: isDark ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)',
15801
+ borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(9, 9, 11, 0.05)',
15802
+ borderWidth: 1,
15803
+ padding: 12,
15804
+ displayColors: false,
15805
+ callbacks: {
15806
+ label: function(context) {
15807
+ return 'Requests/sec: ' + context.parsed.y.toFixed(2);
15808
+ }
15809
+ }
15810
+ }
15811
+ },
15812
+ scales: {
15813
+ y: {
15814
+ beginAtZero: true,
15815
+ border: {
15816
+ display: false
15817
+ },
15818
+ grid: {
15819
+ color: isDark ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
15820
+ drawBorder: false
15821
+ },
15822
+ ticks: {
15823
+ color: isDark ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)',
15824
+ padding: 8,
15825
+ callback: function(value) {
15826
+ return value.toFixed(1);
15827
+ }
15828
+ }
15829
+ },
15830
+ x: {
15831
+ border: {
15832
+ display: false
15833
+ },
15834
+ grid: {
15835
+ display: false
15836
+ },
15837
+ ticks: {
15838
+ color: isDark ? 'rgb(161, 161, 170)' : 'rgb(113, 113, 122)',
15839
+ padding: 8,
15840
+ maxTicksLimit: 6
15841
+ }
15842
+ }
15843
+ }
15844
+ }
15845
+ });
15846
+
15847
+ // Listen for metrics updates from HTMX
15848
+ window.addEventListener('htmx:afterRequest', function(event) {
15849
+ if (event.detail.pathInfo.requestPath === '/admin/api/metrics') {
15850
+ try {
15851
+ const metrics = JSON.parse(event.detail.xhr.responseText);
15852
+
15853
+ // Update current RPS display
15854
+ const rpsElement = document.getElementById('current-rps');
15855
+ if (rpsElement) {
15856
+ rpsElement.textContent = metrics.requestsPerSecond.toFixed(2);
15857
+ }
15858
+
15859
+ // Add new data point to chart
15860
+ chart.data.datasets[0].data.shift();
15861
+ chart.data.datasets[0].data.push(metrics.requestsPerSecond);
15862
+
15863
+ // Regenerate labels to maintain -60s to now format
15864
+ const newLabels = [];
15865
+ for (let i = maxDataPoints - 1; i >= 1; i--) {
15866
+ newLabels.push(\`-\${i}s\`);
15867
+ }
15868
+ newLabels.push('now');
15869
+ chart.data.labels = newLabels;
15870
+
15871
+ chart.update('none'); // Update without animation for smoother real-time updates
15872
+ } catch (e) {
15873
+ console.error('Error updating metrics:', e);
15874
+ }
15875
+ }
15876
+ });
15877
+ })();
15878
+ </script>
15879
+ `;
15880
+ }
15881
+ function renderRecentActivitySkeleton() {
15882
+ return `
15883
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 animate-pulse">
15884
+ <div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
15885
+ <div class="h-5 w-32 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
15886
+ </div>
15887
+ <div class="px-6 py-6">
15888
+ <div class="space-y-6">
15889
+ ${Array(3).fill(0).map(() => `
15890
+ <div class="flex gap-x-4">
15891
+ <div class="h-10 w-10 rounded-full bg-zinc-200 dark:bg-zinc-700"></div>
15892
+ <div class="flex-auto space-y-2">
15893
+ <div class="h-4 w-48 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
15894
+ <div class="h-3 w-32 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
15895
+ </div>
15896
+ </div>
15897
+ `).join("")}
15898
+ </div>
15899
+ </div>
15900
+ </div>
15901
+ `;
15902
+ }
15903
+ function renderRecentActivity(activities) {
15904
+ const getInitials = (user) => {
15905
+ const parts = user.split(" ").filter((p) => p.length > 0);
15906
+ if (parts.length >= 2) {
15907
+ const first = parts[0]?.[0] || "";
15908
+ const second = parts[1]?.[0] || "";
15909
+ return (first + second).toUpperCase();
15910
+ }
15911
+ return user.substring(0, 2).toUpperCase();
15912
+ };
15913
+ const getRelativeTime = (timestamp) => {
15914
+ const date = new Date(timestamp);
15915
+ const now = /* @__PURE__ */ new Date();
15916
+ const diffMs = now.getTime() - date.getTime();
15917
+ const diffMins = Math.floor(diffMs / 6e4);
15918
+ const diffHours = Math.floor(diffMins / 60);
15919
+ const diffDays = Math.floor(diffHours / 24);
15920
+ if (diffMins < 1) return "just now";
15921
+ if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
15922
+ if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
15923
+ return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
15924
+ };
15925
+ const getColorClasses = (type) => {
15926
+ switch (type) {
15927
+ case "content":
15928
+ return {
15929
+ bgColor: "bg-lime-500/10 dark:bg-lime-400/10",
15930
+ textColor: "text-lime-700 dark:text-lime-300"
15931
+ };
15932
+ case "media":
15933
+ return {
15934
+ bgColor: "bg-cyan-500/10 dark:bg-cyan-400/10",
15935
+ textColor: "text-cyan-700 dark:text-cyan-300"
15936
+ };
15937
+ case "user":
15938
+ return {
15939
+ bgColor: "bg-pink-500/10 dark:bg-pink-400/10",
15940
+ textColor: "text-pink-700 dark:text-pink-300"
15941
+ };
15942
+ case "collection":
15943
+ return {
15944
+ bgColor: "bg-purple-500/10 dark:bg-purple-400/10",
15945
+ textColor: "text-purple-700 dark:text-purple-300"
15946
+ };
15947
+ default:
15948
+ return {
15949
+ bgColor: "bg-gray-500/10 dark:bg-gray-400/10",
15950
+ textColor: "text-gray-700 dark:text-gray-300"
15951
+ };
15952
+ }
15953
+ };
15954
+ const formattedActivities = (activities || []).map((activity) => {
15955
+ const colors = getColorClasses(activity.type);
15956
+ return {
15957
+ ...activity,
15958
+ initials: getInitials(activity.user),
15959
+ time: getRelativeTime(activity.timestamp),
15960
+ ...colors
15961
+ };
15962
+ });
15963
+ if (formattedActivities.length === 0) {
15964
+ formattedActivities.push({
15965
+ type: "content",
15966
+ description: "No recent activity",
15967
+ user: "System",
15968
+ time: "",
15969
+ initials: "SY",
15970
+ bgColor: "bg-gray-500/10 dark:bg-gray-400/10",
15971
+ textColor: "text-gray-700 dark:text-gray-300",
15972
+ id: "0",
15973
+ action: "",
15974
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
15975
+ });
15976
+ }
15977
+ return `
15978
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
15979
+ <div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
15980
+ <div class="flex items-center justify-between">
15981
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Recent Activity</h3>
15982
+ <button class="text-xs/5 font-medium text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 transition-colors">
15983
+ View all
15984
+ </button>
15985
+ </div>
15986
+ </div>
15987
+
15988
+ <div class="px-6 py-6">
15989
+ <ul role="list" class="space-y-6">
15990
+ ${formattedActivities.map(
15991
+ (activity) => `
15992
+ <li class="relative flex gap-x-4">
15993
+ <div class="flex h-10 w-10 flex-none items-center justify-center rounded-full ${activity.bgColor}">
15994
+ <span class="text-xs font-semibold ${activity.textColor}">${activity.initials}</span>
15995
+ </div>
15996
+ <div class="flex-auto">
15997
+ <p class="text-sm/6 font-medium text-zinc-950 dark:text-white">${activity.description}</p>
15998
+ <p class="mt-1 text-xs/5 text-zinc-500 dark:text-zinc-400">
15999
+ <span class="font-medium text-zinc-950 dark:text-white">${activity.user}</span>
16000
+ <span class="text-zinc-400 dark:text-zinc-500"> \xB7 </span>
16001
+ ${activity.time}
16002
+ </p>
16003
+ </div>
16004
+ </li>
16005
+ `
16006
+ ).join("")}
16007
+ </ul>
16008
+ </div>
16009
+ </div>
16010
+ `;
16011
+ }
16012
+ function renderQuickActions() {
16013
+ const actions = [
16014
+ {
16015
+ title: "Create Content",
16016
+ description: "Add new blog post or page",
16017
+ href: "/admin/content/new",
16018
+ icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16019
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
16020
+ </svg>`
16021
+ },
16022
+ {
16023
+ title: "Upload Media",
16024
+ description: "Add images and files",
16025
+ href: "/admin/media",
16026
+ icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16027
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
16028
+ </svg>`
16029
+ },
16030
+ {
16031
+ title: "Manage Users",
16032
+ description: "Add or edit user accounts",
16033
+ href: "/admin/users",
16034
+ icon: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16035
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
16036
+ </svg>`
16037
+ }
16038
+ ];
16039
+ return `
16040
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
16041
+ <div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
16042
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Quick Actions</h3>
16043
+ </div>
16044
+
16045
+ <div class="p-6">
16046
+ <div class="space-y-2">
16047
+ ${actions.map(
16048
+ (action) => `
16049
+ <a href="${action.href}" class="group flex items-center gap-x-3 rounded-lg px-3 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors">
16050
+ <div class="flex h-10 w-10 flex-none items-center justify-center text-zinc-400 dark:text-zinc-500 group-hover:text-zinc-600 dark:group-hover:text-zinc-400">
16051
+ ${action.icon}
16052
+ </div>
16053
+ <div class="flex-auto">
16054
+ <p class="text-sm/6 font-medium text-zinc-950 dark:text-white">${action.title}</p>
16055
+ <p class="text-xs/5 text-zinc-500 dark:text-zinc-400">${action.description}</p>
16056
+ </div>
16057
+ <svg class="h-5 w-5 flex-none text-zinc-400 dark:text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16058
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
16059
+ </svg>
16060
+ </a>
16061
+ `
16062
+ ).join("")}
16063
+ </div>
16064
+ </div>
16065
+ </div>
16066
+ `;
16067
+ }
16068
+ function renderSystemStatus() {
16069
+ return `
16070
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 overflow-hidden">
16071
+ <div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
16072
+ <div class="flex items-center justify-between">
16073
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">System Status</h3>
16074
+ <div class="flex items-center gap-2">
16075
+ <div class="h-2 w-2 rounded-full bg-lime-500 animate-pulse"></div>
16076
+ <span class="text-xs text-zinc-500 dark:text-zinc-400">Live</span>
16077
+ </div>
16078
+ </div>
16079
+ </div>
16080
+
16081
+ <div
16082
+ id="system-status-container"
16083
+ class="p-6"
16084
+ hx-get="/admin/dashboard/system-status"
16085
+ hx-trigger="load, every 30s"
16086
+ hx-swap="innerHTML"
16087
+ >
16088
+ <!-- Loading skeleton with gradient -->
16089
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
16090
+ ${[
16091
+ { color: "from-blue-500/20 to-cyan-500/20", darkColor: "dark:from-blue-500/10 dark:to-cyan-500/10" },
16092
+ { color: "from-purple-500/20 to-pink-500/20", darkColor: "dark:from-purple-500/10 dark:to-pink-500/10" },
16093
+ { color: "from-amber-500/20 to-orange-500/20", darkColor: "dark:from-amber-500/10 dark:to-orange-500/10" },
16094
+ { color: "from-lime-500/20 to-emerald-500/20", darkColor: "dark:from-lime-500/10 dark:to-emerald-500/10" }
16095
+ ].map((gradient, i) => `
16096
+ <div class="relative group">
16097
+ <div class="absolute inset-0 bg-gradient-to-br ${gradient.color} ${gradient.darkColor} rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
16098
+ <div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50 animate-pulse">
16099
+ <div class="flex items-center justify-between mb-3">
16100
+ <div class="h-4 w-24 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
16101
+ <div class="h-6 w-6 bg-zinc-200 dark:bg-zinc-700 rounded-full"></div>
16102
+ </div>
16103
+ <div class="h-3 w-20 bg-zinc-200 dark:bg-zinc-700 rounded"></div>
16104
+ </div>
16105
+ </div>
16106
+ `).join("")}
16107
+ </div>
16108
+ </div>
16109
+ </div>
16110
+
16111
+ <style>
16112
+ @keyframes ping-slow {
16113
+ 0%, 100% { opacity: 1; }
16114
+ 50% { opacity: 0.5; }
16115
+ }
16116
+ .animate-ping-slow {
16117
+ animation: ping-slow 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
16118
+ }
16119
+ </style>
16120
+ `;
16121
+ }
16122
+ function renderStorageUsage(databaseSizeBytes, mediaSizeBytes) {
16123
+ const formatBytes = (bytes) => {
16124
+ if (bytes === 0) return "0 B";
16125
+ const k = 1024;
16126
+ const sizes = ["B", "KB", "MB", "GB"];
16127
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
16128
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
16129
+ };
16130
+ const dbSizeGB = databaseSizeBytes ? databaseSizeBytes / 1024 ** 3 : 0;
16131
+ const dbMaxGB = 10;
16132
+ const dbPercentageRaw = dbSizeGB / dbMaxGB * 100;
16133
+ const dbPercentage = Math.min(Math.max(dbPercentageRaw, 0.5), 100);
16134
+ const dbUsedFormatted = databaseSizeBytes ? formatBytes(databaseSizeBytes) : "Unknown";
16135
+ const mediaUsedFormatted = mediaSizeBytes ? formatBytes(mediaSizeBytes) : "0 B";
16136
+ const storageItems = [
16137
+ {
16138
+ label: "Database",
16139
+ used: dbUsedFormatted,
16140
+ total: "10 GB",
16141
+ percentage: dbPercentage,
16142
+ color: dbPercentage > 80 ? "bg-red-500 dark:bg-red-400" : dbPercentage > 60 ? "bg-amber-500 dark:bg-amber-400" : "bg-cyan-500 dark:bg-cyan-400"
16143
+ },
16144
+ {
16145
+ label: "Media Files",
16146
+ used: mediaUsedFormatted,
16147
+ total: "\u221E",
16148
+ percentage: 0,
16149
+ color: "bg-lime-500 dark:bg-lime-400",
16150
+ note: "Stored in R2"
16151
+ },
16152
+ {
16153
+ label: "Cache (KV)",
16154
+ used: "N/A",
16155
+ total: "\u221E",
16156
+ percentage: 0,
16157
+ color: "bg-purple-500 dark:bg-purple-400",
16158
+ note: "Unlimited"
16159
+ }
16160
+ ];
16161
+ return `
16162
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
16163
+ <div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
16164
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Storage Usage</h3>
16165
+ </div>
16166
+
16167
+ <div class="px-6 py-6">
16168
+ <dl class="space-y-6">
16169
+ ${storageItems.map(
16170
+ (item) => `
16171
+ <div>
16172
+ <div class="flex items-center justify-between mb-2">
16173
+ <dt class="text-sm/6 text-zinc-500 dark:text-zinc-400">
16174
+ ${item.label}
16175
+ ${item.note ? `<span class="ml-2 text-xs text-zinc-400 dark:text-zinc-500">(${item.note})</span>` : ""}
16176
+ </dt>
16177
+ <dd class="text-sm/6 font-medium text-zinc-950 dark:text-white">${item.used} / ${item.total}</dd>
16178
+ </div>
16179
+ <div class="w-full bg-zinc-100 dark:bg-zinc-800 rounded-full h-1.5 overflow-hidden">
16180
+ <div class="${item.color} h-full rounded-full transition-all duration-300" style="width: ${item.percentage}%"></div>
16181
+ </div>
16182
+ </div>
16183
+ `
16184
+ ).join("")}
16185
+ </dl>
16186
+ </div>
16187
+ </div>
16188
+ `;
16189
+ }
16190
+
16191
+ // src/routes/admin-dashboard.ts
16192
+ var VERSION = getCoreVersion();
16193
+ var router = new Hono();
16194
+ router.use("*", requireAuth());
16195
+ router.get("/", async (c) => {
16196
+ const user = c.get("user");
16197
+ try {
16198
+ const pageData = {
16199
+ user: {
16200
+ name: user.email.split("@")[0] || user.email,
16201
+ email: user.email,
16202
+ role: user.role
16203
+ },
16204
+ version: VERSION
16205
+ };
16206
+ return c.html(renderDashboardPage(pageData));
16207
+ } catch (error) {
16208
+ console.error("Dashboard error:", error);
16209
+ const pageData = {
16210
+ user: {
16211
+ name: user.email,
16212
+ email: user.email,
16213
+ role: user.role
16214
+ },
16215
+ version: VERSION
16216
+ };
16217
+ return c.html(renderDashboardPage(pageData));
16218
+ }
16219
+ });
16220
+ router.get("/dashboard/stats", async (c) => {
16221
+ try {
16222
+ const db = c.env.DB;
16223
+ let collectionsCount = 0;
16224
+ try {
16225
+ const collectionsStmt = db.prepare("SELECT COUNT(*) as count FROM collections WHERE is_active = 1");
16226
+ const collectionsResult = await collectionsStmt.first();
16227
+ collectionsCount = collectionsResult?.count || 0;
16228
+ } catch (error) {
16229
+ console.error("Error fetching collections count:", error);
16230
+ }
16231
+ let contentCount = 0;
16232
+ try {
16233
+ const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE deleted_at IS NULL");
16234
+ const contentResult = await contentStmt.first();
16235
+ contentCount = contentResult?.count || 0;
16236
+ } catch (error) {
16237
+ console.error("Error fetching content count:", error);
16238
+ }
16239
+ let mediaCount = 0;
16240
+ let mediaSize = 0;
16241
+ try {
16242
+ const mediaStmt = db.prepare("SELECT COUNT(*) as count, COALESCE(SUM(size), 0) as total_size FROM media WHERE deleted_at IS NULL");
16243
+ const mediaResult = await mediaStmt.first();
16244
+ mediaCount = mediaResult?.count || 0;
16245
+ mediaSize = mediaResult?.total_size || 0;
16246
+ } catch (error) {
16247
+ console.error("Error fetching media count:", error);
16248
+ }
16249
+ let usersCount = 0;
16250
+ try {
16251
+ const usersStmt = db.prepare("SELECT COUNT(*) as count FROM users WHERE is_active = 1");
16252
+ const usersResult = await usersStmt.first();
16253
+ usersCount = usersResult?.count || 0;
16254
+ } catch (error) {
16255
+ console.error("Error fetching users count:", error);
16256
+ }
16257
+ const html9 = renderStatsCards({
16258
+ collections: collectionsCount,
16259
+ contentItems: contentCount,
16260
+ mediaFiles: mediaCount,
16261
+ users: usersCount,
16262
+ mediaSize
16263
+ });
16264
+ return c.html(html9);
16265
+ } catch (error) {
16266
+ console.error("Error fetching stats:", error);
16267
+ return c.html('<div class="text-red-500">Failed to load statistics</div>');
16268
+ }
16269
+ });
16270
+ router.get("/dashboard/storage", async (c) => {
16271
+ try {
16272
+ const db = c.env.DB;
16273
+ let databaseSize = 0;
16274
+ try {
16275
+ const result = await db.prepare("SELECT 1").run();
16276
+ databaseSize = result?.meta?.size_after || 0;
16277
+ } catch (error) {
16278
+ console.error("Error fetching database size:", error);
16279
+ }
16280
+ let mediaSize = 0;
16281
+ try {
16282
+ const mediaStmt = db.prepare("SELECT COALESCE(SUM(size), 0) as total_size FROM media WHERE deleted_at IS NULL");
16283
+ const mediaResult = await mediaStmt.first();
16284
+ mediaSize = mediaResult?.total_size || 0;
16285
+ } catch (error) {
16286
+ console.error("Error fetching media size:", error);
16287
+ }
16288
+ const html9 = renderStorageUsage(databaseSize, mediaSize);
16289
+ return c.html(html9);
16290
+ } catch (error) {
16291
+ console.error("Error fetching storage usage:", error);
16292
+ return c.html('<div class="text-red-500">Failed to load storage information</div>');
16293
+ }
16294
+ });
16295
+ router.get("/dashboard/recent-activity", async (c) => {
16296
+ try {
16297
+ const db = c.env.DB;
16298
+ const limit = parseInt(c.req.query("limit") || "5");
16299
+ const activityStmt = db.prepare(`
16300
+ SELECT
16301
+ a.id,
16302
+ a.action,
16303
+ a.resource_type,
16304
+ a.resource_id,
16305
+ a.details,
16306
+ a.created_at,
16307
+ u.email,
16308
+ u.first_name,
16309
+ u.last_name
16310
+ FROM activity_logs a
16311
+ LEFT JOIN users u ON a.user_id = u.id
16312
+ WHERE a.resource_type IN ('content', 'collections', 'users', 'media')
16313
+ ORDER BY a.created_at DESC
16314
+ LIMIT ?
16315
+ `);
16316
+ const { results } = await activityStmt.bind(limit).all();
16317
+ const activities = (results || []).map((row) => {
16318
+ const userName = row.first_name && row.last_name ? `${row.first_name} ${row.last_name}` : row.email || "System";
16319
+ let description = "";
16320
+ if (row.action === "create") {
16321
+ description = `Created new ${row.resource_type}`;
16322
+ } else if (row.action === "update") {
16323
+ description = `Updated ${row.resource_type}`;
16324
+ } else if (row.action === "delete") {
16325
+ description = `Deleted ${row.resource_type}`;
16326
+ } else {
16327
+ description = `${row.action} ${row.resource_type}`;
16328
+ }
16329
+ return {
16330
+ id: row.id,
16331
+ type: row.resource_type,
16332
+ action: row.action,
16333
+ description,
16334
+ timestamp: new Date(Number(row.created_at)).toISOString(),
16335
+ user: userName
16336
+ };
16337
+ });
16338
+ const html9 = renderRecentActivity(activities);
16339
+ return c.html(html9);
16340
+ } catch (error) {
16341
+ console.error("Error fetching recent activity:", error);
16342
+ const html9 = renderRecentActivity([]);
16343
+ return c.html(html9);
16344
+ }
16345
+ });
16346
+ router.get("/api/metrics", async (c) => {
16347
+ try {
16348
+ const { metricsTracker } = await import('../utils/metrics-tracker');
16349
+ return c.json({
16350
+ requestsPerSecond: metricsTracker.getRequestsPerSecond(),
16351
+ totalRequests: metricsTracker.getTotalRequests(),
16352
+ averageRPS: metricsTracker.getAverageRPS(),
16353
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16354
+ });
16355
+ } catch (error) {
16356
+ console.error("Error fetching metrics:", error);
16357
+ return c.json({
16358
+ requestsPerSecond: 0,
16359
+ totalRequests: 0,
16360
+ averageRPS: 0,
16361
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16362
+ });
16363
+ }
16364
+ });
16365
+ router.get("/dashboard/system-status", async (c) => {
16366
+ try {
16367
+ const html9 = `
16368
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
16369
+ <div class="relative group">
16370
+ <div class="absolute inset-0 bg-gradient-to-br from-blue-500/20 to-cyan-500/20 dark:from-blue-500/10 dark:to-cyan-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
16371
+ <div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
16372
+ <div class="flex items-center justify-between mb-3">
16373
+ <span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">API Status</span>
16374
+ <svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
16375
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
16376
+ </svg>
16377
+ </div>
16378
+ <p class="text-xs text-zinc-500 dark:text-zinc-400">Operational</p>
16379
+ </div>
16380
+ </div>
16381
+
16382
+ <div class="relative group">
16383
+ <div class="absolute inset-0 bg-gradient-to-br from-purple-500/20 to-pink-500/20 dark:from-purple-500/10 dark:to-pink-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
16384
+ <div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
16385
+ <div class="flex items-center justify-between mb-3">
16386
+ <span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">Database</span>
16387
+ <svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
16388
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
16389
+ </svg>
16390
+ </div>
16391
+ <p class="text-xs text-zinc-500 dark:text-zinc-400">Connected</p>
16392
+ </div>
16393
+ </div>
16394
+
16395
+ <div class="relative group">
16396
+ <div class="absolute inset-0 bg-gradient-to-br from-amber-500/20 to-orange-500/20 dark:from-amber-500/10 dark:to-orange-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
16397
+ <div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
16398
+ <div class="flex items-center justify-between mb-3">
16399
+ <span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">R2 Storage</span>
16400
+ <svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
16401
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
16402
+ </svg>
16403
+ </div>
16404
+ <p class="text-xs text-zinc-500 dark:text-zinc-400">Available</p>
16405
+ </div>
16406
+ </div>
16407
+
16408
+ <div class="relative group">
16409
+ <div class="absolute inset-0 bg-gradient-to-br from-lime-500/20 to-emerald-500/20 dark:from-lime-500/10 dark:to-emerald-500/10 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
16410
+ <div class="relative bg-zinc-50 dark:bg-zinc-800/50 rounded-xl p-5 border border-zinc-200/50 dark:border-zinc-700/50">
16411
+ <div class="flex items-center justify-between mb-3">
16412
+ <span class="text-sm font-medium text-zinc-600 dark:text-zinc-400">KV Cache</span>
16413
+ <svg class="w-6 h-6 text-lime-500" fill="currentColor" viewBox="0 0 20 20">
16414
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
16415
+ </svg>
16416
+ </div>
16417
+ <p class="text-xs text-zinc-500 dark:text-zinc-400">Ready</p>
16418
+ </div>
16419
+ </div>
16420
+ </div>
16421
+ `;
16422
+ return c.html(html9);
16423
+ } catch (error) {
16424
+ console.error("Error fetching system status:", error);
16425
+ return c.html('<div class="text-red-500">Failed to load system status</div>');
16426
+ }
16427
+ });
16428
+
16429
+ // src/templates/pages/admin-collections-list.template.ts
16430
+ init_admin_layout_catalyst_template();
16431
+
16432
+ // src/templates/components/table.template.ts
16433
+ function renderTable2(data) {
16434
+ const tableId = data.tableId || `table-${Math.random().toString(36).substr(2, 9)}`;
16435
+ if (data.rows.length === 0) {
16436
+ return `
16437
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-8 text-center">
16438
+ <div class="text-zinc-500 dark:text-zinc-400">
16439
+ <svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
16440
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
16441
+ </svg>
16442
+ <p class="mt-2 text-sm text-zinc-500 dark:text-zinc-400">${data.emptyMessage || "No data available"}</p>
16443
+ </div>
16444
+ </div>
16445
+ `;
16446
+ }
16447
+ return `
16448
+ <div class="${data.className || ""}" id="${tableId}">
16449
+ ${data.title ? `
16450
+ <div class="px-4 sm:px-0 mb-4">
16451
+ <h3 class="text-base font-semibold text-zinc-950 dark:text-white">${data.title}</h3>
16452
+ </div>
16453
+ ` : ""}
16454
+ <div class="overflow-x-auto">
16455
+ <table class="min-w-full sortable-table">
16456
+ <thead>
16457
+ <tr>
16458
+ ${data.selectable ? `
16459
+ <th class="px-4 py-3.5 text-center sm:pl-0">
16460
+ <div class="flex items-center justify-center">
16461
+ <div class="group grid size-4 grid-cols-1">
16462
+ <input type="checkbox" id="select-all-${tableId}" class="col-start-1 row-start-1 appearance-none rounded border border-white/10 bg-white/5 checked:border-cyan-500 checked:bg-cyan-500 indeterminate:border-cyan-500 indeterminate:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-500 disabled:border-white/5 disabled:bg-white/10 disabled:checked:bg-white/10 forced-colors:appearance-auto row-checkbox" />
16463
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-white/25">
16464
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
16465
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
16466
+ </svg>
16467
+ </div>
16468
+ </div>
16469
+ </th>
16470
+ ` : ""}
16471
+ ${data.columns.map((column, index) => {
16472
+ const isFirst = index === 0 && !data.selectable;
16473
+ const isLast = index === data.columns.length - 1;
16474
+ return `
16475
+ <th class="px-4 py-3.5 text-left text-sm font-semibold text-zinc-950 dark:text-white ${isFirst ? "sm:pl-0" : ""} ${isLast ? "sm:pr-0" : ""} ${column.className || ""}">
16476
+ ${column.sortable ? `
16477
+ <button
16478
+ class="flex items-center gap-x-2 hover:text-zinc-700 dark:hover:text-zinc-300 transition-colors sort-btn text-left"
16479
+ data-column="${column.key}"
16480
+ data-sort-type="${column.sortType || "string"}"
16481
+ data-sort-direction="none"
16482
+ onclick="sortTable('${tableId}', '${column.key}', '${column.sortType || "string"}')"
16483
+ >
16484
+ <span>${column.label}</span>
16485
+ <div class="sort-icons flex flex-col">
16486
+ <svg class="w-3 h-3 sort-up opacity-30" fill="currentColor" viewBox="0 0 20 20">
16487
+ <path fill-rule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clip-rule="evenodd" />
16488
+ </svg>
16489
+ <svg class="w-3 h-3 sort-down opacity-30 -mt-1" fill="currentColor" viewBox="0 0 20 20">
16490
+ <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
16491
+ </svg>
16492
+ </div>
16493
+ </button>
16494
+ ` : column.label}
16495
+ </th>
16496
+ `;
16497
+ }).join("")}
16498
+ </tr>
16499
+ </thead>
16500
+ <tbody>
16501
+ ${data.rows.map((row, rowIndex) => {
16502
+ if (!row) return "";
16503
+ const clickableClass = data.rowClickable ? "cursor-pointer" : "";
16504
+ const clickHandler = data.rowClickable && data.rowClickUrl ? `onclick="window.location.href='${data.rowClickUrl(row)}'"` : "";
16505
+ return `
16506
+ <tr class="group border-t border-zinc-950/5 dark:border-white/5 hover:bg-gradient-to-r hover:from-cyan-50/50 hover:via-blue-50/30 hover:to-purple-50/50 dark:hover:from-cyan-900/20 dark:hover:via-blue-900/10 dark:hover:to-purple-900/20 hover:shadow-sm hover:shadow-cyan-500/5 dark:hover:shadow-cyan-400/5 transition-all duration-300 ${clickableClass}" ${clickHandler}>
16507
+ ${data.selectable ? `
16508
+ <td class="px-4 py-4 sm:pl-0" onclick="event.stopPropagation()">
16509
+ <div class="flex items-center justify-center">
16510
+ <div class="group grid size-4 grid-cols-1">
16511
+ <input type="checkbox" value="${row.id || ""}" class="col-start-1 row-start-1 appearance-none rounded border border-white/10 bg-white/5 checked:border-cyan-500 checked:bg-cyan-500 indeterminate:border-cyan-500 indeterminate:bg-cyan-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-500 disabled:border-white/5 disabled:bg-white/10 disabled:checked:bg-white/10 forced-colors:appearance-auto row-checkbox" />
16512
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-white/25">
16513
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
16514
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
16515
+ </svg>
16516
+ </div>
16517
+ </div>
16518
+ </td>
16519
+ ` : ""}
16520
+ ${data.columns.map((column, colIndex) => {
16521
+ const value = row[column.key];
16522
+ const displayValue = column.render ? column.render(value, row) : value;
16523
+ const stopPropagation = column.key === "actions" ? 'onclick="event.stopPropagation()"' : "";
16524
+ const isFirst = colIndex === 0 && !data.selectable;
16525
+ const isLast = colIndex === data.columns.length - 1;
16526
+ return `
16527
+ <td class="px-4 py-4 text-sm text-zinc-500 dark:text-zinc-400 ${isFirst ? "sm:pl-0 font-medium text-zinc-950 dark:text-white" : ""} ${isLast ? "sm:pr-0" : ""} ${column.className || ""}" ${stopPropagation}>
16528
+ ${displayValue || ""}
16529
+ </td>
16530
+ `;
16531
+ }).join("")}
16532
+ </tr>
16533
+ `;
16534
+ }).join("")}
16535
+ </tbody>
16536
+ </table>
16537
+ </div>
16538
+
16539
+ <script>
16540
+ // Table sorting functionality
16541
+ window.sortTable = function(tableId, column, sortType) {
16542
+ const tableContainer = document.getElementById(tableId);
16543
+ const table = tableContainer.querySelector('.sortable-table');
16544
+ const tbody = table.querySelector('tbody');
16545
+ const rows = Array.from(tbody.querySelectorAll('tr'));
16546
+ const headerBtn = table.querySelector(\`[data-column="\${column}"]\`);
16547
+
16548
+ // Get current sort direction
16549
+ let direction = headerBtn.getAttribute('data-sort-direction');
16550
+
16551
+ // Reset all sort indicators
16552
+ table.querySelectorAll('.sort-btn').forEach(btn => {
16553
+ btn.setAttribute('data-sort-direction', 'none');
16554
+ btn.querySelectorAll('.sort-up, .sort-down').forEach(icon => {
16555
+ icon.classList.add('opacity-30');
16556
+ icon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
16557
+ });
16558
+ });
16559
+
16560
+ // Determine new direction
16561
+ if (direction === 'none' || direction === 'desc') {
16562
+ direction = 'asc';
16563
+ } else {
16564
+ direction = 'desc';
16565
+ }
16566
+
16567
+ // Update current header
16568
+ headerBtn.setAttribute('data-sort-direction', direction);
16569
+ const upIcon = headerBtn.querySelector('.sort-up');
16570
+ const downIcon = headerBtn.querySelector('.sort-down');
16571
+
16572
+ if (direction === 'asc') {
16573
+ upIcon.classList.remove('opacity-30');
16574
+ upIcon.classList.add('opacity-100', 'text-zinc-950', 'dark:text-white');
16575
+ downIcon.classList.add('opacity-30');
16576
+ downIcon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
16577
+ } else {
16578
+ downIcon.classList.remove('opacity-30');
16579
+ downIcon.classList.add('opacity-100', 'text-zinc-950', 'dark:text-white');
16580
+ upIcon.classList.add('opacity-30');
16581
+ upIcon.classList.remove('opacity-100', 'text-zinc-950', 'dark:text-white');
16582
+ }
16583
+
16584
+ // Find column index (accounting for potential select column)
16585
+ const headers = Array.from(table.querySelectorAll('th'));
16586
+ const selectableOffset = table.querySelector('input[id^="select-all"]') ? 1 : 0;
16587
+ const columnIndex = headers.findIndex(th => th.querySelector(\`[data-column="\${column}"]\`)) - selectableOffset;
16588
+
16589
+ // Sort rows
16590
+ rows.sort((a, b) => {
16591
+ const aCell = a.children[columnIndex + selectableOffset];
16592
+ const bCell = b.children[columnIndex + selectableOffset];
16593
+
16594
+ if (!aCell || !bCell) return 0;
16595
+
16596
+ let aValue = aCell.textContent.trim();
16597
+ let bValue = bCell.textContent.trim();
16598
+
16599
+ // Handle different sort types
16600
+ switch (sortType) {
16601
+ case 'number':
16602
+ aValue = parseFloat(aValue.replace(/[^0-9.-]/g, '')) || 0;
16603
+ bValue = parseFloat(bValue.replace(/[^0-9.-]/g, '')) || 0;
16604
+ break;
16605
+ case 'date':
16606
+ aValue = new Date(aValue).getTime() || 0;
16607
+ bValue = new Date(bValue).getTime() || 0;
16608
+ break;
16609
+ case 'boolean':
16610
+ aValue = aValue.toLowerCase() === 'true' || aValue.toLowerCase() === 'published' || aValue.toLowerCase() === 'active';
16611
+ bValue = bValue.toLowerCase() === 'true' || bValue.toLowerCase() === 'published' || bValue.toLowerCase() === 'active';
16612
+ break;
16613
+ default: // string
16614
+ aValue = aValue.toLowerCase();
16615
+ bValue = bValue.toLowerCase();
16616
+ }
16617
+
16618
+ if (aValue < bValue) return direction === 'asc' ? -1 : 1;
16619
+ if (aValue > bValue) return direction === 'asc' ? 1 : -1;
16620
+ return 0;
16621
+ });
16622
+
16623
+ // Re-append sorted rows
16624
+ rows.forEach(row => tbody.appendChild(row));
16625
+ };
16626
+
16627
+ // Select all functionality
16628
+ document.addEventListener('DOMContentLoaded', function() {
16629
+ document.querySelectorAll('[id^="select-all"]').forEach(selectAll => {
16630
+ selectAll.addEventListener('change', function() {
16631
+ const tableId = this.id.replace('select-all-', '');
16632
+ const table = document.getElementById(tableId);
16633
+ if (table) {
16634
+ const checkboxes = table.querySelectorAll('.row-checkbox');
16635
+ checkboxes.forEach(checkbox => {
16636
+ checkbox.checked = this.checked;
16637
+ });
16638
+ }
16639
+ });
16640
+ });
16641
+ });
16642
+ </script>
16643
+ </div>
16644
+ `;
16645
+ }
16646
+
16647
+ // src/templates/pages/admin-collections-list.template.ts
16648
+ function renderCollectionsListPage(data) {
16649
+ const tableData = {
16650
+ tableId: "collections-table",
16651
+ rowClickable: true,
16652
+ rowClickUrl: (collection) => `/admin/collections/${collection.id}`,
16653
+ columns: [
16654
+ {
16655
+ key: "name",
16656
+ label: "Name",
16657
+ sortable: true,
16658
+ sortType: "string",
16659
+ render: (value, collection) => `
16660
+ <div class="flex items-center gap-2 ml-2">
16661
+ <span class="inline-flex items-center rounded-md bg-cyan-50 dark:bg-cyan-500/10 px-2.5 py-1 text-sm font-medium text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-700/10 dark:ring-cyan-400/20">
16662
+ ${collection.name}
16663
+ </span>
16664
+ ${collection.managed ? `
16665
+ <span class="inline-flex items-center rounded-md bg-purple-50 dark:bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300 ring-1 ring-inset ring-purple-700/10 dark:ring-purple-400/20" title="Config-managed collection (read-only in UI)">
16666
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
16667
+ <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
16668
+ </svg>
16669
+ Config
16670
+ </span>
16671
+ ` : ""}
16672
+ </div>
16673
+ `
16674
+ },
16675
+ {
16676
+ key: "display_name",
16677
+ label: "Display Name",
16678
+ sortable: true,
16679
+ sortType: "string"
16680
+ },
16681
+ {
16682
+ key: "description",
16683
+ label: "Description",
16684
+ sortable: true,
16685
+ sortType: "string",
16686
+ render: (value, collection) => collection.description || '<span class="text-zinc-500 dark:text-zinc-400">-</span>'
16687
+ },
16688
+ {
16689
+ key: "field_count",
16690
+ label: "Fields",
16691
+ sortable: true,
16692
+ sortType: "number",
16693
+ render: (value, collection) => {
16694
+ const count = collection.field_count || 0;
16695
+ return `
16696
+ <div class="flex items-center">
16697
+ <span class="inline-flex items-center rounded-md bg-pink-50 dark:bg-pink-500/10 px-2.5 py-1 text-sm font-medium text-pink-700 dark:text-pink-300 ring-1 ring-inset ring-pink-700/10 dark:ring-pink-400/20">
16698
+ ${count} ${count === 1 ? "field" : "fields"}
16699
+ </span>
16700
+ </div>
16701
+ `;
16702
+ }
16703
+ },
16704
+ {
16705
+ key: "managed",
16706
+ label: "Source",
16707
+ sortable: true,
16708
+ sortType: "string",
16709
+ render: (value, collection) => {
16710
+ if (collection.managed) {
16711
+ return `
16712
+ <div class="flex items-center gap-1.5">
16713
+ <svg class="w-4 h-4 text-purple-600 dark:text-purple-400" fill="currentColor" viewBox="0 0 20 20">
16714
+ <path fill-rule="evenodd" d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"/>
16715
+ </svg>
16716
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Code</span>
16717
+ </div>
16718
+ `;
16719
+ } else {
16720
+ return `
16721
+ <div class="flex items-center gap-1.5">
16722
+ <svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
16723
+ <path d="M3 12v3c0 1.657 3.134 3 7 3s7-1.343 7-3v-3c0 1.657-3.134 3-7 3s-7-1.343-7-3z"/>
16724
+ <path d="M3 7v3c0 1.657 3.134 3 7 3s7-1.343 7-3V7c0 1.657-3.134 3-7 3S3 8.657 3 7z"/>
16725
+ <path d="M17 5c0 1.657-3.134 3-7 3S3 6.657 3 5s3.134-3 7-3 7 1.343 7 3z"/>
16726
+ </svg>
16727
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Database</span>
16728
+ </div>
16729
+ `;
16730
+ }
16731
+ }
16732
+ },
16733
+ {
16734
+ key: "formattedDate",
16735
+ label: "Created",
16736
+ sortable: true,
16737
+ sortType: "date"
16738
+ },
16739
+ {
16740
+ key: "actions",
16741
+ label: "Content",
16742
+ sortable: false,
16743
+ render: (value, collection) => {
16744
+ if (!collection || !collection.id) return '<span class="text-zinc-500 dark:text-zinc-400">-</span>';
16745
+ return `
16746
+ <div class="flex items-center space-x-2">
16747
+ <a href="/admin/content?model=${collection.name}" class="inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-lg bg-zinc-950 dark:bg-white text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors">
16748
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16749
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
16750
+ </svg>
16751
+ </a>
16752
+ </div>
16753
+ `;
16754
+ }
16755
+ }
16756
+ ],
16757
+ rows: data.collections,
16758
+ emptyMessage: "No collections found."
16759
+ };
16760
+ const pageContent = `
16761
+ <div>
16762
+ <!-- Header -->
16763
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
16764
+ <div>
16765
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Collections</h1>
16766
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage your content collections and their schemas</p>
16767
+ </div>
16768
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
16769
+ <a href="/admin/collections/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
16770
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
16771
+ <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
16772
+ </svg>
16773
+ New Collection
16774
+ </a>
16775
+ </div>
16776
+ </div>
16777
+
16778
+ <!-- Filters -->
16779
+ <div class="relative rounded-xl overflow-hidden mb-6">
16780
+ <!-- Gradient Background -->
16781
+ <div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/10 to-purple-500/10 dark:from-cyan-400/20 dark:via-blue-400/20 dark:to-purple-400/20"></div>
16782
+
16783
+ <div class="relative bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
16784
+ <div class="px-6 py-5">
16785
+ <div class="flex items-center justify-between">
16786
+ <div class="flex items-center space-x-4">
16787
+ <form onsubmit="performSearch(event)" class="flex items-center space-x-2">
16788
+ <div class="relative group">
16789
+ <input
16790
+ id="collections-search"
16791
+ type="text"
16792
+ placeholder="Search collections..."
16793
+ value="${data.search || ""}"
16794
+ oninput="toggleClearButton()"
16795
+ class="rounded-full bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm px-4 py-2.5 pl-11 pr-10 text-sm w-72 text-zinc-950 dark:text-white border-2 border-cyan-200/50 dark:border-cyan-700/50 placeholder:text-zinc-400 dark:placeholder:text-zinc-500 focus:outline-none focus:border-cyan-500 dark:focus:border-cyan-400 focus:bg-white dark:focus:bg-zinc-800 focus:shadow-lg focus:shadow-cyan-500/20 dark:focus:shadow-cyan-400/20 transition-all duration-300"
16796
+ >
16797
+ <div class="absolute left-3.5 top-2.5 flex items-center justify-center w-5 h-5 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 dark:from-cyan-300 dark:to-blue-400 opacity-90 group-focus-within:opacity-100 transition-opacity">
16798
+ <svg class="h-3 w-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
16799
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
16800
+ </svg>
16801
+ </div>
16802
+ <button
16803
+ type="button"
16804
+ id="clear-search"
16805
+ onclick="clearSearch()"
16806
+ class="${data.search ? "" : "hidden"} absolute right-3 top-3 flex items-center justify-center w-5 h-5 rounded-full bg-zinc-400/20 dark:bg-zinc-500/20 hover:bg-zinc-400/30 dark:hover:bg-zinc-500/30 transition-colors"
16807
+ >
16808
+ <svg class="h-3 w-3 text-zinc-600 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
16809
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
16810
+ </svg>
16811
+ </button>
16812
+ </div>
16813
+ <button
16814
+ type="submit"
16815
+ class="inline-flex items-center gap-x-1.5 px-4 py-2.5 bg-gradient-to-r from-cyan-500 to-blue-500 dark:from-cyan-400 dark:to-blue-400 text-white text-sm font-medium rounded-full hover:from-cyan-600 hover:to-blue-600 dark:hover:from-cyan-500 dark:hover:to-blue-500 shadow-md hover:shadow-lg transition-all duration-200"
16816
+ >
16817
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
16818
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
16819
+ </svg>
16820
+ Search
16821
+ </button>
16822
+ </form>
16823
+ <script>
16824
+ function performSearch(event) {
16825
+ event.preventDefault();
16826
+ const searchInput = document.getElementById('collections-search');
16827
+ const value = searchInput.value.trim();
16828
+ const params = new URLSearchParams(window.location.search);
16829
+ if (value) {
16830
+ params.set('search', value);
16831
+ } else {
16832
+ params.delete('search');
16833
+ }
16834
+ window.location.href = window.location.pathname + '?' + params.toString();
16835
+ }
16836
+
16837
+ function clearSearch() {
16838
+ const params = new URLSearchParams(window.location.search);
16839
+ params.delete('search');
16840
+ window.location.href = window.location.pathname + (params.toString() ? '?' + params.toString() : '');
16841
+ }
16842
+
16843
+ function toggleClearButton() {
16844
+ const searchInput = document.getElementById('collections-search');
16845
+ const clearButton = document.getElementById('clear-search');
16846
+ if (searchInput.value.trim()) {
16847
+ clearButton.classList.remove('hidden');
16848
+ } else {
16849
+ clearButton.classList.add('hidden');
16850
+ }
16851
+ }
16852
+ </script>
16853
+ </div>
16854
+ <div class="flex items-center gap-x-3">
16855
+ <span class="text-sm/6 font-medium text-zinc-700 dark:text-zinc-300 px-3 py-1.5 rounded-full bg-white/60 dark:bg-zinc-800/60 backdrop-blur-sm">${data.collections.length} ${data.collections.length === 1 ? "collection" : "collections"}</span>
16856
+ <button
16857
+ class="inline-flex items-center gap-x-1.5 px-3 py-1.5 bg-white/90 dark:bg-zinc-800/90 backdrop-blur-sm text-zinc-950 dark:text-white text-sm font-medium rounded-full ring-1 ring-inset ring-cyan-200/50 dark:ring-cyan-700/50 hover:bg-gradient-to-r hover:from-cyan-50 hover:to-blue-50 dark:hover:from-cyan-900/30 dark:hover:to-blue-900/30 hover:ring-cyan-300 dark:hover:ring-cyan-600 transition-all duration-200"
16858
+ onclick="location.reload()"
16859
+ >
16860
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16861
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
16862
+ </svg>
16863
+ Refresh
16864
+ </button>
16865
+ </div>
16866
+ </div>
16867
+ </div>
16868
+ </div>
16869
+ </div>
16870
+
16871
+ <!-- Collections List -->
16872
+ <div id="collections-list">
16873
+ ${renderTable2(tableData)}
16874
+ </div>
16875
+
16876
+ <!-- Empty State -->
16877
+ ${data.collections.length === 0 ? `
16878
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-12 text-center">
16879
+ <svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
16880
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
16881
+ </svg>
16882
+ <h3 class="mt-4 text-base/7 font-semibold text-zinc-950 dark:text-white">No collections found</h3>
16883
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Get started by creating your first collection</p>
16884
+ <div class="mt-6">
16885
+ <a href="/admin/collections/new" class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
16886
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
16887
+ <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
16888
+ </svg>
16889
+ New Collection
16890
+ </a>
16891
+ </div>
16892
+ </div>
16893
+ ` : ""}
16894
+ </div>
16895
+ `;
16896
+ const layoutData = {
16897
+ title: "Collections",
16898
+ pageTitle: "Collections",
16899
+ currentPath: "/admin/collections",
16900
+ user: data.user,
16901
+ version: data.version,
16902
+ content: pageContent
16903
+ };
16904
+ return renderAdminLayoutCatalyst(layoutData);
16905
+ }
16906
+
16907
+ // src/templates/pages/admin-collections-form.template.ts
16908
+ init_admin_layout_catalyst_template();
16909
+ function renderCollectionFormPage(data) {
16910
+ const isEdit = data.isEdit || !!data.id;
16911
+ const title = isEdit ? "Edit Collection" : "Create New Collection";
16912
+ const subtitle = isEdit ? `Update collection: ${data.display_name}` : "Define a new content collection with custom fields and settings.";
16913
+ const fields = [
16914
+ {
16915
+ name: "displayName",
16916
+ label: "Display Name",
16917
+ type: "text",
16918
+ value: data.display_name || "",
16919
+ placeholder: "Blog Posts",
16920
+ required: true,
16921
+ readonly: data.managed,
16922
+ className: data.managed ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 cursor-not-allowed" : ""
16923
+ },
16924
+ {
16925
+ name: "name",
16926
+ label: "Collection Name",
16927
+ type: "text",
16928
+ value: data.name || "",
16929
+ placeholder: "blog_posts",
16930
+ required: true,
16931
+ readonly: isEdit,
16932
+ helpText: isEdit ? "Collection name cannot be changed" : "Lowercase letters, numbers, and underscores only",
16933
+ className: isEdit ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 cursor-not-allowed" : ""
16934
+ },
16935
+ {
16936
+ name: "description",
16937
+ label: "Description",
16938
+ type: "textarea",
16939
+ value: data.description || "",
16940
+ placeholder: "Description of this collection...",
16941
+ rows: 3,
16942
+ readonly: data.managed,
16943
+ className: data.managed ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 cursor-not-allowed" : ""
16944
+ }
16945
+ ];
16946
+ const formData = {
16947
+ id: "collection-form",
16948
+ ...isEdit ? { hxPut: `/admin/collections/${data.id}`, action: `/admin/collections/${data.id}`, method: "PUT" } : { hxPost: "/admin/collections", action: "/admin/collections" },
16949
+ hxTarget: "#form-messages",
16950
+ fields,
16951
+ submitButtons: data.managed ? [] : [
16952
+ {
16953
+ label: isEdit ? "Update Collection" : "Create Collection",
16954
+ type: "submit",
16955
+ className: "btn-primary"
16956
+ }
16957
+ ]
16958
+ };
16959
+ const pageContent = `
16960
+ <div class="space-y-6">
16961
+ <!-- Config-Managed Collection Banner -->
16962
+ ${data.managed ? `
16963
+ <div class="rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-900/30 p-4">
16964
+ <div class="flex items-start gap-x-3">
16965
+ <svg class="h-5 w-5 text-amber-600 dark:text-amber-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
16966
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
16967
+ </svg>
16968
+ <div class="flex-1">
16969
+ <h3 class="text-sm/6 font-semibold text-amber-900 dark:text-amber-300">
16970
+ Config-Managed Collection
16971
+ </h3>
16972
+ <div class="text-sm/6 text-amber-800 dark:text-amber-400 mt-1 space-y-1">
16973
+ <p>This collection is managed by a configuration file and cannot be edited through the admin interface.</p>
16974
+ <p class="mt-2">
16975
+ <span class="font-medium">Config file:</span>
16976
+ <code class="ml-2 px-2 py-0.5 rounded bg-amber-100 dark:bg-amber-900/40 text-amber-900 dark:text-amber-300 font-mono text-xs">
16977
+ src/collections/${data.name}.collection.ts
16978
+ </code>
16979
+ </p>
16980
+ <p class="mt-2 text-xs">
16981
+ To modify this collection's schema, edit the configuration file directly in your code editor.
16982
+ </p>
16983
+ </div>
16984
+ </div>
16985
+ </div>
16986
+ </div>
16987
+ ` : ""}
16988
+
16989
+ <!-- Header -->
16990
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
16991
+ <div>
16992
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">${title}</h1>
16993
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">${subtitle}</p>
16994
+ </div>
16995
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
16996
+ <a href="/admin/collections" class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm">
16997
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
16998
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
16999
+ </svg>
17000
+ Back to Collections
17001
+ </a>
17002
+ </div>
17003
+ </div>
17004
+
17005
+ <!-- Form Container -->
17006
+ <div class="rounded-lg bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 overflow-hidden">
17007
+ <!-- Form Header -->
17008
+ <div class="border-b border-zinc-950/5 dark:border-white/10 px-6 py-6">
17009
+ <div class="flex items-center gap-x-3">
17010
+ <div class="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-50 dark:bg-zinc-800 ring-1 ring-zinc-950/10 dark:ring-white/10">
17011
+ <svg class="h-6 w-6 text-zinc-950 dark:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
17012
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
17013
+ </svg>
17014
+ </div>
17015
+ <div>
17016
+ <h2 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Collection Details</h2>
17017
+ <p class="text-sm/6 text-zinc-500 dark:text-zinc-400">Configure your collection settings below</p>
17018
+ </div>
17019
+ </div>
17020
+ </div>
17021
+
17022
+ <!-- Form Content -->
17023
+ <div class="px-6 py-6">
17024
+ <div id="form-messages"></div>
17025
+ ${data.error ? renderAlert2({ type: "error", message: data.error, dismissible: true }) : ""}
17026
+ ${data.success ? renderAlert2({ type: "success", message: data.success, dismissible: true }) : ""}
17027
+
17028
+ <!-- Form Styling -->
17029
+ <style>
17030
+ #collection-form .form-group {
17031
+ margin-bottom: 1.5rem;
17032
+ }
17033
+
17034
+ #collection-form .form-label {
17035
+ display: block;
17036
+ font-size: 0.875rem;
17037
+ font-weight: 500;
17038
+ margin-bottom: 0.5rem;
17039
+ line-height: 1.5rem;
17040
+ }
17041
+
17042
+ .dark #collection-form .form-label {
17043
+ color: white;
17044
+ }
17045
+
17046
+ html:not(.dark) #collection-form .form-label {
17047
+ color: #09090b; /* zinc-950 */
17048
+ }
17049
+
17050
+ #collection-form .form-input,
17051
+ #collection-form .form-textarea {
17052
+ width: 100%;
17053
+ padding: 0.625rem 0.75rem;
17054
+ border-radius: 0.5rem;
17055
+ font-size: 0.875rem;
17056
+ line-height: 1.5rem;
17057
+ transition: all 0.15s;
17058
+ }
17059
+
17060
+ html:not(.dark) #collection-form .form-input,
17061
+ html:not(.dark) #collection-form .form-textarea {
17062
+ background: white;
17063
+ border: 1px solid rgba(9, 9, 11, 0.1); /* zinc-950/10 */
17064
+ color: #09090b; /* zinc-950 */
17065
+ }
17066
+
17067
+ .dark #collection-form .form-input,
17068
+ .dark #collection-form .form-textarea {
17069
+ background: #18181b; /* zinc-900 */
17070
+ border: 1px solid rgba(255, 255, 255, 0.1);
17071
+ color: white;
17072
+ }
17073
+
17074
+ #collection-form .form-input:focus,
17075
+ #collection-form .form-textarea:focus {
17076
+ outline: none;
17077
+ box-shadow: 0 0 0 2px #2563eb; /* blue-600 */
17078
+ }
17079
+
17080
+ .dark #collection-form .form-input:focus,
17081
+ .dark #collection-form .form-textarea:focus {
17082
+ box-shadow: 0 0 0 2px #3b82f6; /* blue-500 */
17083
+ }
17084
+
17085
+ html:not(.dark) #collection-form .form-input::placeholder,
17086
+ html:not(.dark) #collection-form .form-textarea::placeholder {
17087
+ color: #71717a; /* zinc-500 */
17088
+ }
17089
+
17090
+ .dark #collection-form .form-input::placeholder,
17091
+ .dark #collection-form .form-textarea::placeholder {
17092
+ color: #71717a; /* zinc-500 */
17093
+ }
17094
+
17095
+ #collection-form .btn {
17096
+ padding: 0.625rem 1rem;
17097
+ font-weight: 600;
17098
+ font-size: 0.875rem;
17099
+ border-radius: 0.5rem;
17100
+ transition: all 0.15s;
17101
+ border: none;
17102
+ cursor: pointer;
17103
+ }
17104
+
17105
+ html:not(.dark) #collection-form .btn-primary {
17106
+ background: #09090b; /* zinc-950 */
17107
+ color: white;
17108
+ }
17109
+
17110
+ html:not(.dark) #collection-form .btn-primary:hover {
17111
+ background: #27272a; /* zinc-800 */
17112
+ }
17113
+
17114
+ .dark #collection-form .btn-primary {
17115
+ background: white;
17116
+ color: #09090b; /* zinc-950 */
17117
+ }
17118
+
17119
+ .dark #collection-form .btn-primary:hover {
17120
+ background: #f4f4f5; /* zinc-100 */
17121
+ }
17122
+
17123
+ #collection-form .form-help-text {
17124
+ font-size: 0.75rem;
17125
+ margin-top: 0.25rem;
17126
+ }
17127
+
17128
+ html:not(.dark) #collection-form .form-help-text {
17129
+ color: #71717a; /* zinc-500 */
17130
+ }
17131
+
17132
+ .dark #collection-form .form-help-text {
17133
+ color: #a1a1aa; /* zinc-400 */
17134
+ }
17135
+ </style>
17136
+
17137
+ ${renderForm(formData)}
17138
+
17139
+ ${isEdit && !data.managed ? `
17140
+ <!-- Fields Management Section -->
17141
+ <div class="mt-8 pt-8 border-t border-zinc-950/5 dark:border-white/10">
17142
+ <div class="flex items-center justify-between mb-6">
17143
+ <div>
17144
+ <h3 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Collection Fields</h3>
17145
+ <p class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">Define the fields that content in this collection will have</p>
17146
+ </div>
17147
+ <button
17148
+ type="button"
17149
+ onclick="showAddFieldModal()"
17150
+ class="inline-flex items-center gap-x-1.5 px-3.5 py-2.5 bg-zinc-950 dark:bg-white text-white dark:text-zinc-950 font-semibold text-sm rounded-lg hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
17151
+ >
17152
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
17153
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
17154
+ </svg>
17155
+ Add Field
17156
+ </button>
17157
+ </div>
17158
+
17159
+ <!-- Fields List -->
17160
+ <div id="fields-list" class="space-y-3">
17161
+ ${(data.fields || []).map((field) => `
17162
+ <div class="field-item bg-zinc-50 dark:bg-zinc-800/50 rounded-lg border border-zinc-950/5 dark:border-white/10 p-4" data-field-id="${field.id}">
17163
+ <div class="flex items-center justify-between">
17164
+ <div class="flex items-center gap-x-4">
17165
+ <div class="drag-handle cursor-move text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-400">
17166
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
17167
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 8h16M4 16h16"/>
17168
+ </svg>
17169
+ </div>
17170
+ <div>
17171
+ <div class="flex items-center gap-x-2">
17172
+ <span class="text-sm/6 font-medium text-zinc-950 dark:text-white">${field.field_label}</span>
17173
+ <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-700 dark:text-cyan-300 ring-1 ring-inset ring-cyan-500/20 dark:ring-cyan-400/20">
17174
+ ${field.field_type}
17175
+ </span>
17176
+ ${field.is_required ? `
17177
+ <span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-pink-500/10 dark:bg-pink-400/10 text-pink-700 dark:text-pink-300 ring-1 ring-inset ring-pink-500/20 dark:ring-pink-400/20">
17178
+ Required
17179
+ </span>
17180
+ ` : ""}
17181
+ </div>
17182
+ <div class="text-sm/6 text-zinc-500 dark:text-zinc-400 mt-1">
17183
+ Field name: <code class="text-zinc-950 dark:text-white font-mono text-xs">${field.field_name}</code>
17184
+ </div>
17185
+ </div>
17186
+ </div>
17187
+ <div class="flex items-center gap-x-2">
17188
+ <button
17189
+ type="button"
17190
+ onclick="editField('${field.id}')"
17191
+ class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-sm font-medium text-zinc-950 dark:text-white hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded-lg transition-colors"
17192
+ >
17193
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
17194
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/>
17195
+ </svg>
17196
+ Edit
17197
+ </button>
17198
+ <button
17199
+ type="button"
17200
+ onclick="deleteField('${field.id}')"
17201
+ class="inline-flex items-center gap-x-1 px-2.5 py-1.5 text-sm font-medium text-pink-700 dark:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg transition-colors"
17202
+ >
17203
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
17204
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/>
17205
+ </svg>
17206
+ Delete
17207
+ </button>
17208
+ </div>
17209
+ </div>
17210
+ </div>
17211
+ `).join("")}
17212
+
17213
+ ${(data.fields || []).length === 0 ? `
17214
+ <div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
17215
+ <svg class="mx-auto h-12 w-12 text-zinc-400 dark:text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
17216
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/>
17217
+ </svg>
17218
+ <p class="mt-4 text-base/7 font-semibold text-zinc-950 dark:text-white">No fields defined</p>
17219
+ <p class="mt-2 text-sm/6">Add your first field to get started</p>
17220
+ </div>
17221
+ ` : ""}
17222
+ </div>
17223
+ </div>
17224
+ ` : `
17225
+ <div class="mt-6 rounded-lg bg-cyan-50 dark:bg-cyan-900/20 border border-cyan-100 dark:border-cyan-900/30 p-4">
17226
+ <div class="flex items-start gap-x-3">
17227
+ <svg class="h-5 w-5 text-cyan-600 dark:text-cyan-400 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
17228
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
17229
+ </svg>
17230
+ <div>
17231
+ <h3 class="text-sm/6 font-medium text-cyan-900 dark:text-cyan-300">
17232
+ Create Collection First
17233
+ </h3>
17234
+ <p class="text-sm/6 text-cyan-800 dark:text-cyan-400 mt-1">
17235
+ After creating the collection, you'll be able to add and configure custom fields.
17236
+ </p>
17237
+ </div>
17238
+ </div>
17239
+ </div>
17240
+ `}
17241
+
17242
+ <!-- Action Buttons -->
17243
+ <div class="mt-6 pt-6 border-t border-zinc-950/5 dark:border-white/10 flex items-center justify-between">
17244
+ <a href="/admin/collections" class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm">
17245
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
17246
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
17247
+ </svg>
17248
+ ${data.managed ? "Back to Collections" : "Cancel"}
17249
+ </a>
17250
+
17251
+ ${isEdit && !data.managed ? `
17252
+ <button
17253
+ type="button"
17254
+ hx-delete="/admin/collections/${data.id}"
17255
+ hx-confirm="Are you sure you want to delete this collection? This action cannot be undone."
17256
+ hx-target="body"
17257
+ class="inline-flex items-center justify-center gap-x-1.5 rounded-lg bg-pink-600 dark:bg-pink-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-pink-700 dark:hover:bg-pink-600 transition-colors shadow-sm"
17258
+ >
17259
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
17260
+ <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"/>
17261
+ </svg>
17262
+ Delete Collection
17263
+ </button>
17264
+ ` : ""}
17265
+ </div>
17266
+ </div>
17267
+ </div>
17268
+ </div>
17269
+
17270
+ <!-- Add/Edit Field Modal -->
17271
+ <div id="field-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 hidden">
17272
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-xl ring-1 ring-zinc-950/5 dark:ring-white/10 w-full max-w-lg mx-4">
17273
+ <div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
17274
+ <div class="flex items-center justify-between">
17275
+ <h3 id="modal-title" class="text-lg font-semibold text-zinc-950 dark:text-white">Add Field</h3>
17276
+ <button onclick="closeFieldModal()" class="text-zinc-500 dark:text-zinc-400 hover:text-zinc-950 dark:hover:text-white transition-colors">
17277
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
17278
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
17279
+ </svg>
17280
+ </button>
17281
+ </div>
17282
+ </div>
17283
+
17284
+ <form id="field-form" class="p-6 space-y-4">
17285
+ <input type="hidden" id="field-id" name="field_id">
17286
+
17287
+ <div>
17288
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Name</label>
17289
+ <input
17290
+ type="text"
17291
+ id="field-name"
17292
+ name="field_name"
17293
+ required
17294
+ pattern="[a-z0-9_]+"
17295
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-4 py-3 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-blue-600 dark:focus:ring-blue-500 focus:outline-none transition-colors"
17296
+ placeholder="field_name"
17297
+ >
17298
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">Lowercase letters, numbers, and underscores only</p>
17299
+ </div>
17300
+
17301
+ <div>
17302
+ <label for="field-type" class="block text-sm/6 font-medium text-zinc-950 dark:text-white">Field Type</label>
17303
+ <div class="mt-2 grid grid-cols-1">
17304
+ <select
17305
+ id="field-type"
17306
+ name="field_type"
17307
+ required
17308
+ class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white/5 dark:bg-white/5 py-1.5 pl-3 pr-8 text-base text-zinc-950 dark:text-white outline outline-1 -outline-offset-1 outline-blue-500/30 dark:outline-blue-400/30 *:bg-white dark:*:bg-zinc-800 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-500 dark:focus-visible:outline-blue-400 sm:text-sm/6"
17309
+ >
17310
+ <option value="">Select field type...</option>
17311
+ <option value="text">Text</option>
17312
+ <option value="richtext">Rich Text</option>
17313
+ <option value="number">Number</option>
17314
+ <option value="boolean">Boolean</option>
17315
+ <option value="date">Date</option>
17316
+ <option value="select">Select</option>
17317
+ <option value="media">Media</option>
17318
+ <option value="guid">GUID (Auto-generated)</option>
17319
+ </select>
17320
+ <svg viewBox="0 0 16 16" fill="currentColor" data-slot="icon" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-blue-600 dark:text-blue-400 sm:size-4">
17321
+ <path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" fill-rule="evenodd" />
17322
+ </svg>
17323
+ </div>
17324
+ <p id="field-type-help" class="text-xs text-zinc-500 dark:text-zinc-400 mt-1"></p>
17325
+ </div>
17326
+
17327
+ <div>
17328
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Label</label>
17329
+ <input
17330
+ type="text"
17331
+ id="field-label"
17332
+ name="field_label"
17333
+ required
17334
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-4 py-3 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-blue-600 dark:focus:ring-blue-500 focus:outline-none transition-colors"
17335
+ placeholder="Field Label"
17336
+ >
17337
+ </div>
17338
+
17339
+ <div class="flex items-center space-x-6">
17340
+ <div class="flex gap-3">
17341
+ <div class="flex h-6 shrink-0 items-center">
17342
+ <div class="group grid size-4 grid-cols-1">
17343
+ <input
17344
+ type="checkbox"
17345
+ id="field-required"
17346
+ name="is_required"
17347
+ value="1"
17348
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
17349
+ />
17350
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
17351
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
17352
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
17353
+ </svg>
17354
+ </div>
17355
+ </div>
17356
+ <div class="text-sm/6">
17357
+ <label for="field-required" class="font-medium text-zinc-950 dark:text-white">Required</label>
17358
+ </div>
17359
+ </div>
17360
+
17361
+ <div class="flex gap-3">
17362
+ <div class="flex h-6 shrink-0 items-center">
17363
+ <div class="group grid size-4 grid-cols-1">
17364
+ <input
17365
+ type="checkbox"
17366
+ id="field-searchable"
17367
+ name="is_searchable"
17368
+ value="1"
17369
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
17370
+ />
17371
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
17372
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
17373
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
17374
+ </svg>
17375
+ </div>
17376
+ </div>
17377
+ <div class="text-sm/6">
17378
+ <label for="field-searchable" class="font-medium text-zinc-950 dark:text-white">Searchable</label>
17379
+ </div>
17380
+ </div>
17381
+ </div>
17382
+
17383
+ <div id="field-options-container" class="hidden">
17384
+ <label class="block text-sm font-medium text-zinc-950 dark:text-white mb-2">Field Options (JSON)</label>
17385
+ <textarea
17386
+ id="field-options"
17387
+ name="field_options"
17388
+ rows="3"
17389
+ class="w-full rounded-lg bg-white dark:bg-zinc-800 px-4 py-3 text-sm text-zinc-950 dark:text-white placeholder-zinc-500 dark:placeholder-zinc-400 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-blue-600 dark:focus:ring-blue-500 focus:outline-none transition-colors font-mono"
17390
+ placeholder='{"maxLength": 200, "placeholder": "Enter text..."}'
17391
+ ></textarea>
17392
+ <p class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">JSON configuration for field-specific options</p>
17393
+ </div>
17394
+
17395
+ <div class="flex justify-end space-x-3 pt-4 border-t border-zinc-950/5 dark:border-white/10">
17396
+ <button
17397
+ type="button"
17398
+ onclick="closeFieldModal()"
17399
+ class="rounded-lg bg-white dark:bg-zinc-800 px-4 py-2 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors"
17400
+ >
17401
+ Cancel
17402
+ </button>
17403
+ <button
17404
+ type="submit"
17405
+ class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 transition-colors"
17406
+ >
17407
+ <span id="submit-text">Add Field</span>
17408
+ </button>
17409
+ </div>
17410
+ </form>
17411
+ </div>
17412
+ </div>
17413
+
17414
+ <script>
17415
+ const collectionId = '${data.id || ""}';
17416
+
17417
+
17418
+ let currentEditingField = null;
17419
+
17420
+ // Field modal functions
17421
+ function showAddFieldModal() {
17422
+ document.getElementById('modal-title').textContent = 'Add Field';
17423
+ document.getElementById('submit-text').textContent = 'Add Field';
17424
+ document.getElementById('field-form').reset();
17425
+ document.getElementById('field-id').value = '';
17426
+ document.getElementById('field-name').disabled = false;
17427
+ currentEditingField = null;
17428
+ document.getElementById('field-modal').classList.remove('hidden');
17429
+ }
17430
+
17431
+ function editField(fieldId) {
17432
+ const fieldItem = document.querySelector(\`[data-field-id="\${fieldId}"]\`);
17433
+ if (!fieldItem) return;
17434
+
17435
+ // Find the field data from the collection's fields array
17436
+ const field = ${JSON.stringify(data.fields || [])}.find(f => f.id === fieldId);
17437
+ if (!field) return;
17438
+
17439
+ // Set up the modal for editing
17440
+ document.getElementById('modal-title').textContent = 'Edit Field';
17441
+ document.getElementById('submit-text').textContent = 'Update Field';
17442
+ document.getElementById('field-id').value = fieldId;
17443
+ currentEditingField = fieldId;
17444
+
17445
+ // Populate form with existing field data
17446
+ document.getElementById('field-name').value = field.field_name || '';
17447
+ document.getElementById('field-name').disabled = true;
17448
+ document.getElementById('field-label').value = field.field_label || '';
17449
+ document.getElementById('field-type').value = field.field_type || '';
17450
+ document.getElementById('field-required').checked = Boolean(field.is_required);
17451
+ document.getElementById('field-searchable').checked = Boolean(field.is_searchable);
17452
+
17453
+ // Handle field options - serialize object back to JSON string
17454
+ if (field.field_options) {
17455
+ document.getElementById('field-options').value = typeof field.field_options === 'string'
17456
+ ? field.field_options
17457
+ : JSON.stringify(field.field_options, null, 2);
17458
+ } else {
17459
+ document.getElementById('field-options').value = '';
17460
+ }
17461
+
17462
+ // Show/hide options container based on field type
17463
+ const fieldType = field.field_type;
17464
+ const optionsContainer = document.getElementById('field-options-container');
17465
+ const helpText = document.getElementById('field-type-help');
17466
+
17467
+ if (['select', 'media', 'richtext', 'guid'].includes(fieldType)) {
17468
+ optionsContainer.classList.remove('hidden');
17469
+
17470
+ // Set help text based on type
17471
+ switch (fieldType) {
17472
+ case 'select':
17473
+ helpText.textContent = 'Create a dropdown select field with custom options';
17474
+ break;
17475
+ case 'media':
17476
+ helpText.textContent = 'Upload and manage media files (images, videos, documents)';
17477
+ break;
17478
+ case 'richtext':
17479
+ helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
17480
+ break;
17481
+ case 'guid':
17482
+ helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
17483
+ break;
17484
+ }
17485
+ } else {
17486
+ optionsContainer.classList.add('hidden');
17487
+
17488
+ // Set help text for other field types
17489
+ switch (fieldType) {
17490
+ case 'text':
17491
+ helpText.textContent = 'Single-line text input for short content';
17492
+ break;
17493
+ case 'number':
17494
+ helpText.textContent = 'Numeric input field for integers or decimals';
17495
+ break;
17496
+ case 'boolean':
17497
+ helpText.textContent = 'True/false checkbox field';
17498
+ break;
17499
+ case 'date':
17500
+ helpText.textContent = 'Date and time picker field';
17501
+ break;
17502
+ default:
17503
+ helpText.textContent = '';
17504
+ }
17505
+ }
17506
+
17507
+ document.getElementById('field-modal').classList.remove('hidden');
17508
+ }
17509
+
17510
+ function closeFieldModal() {
17511
+ document.getElementById('field-modal').classList.add('hidden');
17512
+ currentEditingField = null;
17513
+ }
17514
+
17515
+ let fieldToDelete = null;
17516
+
17517
+ function deleteField(fieldId) {
17518
+ fieldToDelete = fieldId;
17519
+ showConfirmDialog('delete-field-confirm');
17520
+ }
17521
+
17522
+ function performDeleteField() {
17523
+ if (!fieldToDelete) return;
17524
+
17525
+ fetch(\`/admin/collections/\${collectionId}/fields/\${fieldToDelete}\`, {
17526
+ method: 'DELETE'
17527
+ })
17528
+ .then(response => response.json())
17529
+ .then(data => {
17530
+ if (data.success) {
17531
+ location.reload();
17532
+ } else {
17533
+ alert('Error deleting field: ' + data.error);
17534
+ }
17535
+ })
17536
+ .catch(error => {
17537
+ console.error('Error:', error);
17538
+ alert('Error deleting field');
17539
+ })
17540
+ .finally(() => {
17541
+ fieldToDelete = null;
17542
+ });
17543
+ }
17544
+
17545
+ // Field form submission
17546
+ document.getElementById('field-form').addEventListener('submit', function(e) {
17547
+ e.preventDefault();
17548
+
17549
+ if (!collectionId) {
17550
+ alert('Error: Collection ID is missing. Cannot save field.');
17551
+ return;
17552
+ }
17553
+
17554
+ const formData = new FormData(this);
17555
+ const isEditing = currentEditingField !== null;
17556
+
17557
+ const url = isEditing
17558
+ ? \`/admin/collections/\${collectionId}/fields/\${currentEditingField}\`
17559
+ : \`/admin/collections/\${collectionId}/fields\`;
17560
+
17561
+ const method = isEditing ? 'PUT' : 'POST';
17562
+
17563
+
17564
+ fetch(url, {
17565
+ method: method,
17566
+ body: formData
17567
+ })
17568
+ .then(response => {
17569
+ if (!response.ok) {
17570
+ throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);
17571
+ }
17572
+ return response.json();
17573
+ })
17574
+ .then(data => {
17575
+ if (data.success) {
17576
+ location.reload();
17577
+ } else {
17578
+ alert('Error saving field: ' + (data.error || 'Unknown error'));
17579
+ }
17580
+ })
17581
+ .catch(error => {
17582
+ alert('Error saving field: ' + error.message);
17583
+ });
17584
+ });
17585
+
17586
+ // Field type change handler
17587
+ document.getElementById('field-type').addEventListener('change', function() {
17588
+ const optionsContainer = document.getElementById('field-options-container');
17589
+ const fieldOptions = document.getElementById('field-options');
17590
+ const helpText = document.getElementById('field-type-help');
17591
+ const fieldNameInput = document.getElementById('field-name');
17592
+
17593
+ // Show/hide options based on field type
17594
+ if (['select', 'media', 'richtext', 'guid'].includes(this.value)) {
17595
+ optionsContainer.classList.remove('hidden');
17596
+
17597
+ // Set default options and help text based on type
17598
+ switch (this.value) {
17599
+ case 'select':
17600
+ fieldOptions.value = '{"options": ["Option 1", "Option 2"], "multiple": false}';
17601
+ helpText.textContent = 'Create a dropdown select field with custom options';
17602
+ break;
17603
+ case 'media':
17604
+ fieldOptions.value = '{"accept": "image/*", "maxSize": "10MB"}';
17605
+ helpText.textContent = 'Upload and manage media files (images, videos, documents)';
17606
+ break;
17607
+ case 'richtext':
17608
+ fieldOptions.value = '{"toolbar": "full", "height": 400}';
17609
+ helpText.textContent = 'Full-featured WYSIWYG text editor with formatting options';
17610
+ break;
17611
+ case 'guid':
17612
+ fieldOptions.value = '{"autoGenerate": true, "format": "uuid-v4"}';
17613
+ helpText.textContent = 'Automatically generates a unique identifier (UUID v4) for each content item';
17614
+ // Suggest 'id' as field name for GUID fields
17615
+ if (!fieldNameInput.value || fieldNameInput.value === '') {
17616
+ fieldNameInput.value = 'id';
17617
+ }
17618
+ break;
17619
+ }
17620
+ } else {
17621
+ optionsContainer.classList.add('hidden');
17622
+ fieldOptions.value = '{}';
17623
+
17624
+ // Set help text for other field types
17625
+ switch (this.value) {
17626
+ case 'text':
17627
+ helpText.textContent = 'Single-line text input for short content';
17628
+ break;
17629
+ case 'number':
17630
+ helpText.textContent = 'Numeric input field for integers or decimals';
17631
+ break;
17632
+ case 'boolean':
17633
+ helpText.textContent = 'True/false checkbox field';
17634
+ break;
17635
+ case 'date':
17636
+ helpText.textContent = 'Date and time picker field';
17637
+ break;
17638
+ default:
17639
+ helpText.textContent = '';
17640
+ }
17641
+ }
17642
+ });
17643
+
17644
+ // Close modal on escape key
17645
+ document.addEventListener('keydown', function(e) {
17646
+ if (e.key === 'Escape' && !document.getElementById('field-modal').classList.contains('hidden')) {
17647
+ closeFieldModal();
17648
+ }
17649
+ });
17650
+
17651
+ // Close modal on backdrop click
17652
+ document.getElementById('field-modal').addEventListener('click', function(e) {
17653
+ if (e.target === this) {
17654
+ closeFieldModal();
17655
+ }
17656
+ });
17657
+ </script>
17658
+
17659
+ <!-- Confirmation Dialogs -->
17660
+ ${renderConfirmationDialog2({
17661
+ id: "delete-field-confirm",
17662
+ title: "Delete Field",
17663
+ message: "Are you sure you want to delete this field? This action cannot be undone.",
17664
+ confirmText: "Delete",
17665
+ cancelText: "Cancel",
17666
+ iconColor: "red",
17667
+ confirmClass: "bg-red-500 hover:bg-red-400",
17668
+ onConfirm: "performDeleteField()"
17669
+ })}
17670
+
17671
+ ${getConfirmationDialogScript2()}
17672
+ `;
17673
+ const layoutData = {
17674
+ title,
17675
+ pageTitle: "Collections",
17676
+ currentPath: "/admin/collections",
17677
+ user: data.user,
17678
+ version: data.version,
17679
+ content: pageContent
17680
+ };
17681
+ return renderAdminLayoutCatalyst(layoutData);
17682
+ }
17683
+
17684
+ // src/routes/admin-collections.ts
17685
+ var adminCollectionsRoutes = new Hono();
17686
+ adminCollectionsRoutes.get("/collections", async (c) => {
17687
+ try {
17688
+ const user = c.get("user");
17689
+ const db = c.env.DB;
17690
+ const url = new URL(c.req.url);
17691
+ const search = url.searchParams.get("search") || "";
17692
+ let stmt;
17693
+ let results;
17694
+ if (search) {
17695
+ stmt = db.prepare(`
17696
+ SELECT id, name, display_name, description, created_at, managed
17697
+ FROM collections
17698
+ WHERE is_active = 1
17699
+ AND (name LIKE ? OR display_name LIKE ? OR description LIKE ?)
17700
+ ORDER BY created_at DESC
17701
+ `);
17702
+ const searchParam = `%${search}%`;
17703
+ const queryResults = await stmt.bind(searchParam, searchParam, searchParam).all();
17704
+ results = queryResults.results;
17705
+ } else {
17706
+ stmt = db.prepare("SELECT id, name, display_name, description, created_at, managed FROM collections WHERE is_active = 1 ORDER BY created_at DESC");
17707
+ const queryResults = await stmt.all();
17708
+ results = queryResults.results;
17709
+ }
17710
+ const fieldCountStmt = db.prepare("SELECT collection_id, COUNT(*) as count FROM content_fields GROUP BY collection_id");
17711
+ const { results: fieldCountResults } = await fieldCountStmt.all();
17712
+ const fieldCounts = new Map((fieldCountResults || []).map((row) => [String(row.collection_id), Number(row.count)]));
17713
+ const collections = (results || []).filter((row) => row && row.id).map((row) => {
17714
+ return {
17715
+ id: String(row.id || ""),
17716
+ name: String(row.name || ""),
17717
+ display_name: String(row.display_name || ""),
17718
+ description: row.description ? String(row.description) : void 0,
17719
+ created_at: Number(row.created_at || 0),
17720
+ formattedDate: row.created_at ? new Date(Number(row.created_at)).toLocaleDateString() : "Unknown",
17721
+ field_count: fieldCounts.get(String(row.id)) || 0,
17722
+ managed: row.managed === 1
17723
+ };
17724
+ });
17725
+ const pageData = {
17726
+ collections,
17727
+ search,
17728
+ user: user ? {
17729
+ name: user.email,
17730
+ email: user.email,
17731
+ role: user.role
17732
+ } : void 0,
17733
+ version: c.get("appVersion")
17734
+ };
17735
+ return c.html(renderCollectionsListPage(pageData));
17736
+ } catch (error) {
17737
+ console.error("Error fetching collections:", error);
17738
+ return c.html(html`<p>Error loading collections</p>`);
17739
+ }
17740
+ });
17741
+ adminCollectionsRoutes.get("/collections/new", (c) => {
17742
+ const user = c.get("user");
17743
+ const formData = {
17744
+ isEdit: false,
17745
+ user: user ? {
17746
+ name: user.email,
17747
+ email: user.email,
17748
+ role: user.role
17749
+ } : void 0,
17750
+ version: c.get("appVersion")
17751
+ };
17752
+ return c.html(renderCollectionFormPage(formData));
17753
+ });
17754
+ adminCollectionsRoutes.post("/collections", async (c) => {
17755
+ try {
17756
+ const formData = await c.req.formData();
17757
+ const name = formData.get("name");
17758
+ const displayName = formData.get("displayName");
17759
+ const description = formData.get("description");
17760
+ const isHtmx = c.req.header("HX-Request") === "true";
17761
+ if (!name || !displayName) {
17762
+ const errorMsg = "Name and display name are required.";
17763
+ if (isHtmx) {
17764
+ return c.html(html`
17765
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
17766
+ ${errorMsg}
17767
+ </div>
17768
+ `);
17769
+ } else {
17770
+ return c.redirect("/admin/collections/new");
17771
+ }
17772
+ }
17773
+ if (!/^[a-z0-9_]+$/.test(name)) {
17774
+ const errorMsg = "Collection name must contain only lowercase letters, numbers, and underscores.";
17775
+ if (isHtmx) {
17776
+ return c.html(html`
17777
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
17778
+ ${errorMsg}
17779
+ </div>
17780
+ `);
17781
+ } else {
17782
+ return c.redirect("/admin/collections/new");
17783
+ }
17784
+ }
17785
+ const db = c.env.DB;
17786
+ const existingStmt = db.prepare("SELECT id FROM collections WHERE name = ?");
17787
+ const existing = await existingStmt.bind(name).first();
17788
+ if (existing) {
17789
+ const errorMsg = "A collection with this name already exists.";
17790
+ if (isHtmx) {
17791
+ return c.html(html`
17792
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
17793
+ ${errorMsg}
17794
+ </div>
17795
+ `);
17796
+ } else {
17797
+ return c.redirect("/admin/collections/new");
17798
+ }
17799
+ }
17800
+ const basicSchema = {
17801
+ type: "object",
17802
+ properties: {
17803
+ title: {
17804
+ type: "string",
17805
+ title: "Title",
17806
+ required: true
17807
+ },
17808
+ content: {
17809
+ type: "string",
17810
+ title: "Content",
17811
+ format: "richtext"
17812
+ },
17813
+ status: {
17814
+ type: "string",
17815
+ title: "Status",
17816
+ enum: ["draft", "published", "archived"],
17817
+ default: "draft"
17818
+ }
17819
+ },
17820
+ required: ["title"]
17821
+ };
17822
+ const collectionId = globalThis.crypto.randomUUID();
17823
+ const now = Date.now();
17824
+ const insertStmt = db.prepare(`
17825
+ INSERT INTO collections (id, name, display_name, description, schema, is_active, created_at, updated_at)
17826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
17827
+ `);
17828
+ await insertStmt.bind(
17829
+ collectionId,
17830
+ name,
17831
+ displayName,
17832
+ description || null,
17833
+ JSON.stringify(basicSchema),
17834
+ 1,
17835
+ // is_active
17836
+ now,
17837
+ now
17838
+ ).run();
17839
+ try {
17840
+ await c.env.CACHE_KV.delete("cache:collections:all");
17841
+ await c.env.CACHE_KV.delete(`cache:collection:${name}`);
17842
+ } catch (e) {
17843
+ console.error("Error clearing cache:", e);
17844
+ }
17845
+ if (isHtmx) {
17846
+ return c.html(html`
17847
+ <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
17848
+ Collection created successfully! Redirecting...
17849
+ <script>
17850
+ setTimeout(() => {
17851
+ window.location.href = '/admin/collections';
17852
+ }, 1500);
17853
+ </script>
17854
+ </div>
17855
+ `);
17856
+ } else {
17857
+ return c.redirect("/admin/collections");
17858
+ }
17859
+ } catch (error) {
17860
+ console.error("Error creating collection:", error);
17861
+ const isHtmx = c.req.header("HX-Request") === "true";
17862
+ if (isHtmx) {
17863
+ return c.html(html`
17864
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
17865
+ Failed to create collection. Please try again.
17866
+ </div>
17867
+ `);
17868
+ } else {
17869
+ return c.redirect("/admin/collections/new");
17870
+ }
17871
+ }
17872
+ });
17873
+ adminCollectionsRoutes.get("/collections/:id", async (c) => {
17874
+ try {
17875
+ const id = c.req.param("id");
17876
+ const user = c.get("user");
17877
+ const db = c.env.DB;
17878
+ const stmt = db.prepare("SELECT * FROM collections WHERE id = ?");
17879
+ const collection = await stmt.bind(id).first();
17880
+ if (!collection) {
17881
+ const formData2 = {
17882
+ isEdit: true,
17883
+ error: "Collection not found.",
17884
+ user: user ? {
17885
+ name: user.email,
17886
+ email: user.email,
17887
+ role: user.role
17888
+ } : void 0,
17889
+ version: c.get("appVersion")
17890
+ };
17891
+ return c.html(renderCollectionFormPage(formData2));
17892
+ }
17893
+ const fieldsStmt = db.prepare(`
17894
+ SELECT * FROM content_fields
17895
+ WHERE collection_id = ?
17896
+ ORDER BY field_order ASC
17897
+ `);
17898
+ const { results: fieldsResults } = await fieldsStmt.bind(id).all();
17899
+ const fields = (fieldsResults || []).map((row) => ({
17900
+ id: row.id,
17901
+ field_name: row.field_name,
17902
+ field_type: row.field_type,
17903
+ field_label: row.field_label,
17904
+ field_options: row.field_options ? JSON.parse(row.field_options) : {},
17905
+ field_order: row.field_order,
17906
+ is_required: row.is_required === 1,
17907
+ is_searchable: row.is_searchable === 1
17908
+ }));
17909
+ const formData = {
17910
+ id: collection.id,
17911
+ name: collection.name,
17912
+ display_name: collection.display_name,
17913
+ description: collection.description,
17914
+ fields,
17915
+ managed: collection.managed === 1,
17916
+ isEdit: true,
17917
+ user: user ? {
17918
+ name: user.email,
17919
+ email: user.email,
17920
+ role: user.role
17921
+ } : void 0,
17922
+ version: c.get("appVersion")
17923
+ };
17924
+ return c.html(renderCollectionFormPage(formData));
17925
+ } catch (error) {
17926
+ console.error("Error fetching collection:", error);
17927
+ const user = c.get("user");
17928
+ const formData = {
17929
+ isEdit: true,
17930
+ error: "Failed to load collection.",
17931
+ user: user ? {
17932
+ name: user.email,
17933
+ email: user.email,
17934
+ role: user.role
17935
+ } : void 0,
17936
+ version: c.get("appVersion")
17937
+ };
17938
+ return c.html(renderCollectionFormPage(formData));
17939
+ }
17940
+ });
17941
+ adminCollectionsRoutes.put("/collections/:id", async (c) => {
17942
+ try {
17943
+ const id = c.req.param("id");
17944
+ const formData = await c.req.formData();
17945
+ const displayName = formData.get("displayName");
17946
+ const description = formData.get("description");
17947
+ if (!displayName) {
17948
+ return c.html(html`
17949
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
17950
+ Display name is required.
17951
+ </div>
17952
+ `);
17953
+ }
17954
+ const db = c.env.DB;
17955
+ const updateStmt = db.prepare(`
17956
+ UPDATE collections
17957
+ SET display_name = ?, description = ?, updated_at = ?
17958
+ WHERE id = ?
17959
+ `);
17960
+ await updateStmt.bind(displayName, description || null, Date.now(), id).run();
17961
+ return c.html(html`
17962
+ <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
17963
+ Collection updated successfully!
17964
+ </div>
17965
+ `);
17966
+ } catch (error) {
17967
+ console.error("Error updating collection:", error);
17968
+ return c.html(html`
17969
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
17970
+ Failed to update collection. Please try again.
17971
+ </div>
17972
+ `);
17973
+ }
17974
+ });
17975
+ adminCollectionsRoutes.delete("/collections/:id", async (c) => {
17976
+ try {
17977
+ const id = c.req.param("id");
17978
+ const db = c.env.DB;
17979
+ const contentStmt = db.prepare("SELECT COUNT(*) as count FROM content WHERE collection_id = ?");
17980
+ const contentResult = await contentStmt.bind(id).first();
17981
+ if (contentResult && contentResult.count > 0) {
17982
+ return c.html(html`
17983
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
17984
+ Cannot delete collection: it contains ${contentResult.count} content item(s). Delete all content first.
17985
+ </div>
17986
+ `);
17987
+ }
17988
+ const deleteFieldsStmt = db.prepare("DELETE FROM content_fields WHERE collection_id = ?");
17989
+ await deleteFieldsStmt.bind(id).run();
17990
+ const deleteStmt = db.prepare("DELETE FROM collections WHERE id = ?");
17991
+ await deleteStmt.bind(id).run();
17992
+ return c.html(html`
17993
+ <script>
17994
+ window.location.href = '/admin/collections';
17995
+ </script>
17996
+ `);
17997
+ } catch (error) {
17998
+ console.error("Error deleting collection:", error);
17999
+ return c.html(html`
18000
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
18001
+ Failed to delete collection. Please try again.
18002
+ </div>
18003
+ `);
18004
+ }
18005
+ });
18006
+ adminCollectionsRoutes.post("/collections/:id/fields", async (c) => {
18007
+ try {
18008
+ const collectionId = c.req.param("id");
18009
+ const formData = await c.req.formData();
18010
+ const fieldName = formData.get("field_name");
18011
+ const fieldType = formData.get("field_type");
18012
+ const fieldLabel = formData.get("field_label");
18013
+ const isRequired = formData.get("is_required") === "1";
18014
+ const isSearchable = formData.get("is_searchable") === "1";
18015
+ const fieldOptions = formData.get("field_options") || "{}";
18016
+ if (!fieldName || !fieldType || !fieldLabel) {
18017
+ return c.json({ success: false, error: "Field name, type, and label are required." });
18018
+ }
18019
+ if (!/^[a-z0-9_]+$/.test(fieldName)) {
18020
+ return c.json({ success: false, error: "Field name must contain only lowercase letters, numbers, and underscores." });
18021
+ }
18022
+ const db = c.env.DB;
18023
+ const existingStmt = db.prepare("SELECT id FROM content_fields WHERE collection_id = ? AND field_name = ?");
18024
+ const existing = await existingStmt.bind(collectionId, fieldName).first();
18025
+ if (existing) {
18026
+ return c.json({ success: false, error: "A field with this name already exists." });
18027
+ }
18028
+ const orderStmt = db.prepare("SELECT MAX(field_order) as max_order FROM content_fields WHERE collection_id = ?");
18029
+ const orderResult = await orderStmt.bind(collectionId).first();
18030
+ const nextOrder = (orderResult?.max_order || 0) + 1;
18031
+ const fieldId = globalThis.crypto.randomUUID();
18032
+ const now = Date.now();
18033
+ const insertStmt = db.prepare(`
18034
+ INSERT INTO content_fields (
18035
+ id, collection_id, field_name, field_type, field_label,
18036
+ field_options, field_order, is_required, is_searchable,
18037
+ created_at, updated_at
18038
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
18039
+ `);
18040
+ await insertStmt.bind(
18041
+ fieldId,
18042
+ collectionId,
18043
+ fieldName,
18044
+ fieldType,
18045
+ fieldLabel,
18046
+ fieldOptions,
18047
+ nextOrder,
18048
+ isRequired ? 1 : 0,
18049
+ isSearchable ? 1 : 0,
18050
+ now,
18051
+ now
18052
+ ).run();
18053
+ return c.json({ success: true, fieldId });
18054
+ } catch (error) {
18055
+ console.error("Error adding field:", error);
18056
+ return c.json({ success: false, error: "Failed to add field." });
18057
+ }
18058
+ });
18059
+ adminCollectionsRoutes.put("/collections/:collectionId/fields/:fieldId", async (c) => {
18060
+ try {
18061
+ const fieldId = c.req.param("fieldId");
18062
+ const formData = await c.req.formData();
18063
+ const fieldLabel = formData.get("field_label");
18064
+ const isRequired = formData.get("is_required") === "1";
18065
+ const isSearchable = formData.get("is_searchable") === "1";
18066
+ const fieldOptions = formData.get("field_options") || "{}";
18067
+ if (!fieldLabel) {
18068
+ return c.json({ success: false, error: "Field label is required." });
18069
+ }
18070
+ const db = c.env.DB;
18071
+ const updateStmt = db.prepare(`
18072
+ UPDATE content_fields
18073
+ SET field_label = ?, field_options = ?, is_required = ?, is_searchable = ?, updated_at = ?
18074
+ WHERE id = ?
18075
+ `);
18076
+ await updateStmt.bind(fieldLabel, fieldOptions, isRequired ? 1 : 0, isSearchable ? 1 : 0, Date.now(), fieldId).run();
18077
+ return c.json({ success: true });
18078
+ } catch (error) {
18079
+ console.error("Error updating field:", error);
18080
+ return c.json({ success: false, error: "Failed to update field." });
18081
+ }
18082
+ });
18083
+ adminCollectionsRoutes.delete("/collections/:collectionId/fields/:fieldId", async (c) => {
18084
+ try {
18085
+ const fieldId = c.req.param("fieldId");
18086
+ const db = c.env.DB;
18087
+ const deleteStmt = db.prepare("DELETE FROM content_fields WHERE id = ?");
18088
+ await deleteStmt.bind(fieldId).run();
18089
+ return c.json({ success: true });
18090
+ } catch (error) {
18091
+ console.error("Error deleting field:", error);
18092
+ return c.json({ success: false, error: "Failed to delete field." });
18093
+ }
18094
+ });
18095
+ adminCollectionsRoutes.post("/collections/:collectionId/fields/reorder", async (c) => {
18096
+ try {
18097
+ const body = await c.req.json();
18098
+ const fieldIds = body.fieldIds;
18099
+ if (!Array.isArray(fieldIds)) {
18100
+ return c.json({ success: false, error: "Invalid field order data." });
18101
+ }
18102
+ const db = c.env.DB;
18103
+ for (let i = 0; i < fieldIds.length; i++) {
18104
+ const updateStmt = db.prepare("UPDATE content_fields SET field_order = ?, updated_at = ? WHERE id = ?");
18105
+ await updateStmt.bind(i + 1, Date.now(), fieldIds[i]).run();
18106
+ }
18107
+ return c.json({ success: true });
18108
+ } catch (error) {
18109
+ console.error("Error reordering fields:", error);
18110
+ return c.json({ success: false, error: "Failed to reorder fields." });
18111
+ }
18112
+ });
18113
+
18114
+ // src/templates/pages/admin-settings.template.ts
18115
+ init_admin_layout_catalyst_template();
18116
+ function renderSettingsPage(data) {
18117
+ const activeTab = data.activeTab || "general";
18118
+ const pageContent = `
18119
+ <div>
18120
+ <!-- Header -->
18121
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-6">
18122
+ <div>
18123
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Settings</h1>
18124
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage your application settings and preferences</p>
18125
+ </div>
18126
+ <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none flex space-x-3">
18127
+ <button
18128
+ onclick="resetSettings()"
18129
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm"
18130
+ >
18131
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18132
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
18133
+ </svg>
18134
+ Reset to Defaults
18135
+ </button>
18136
+ <button
18137
+ onclick="saveAllSettings()"
18138
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm"
18139
+ >
18140
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18141
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
18142
+ </svg>
18143
+ Save All Changes
18144
+ </button>
18145
+ </div>
18146
+ </div>
18147
+
18148
+ <!-- Settings Navigation Tabs -->
18149
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 mb-6 overflow-hidden">
18150
+ <div class="border-b border-zinc-950/5 dark:border-white/10">
18151
+ <nav class="flex overflow-x-auto" role="tablist">
18152
+ ${renderTabButton("general", "General", "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z", activeTab)}
18153
+ ${renderTabButton("appearance", "Appearance", "M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM21 5a2 2 0 00-2-2h-4a2 2 0 00-2 2v12a4 4 0 004 4h4a2 2 0 002-2V5z", activeTab)}
18154
+ ${renderTabButton("security", "Security", "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z", activeTab)}
18155
+ ${renderTabButton("notifications", "Notifications", "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9", activeTab)}
18156
+ ${renderTabButton("storage", "Storage", "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12", activeTab)}
18157
+ ${renderTabButton("migrations", "Migrations", "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4", activeTab)}
18158
+ ${renderTabButton("database-tools", "Database Tools", "M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01", activeTab)}
18159
+ </nav>
18160
+ </div>
18161
+ </div>
18162
+
18163
+ <!-- Settings Content -->
18164
+ <div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10">
18165
+ <div id="settings-content" class="p-8">
18166
+ ${renderTabContent(activeTab, data.settings)}
18167
+ </div>
18168
+ </div>
18169
+ </div>
18170
+
18171
+ <script>
18172
+ // Initialize tab-specific features on page load
18173
+ const currentTab = '${activeTab}';
18174
+
18175
+ function saveAllSettings() {
18176
+ // Collect all form data
18177
+ const formData = new FormData();
18178
+
18179
+ // Get all form inputs
18180
+ document.querySelectorAll('input, select, textarea').forEach(input => {
18181
+ if (input.type === 'checkbox') {
18182
+ formData.append(input.name, input.checked);
18183
+ } else if (input.name) {
18184
+ formData.append(input.name, input.value);
18185
+ }
18186
+ });
18187
+
18188
+ // Show loading state
18189
+ const saveBtn = document.querySelector('button[onclick="saveAllSettings()"]');
18190
+ const originalText = saveBtn.innerHTML;
18191
+ saveBtn.innerHTML = 'Saving...';
18192
+ saveBtn.disabled = true;
18193
+
18194
+ // Simulate save (replace with actual API call)
18195
+ setTimeout(() => {
18196
+ saveBtn.innerHTML = originalText;
18197
+ saveBtn.disabled = false;
18198
+ showNotification('Settings saved successfully!', 'success');
18199
+ }, 1000);
18200
+ }
18201
+
18202
+ function resetSettings() {
18203
+ showConfirmDialog('reset-settings-confirm');
18204
+ }
18205
+
18206
+ function performResetSettings() {
18207
+ showNotification('Settings reset to defaults', 'info');
18208
+ setTimeout(() => {
18209
+ window.location.reload();
18210
+ }, 1000);
18211
+ }
18212
+
18213
+ // Migration functions
18214
+ window.refreshMigrationStatus = async function() {
18215
+ try {
18216
+ const response = await fetch('/admin/api/migrations/status');
18217
+ const result = await response.json();
18218
+
18219
+ if (result.success) {
18220
+ updateMigrationUI(result.data);
18221
+ } else {
18222
+ console.error('Failed to refresh migration status');
18223
+ }
18224
+ } catch (error) {
18225
+ console.error('Error loading migration status:', error);
18226
+ }
18227
+ };
18228
+
18229
+ window.runPendingMigrations = async function() {
18230
+ const btn = document.getElementById('run-migrations-btn');
18231
+ if (!btn || btn.disabled) return;
18232
+
18233
+ showConfirmDialog('run-migrations-confirm');
18234
+ };
18235
+
18236
+ window.performRunMigrations = async function() {
18237
+ const btn = document.getElementById('run-migrations-btn');
18238
+ if (!btn) return;
18239
+
18240
+ btn.disabled = true;
18241
+ btn.innerHTML = 'Running...';
18242
+
18243
+ try {
18244
+ const response = await fetch('/admin/api/migrations/run', {
18245
+ method: 'POST'
18246
+ });
18247
+ const result = await response.json();
18248
+
18249
+ if (result.success) {
18250
+ alert(result.message);
18251
+ setTimeout(() => window.refreshMigrationStatus(), 1000);
18252
+ } else {
18253
+ alert(result.error || 'Failed to run migrations');
18254
+ }
18255
+ } catch (error) {
18256
+ alert('Error running migrations');
18257
+ } finally {
18258
+ btn.disabled = false;
18259
+ btn.innerHTML = 'Run Pending';
18260
+ }
18261
+ };
18262
+
18263
+ window.validateSchema = async function() {
18264
+ try {
18265
+ const response = await fetch('/admin/api/migrations/validate');
18266
+ const result = await response.json();
18267
+
18268
+ if (result.success) {
18269
+ if (result.data.valid) {
18270
+ alert('Database schema is valid');
18271
+ } else {
18272
+ alert(\`Schema validation failed: \${result.data.issues.join(', ')}\`);
18273
+ }
18274
+ } else {
18275
+ alert('Failed to validate schema');
18276
+ }
18277
+ } catch (error) {
18278
+ alert('Error validating schema');
18279
+ }
18280
+ };
18281
+
18282
+ window.updateMigrationUI = function(data) {
18283
+ const totalEl = document.getElementById('total-migrations');
18284
+ const appliedEl = document.getElementById('applied-migrations');
18285
+ const pendingEl = document.getElementById('pending-migrations');
18286
+
18287
+ if (totalEl) totalEl.textContent = data.totalMigrations;
18288
+ if (appliedEl) appliedEl.textContent = data.appliedMigrations;
18289
+ if (pendingEl) pendingEl.textContent = data.pendingMigrations;
18290
+
18291
+ const runBtn = document.getElementById('run-migrations-btn');
18292
+ if (runBtn) {
18293
+ runBtn.disabled = data.pendingMigrations === 0;
18294
+ }
18295
+
18296
+ // Update migrations list
18297
+ const listContainer = document.getElementById('migrations-list');
18298
+ if (listContainer && data.migrations && data.migrations.length > 0) {
18299
+ listContainer.innerHTML = data.migrations.map(migration => \`
18300
+ <div class="px-6 py-4 flex items-center justify-between">
18301
+ <div class="flex-1">
18302
+ <div class="flex items-center space-x-3">
18303
+ <div class="flex-shrink-0">
18304
+ \${migration.applied
18305
+ ? '<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
18306
+ : '<svg class="w-5 h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
18307
+ }
18308
+ </div>
18309
+ <div>
18310
+ <h5 class="text-white font-medium">\${migration.name}</h5>
18311
+ <p class="text-sm text-gray-300">\${migration.filename}</p>
18312
+ \${migration.description ? \`<p class="text-xs text-gray-400 mt-1">\${migration.description}</p>\` : ''}
18313
+ </div>
18314
+ </div>
18315
+ </div>
18316
+
18317
+ <div class="flex items-center space-x-4 text-sm">
18318
+ \${migration.size ? \`<span class="text-gray-400">\${(migration.size / 1024).toFixed(1)} KB</span>\` : ''}
18319
+ <span class="px-2 py-1 rounded-full text-xs font-medium \${
18320
+ migration.applied
18321
+ ? 'bg-green-100 text-green-800'
18322
+ : 'bg-orange-100 text-orange-800'
18323
+ }">
18324
+ \${migration.applied ? 'Applied' : 'Pending'}
18325
+ </span>
18326
+ \${migration.appliedAt ? \`<span class="text-gray-400">\${new Date(migration.appliedAt).toLocaleDateString()}</span>\` : ''}
18327
+ </div>
18328
+ </div>
18329
+ \`).join('');
18330
+ }
18331
+ };
18332
+
18333
+ // Auto-load migrations when switching to that tab
18334
+ function initializeMigrations() {
18335
+ if (currentTab === 'migrations') {
18336
+ setTimeout(window.refreshMigrationStatus, 500);
18337
+ }
18338
+ }
18339
+
18340
+ // Database Tools functions
18341
+ window.refreshDatabaseStats = async function() {
18342
+ try {
18343
+ const response = await fetch('/admin/database-tools/api/stats');
18344
+ const result = await response.json();
18345
+
18346
+ if (result.success) {
18347
+ updateDatabaseToolsUI(result.data);
18348
+ } else {
18349
+ console.error('Failed to refresh database stats');
18350
+ }
18351
+ } catch (error) {
18352
+ console.error('Error loading database stats:', error);
18353
+ }
18354
+ };
18355
+
18356
+ window.createDatabaseBackup = async function() {
18357
+ const btn = document.getElementById('create-backup-btn');
18358
+ if (!btn) return;
18359
+
18360
+ btn.disabled = true;
18361
+ btn.innerHTML = 'Creating Backup...';
18362
+
18363
+ try {
18364
+ const response = await fetch('/admin/database-tools/api/backup', {
18365
+ method: 'POST'
18366
+ });
18367
+ const result = await response.json();
18368
+
18369
+ if (result.success) {
18370
+ alert(result.message);
18371
+ setTimeout(() => window.refreshDatabaseStats(), 1000);
18372
+ } else {
18373
+ alert(result.error || 'Failed to create backup');
18374
+ }
18375
+ } catch (error) {
18376
+ alert('Error creating backup');
18377
+ } finally {
18378
+ btn.disabled = false;
18379
+ btn.innerHTML = 'Create Backup';
18380
+ }
18381
+ };
18382
+
18383
+ window.truncateDatabase = async function() {
18384
+ // Show dangerous operation warning
18385
+ const confirmText = prompt(
18386
+ 'WARNING: This will delete ALL data except your admin account!\\n\\n' +
18387
+ 'This action CANNOT be undone!\\n\\n' +
18388
+ 'Type "TRUNCATE ALL DATA" to confirm:'
18389
+ );
18390
+
18391
+ if (confirmText !== 'TRUNCATE ALL DATA') {
18392
+ alert('Operation cancelled. Confirmation text did not match.');
18393
+ return;
18394
+ }
18395
+
18396
+ const btn = document.getElementById('truncate-db-btn');
18397
+ if (!btn) return;
18398
+
18399
+ btn.disabled = true;
18400
+ btn.innerHTML = 'Truncating...';
18401
+
18402
+ try {
18403
+ const response = await fetch('/admin/database-tools/api/truncate', {
18404
+ method: 'POST',
18405
+ headers: {
18406
+ 'Content-Type': 'application/json'
18407
+ },
18408
+ body: JSON.stringify({
18409
+ confirmText: confirmText
18410
+ })
18411
+ });
18412
+ const result = await response.json();
18413
+
18414
+ if (result.success) {
18415
+ alert(result.message + '\\n\\nTables cleared: ' + result.data.tablesCleared.join(', '));
18416
+ setTimeout(() => {
18417
+ window.refreshDatabaseStats();
18418
+ // Optionally reload page to refresh all data
18419
+ window.location.reload();
18420
+ }, 2000);
18421
+ } else {
18422
+ alert(result.error || 'Failed to truncate database');
18423
+ }
18424
+ } catch (error) {
18425
+ alert('Error truncating database');
18426
+ } finally {
18427
+ btn.disabled = false;
18428
+ btn.innerHTML = 'Truncate All Data';
18429
+ }
18430
+ };
18431
+
18432
+ window.validateDatabase = async function() {
18433
+ try {
18434
+ const response = await fetch('/admin/database-tools/api/validate');
18435
+ const result = await response.json();
18436
+
18437
+ if (result.success) {
18438
+ if (result.data.valid) {
18439
+ alert('Database validation passed. No issues found.');
18440
+ } else {
18441
+ alert('Database validation failed:\\n\\n' + result.data.issues.join('\\n'));
18442
+ }
18443
+ } else {
18444
+ alert('Failed to validate database');
18445
+ }
18446
+ } catch (error) {
18447
+ alert('Error validating database');
18448
+ }
18449
+ };
18450
+
18451
+ window.updateDatabaseToolsUI = function(data) {
18452
+ const totalTablesEl = document.getElementById('total-tables');
18453
+ const totalRowsEl = document.getElementById('total-rows');
18454
+ const tablesListEl = document.getElementById('tables-list');
18455
+
18456
+ if (totalTablesEl) totalTablesEl.textContent = data.tables.length;
18457
+ if (totalRowsEl) totalRowsEl.textContent = data.totalRows.toLocaleString();
18458
+
18459
+ if (tablesListEl && data.tables && data.tables.length > 0) {
18460
+ tablesListEl.innerHTML = data.tables.map(table => \`
18461
+ <a
18462
+ href="/admin/database-tools/tables/\${table.name}"
18463
+ class="flex items-center justify-between py-3 px-4 rounded-lg bg-white dark:bg-white/5 hover:bg-zinc-50 dark:hover:bg-white/10 cursor-pointer transition-colors ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 no-underline"
18464
+ >
18465
+ <div class="flex items-center space-x-3">
18466
+ <svg class="w-5 h-5 text-zinc-500 dark:text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18467
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/>
18468
+ </svg>
18469
+ <span class="text-zinc-950 dark:text-white font-medium">\${table.name}</span>
18470
+ </div>
18471
+ <div class="flex items-center space-x-3">
18472
+ <span class="text-zinc-500 dark:text-zinc-400 text-sm">\${table.rowCount.toLocaleString()} rows</span>
18473
+ <svg class="w-4 h-4 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18474
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
18475
+ </svg>
18476
+ </div>
18477
+ </a>
18478
+ \`).join('');
18479
+ }
18480
+ };
18481
+
18482
+ // Auto-load tab-specific data after all functions are defined
18483
+ if (currentTab === 'migrations') {
18484
+ setTimeout(window.refreshMigrationStatus, 500);
18485
+ }
18486
+
18487
+ if (currentTab === 'database-tools') {
18488
+ setTimeout(window.refreshDatabaseStats, 500);
18489
+ }
18490
+ </script>
18491
+
18492
+ <!-- Confirmation Dialogs -->
18493
+ ${renderConfirmationDialog2({
18494
+ id: "reset-settings-confirm",
18495
+ title: "Reset Settings",
18496
+ message: "Are you sure you want to reset all settings to their default values? This action cannot be undone.",
18497
+ confirmText: "Reset",
18498
+ cancelText: "Cancel",
18499
+ iconColor: "yellow",
18500
+ confirmClass: "bg-yellow-500 hover:bg-yellow-400",
18501
+ onConfirm: "performResetSettings()"
18502
+ })}
18503
+
18504
+ ${renderConfirmationDialog2({
18505
+ id: "run-migrations-confirm",
18506
+ title: "Run Migrations",
18507
+ message: "Are you sure you want to run pending migrations? This action cannot be undone.",
18508
+ confirmText: "Run Migrations",
18509
+ cancelText: "Cancel",
18510
+ iconColor: "blue",
18511
+ confirmClass: "bg-blue-500 hover:bg-blue-400",
18512
+ onConfirm: "performRunMigrations()"
18513
+ })}
18514
+
18515
+ ${getConfirmationDialogScript2()}
18516
+ `;
18517
+ const layoutData = {
18518
+ title: "Settings",
18519
+ pageTitle: "Settings",
18520
+ currentPath: "/admin/settings",
18521
+ user: data.user,
18522
+ version: data.version,
18523
+ content: pageContent
18524
+ };
18525
+ return renderAdminLayoutCatalyst(layoutData);
18526
+ }
18527
+ function renderTabButton(tabId, label, iconPath, activeTab) {
18528
+ const isActive = activeTab === tabId;
18529
+ const baseClasses = "flex items-center space-x-2 px-4 py-3 text-sm font-medium transition-colors border-b-2 whitespace-nowrap no-underline";
18530
+ const activeClasses = isActive ? "border-zinc-950 dark:border-white text-zinc-950 dark:text-white" : "border-transparent text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-700";
18531
+ return `
18532
+ <a
18533
+ href="/admin/settings/${tabId}"
18534
+ data-tab="${tabId}"
18535
+ class="${baseClasses} ${activeClasses}"
18536
+ >
18537
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18538
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}"/>
18539
+ </svg>
18540
+ <span>${label}</span>
18541
+ </a>
18542
+ `;
18543
+ }
18544
+ function renderTabContent(activeTab, settings) {
18545
+ switch (activeTab) {
18546
+ case "general":
18547
+ return renderGeneralSettings(settings?.general);
18548
+ case "appearance":
18549
+ return renderAppearanceSettings(settings?.appearance);
18550
+ case "security":
18551
+ return renderSecuritySettings(settings?.security);
18552
+ case "notifications":
18553
+ return renderNotificationSettings(settings?.notifications);
18554
+ case "storage":
18555
+ return renderStorageSettings(settings?.storage);
18556
+ case "migrations":
18557
+ return renderMigrationSettings(settings?.migrations);
18558
+ case "database-tools":
18559
+ return renderDatabaseToolsSettings(settings?.databaseTools);
18560
+ default:
18561
+ return renderGeneralSettings(settings?.general);
18562
+ }
18563
+ }
18564
+ function renderGeneralSettings(settings) {
18565
+ return `
18566
+ <div class="space-y-6">
18567
+ <div>
18568
+ <h3 class="text-lg/7 font-semibold text-zinc-950 dark:text-white">General Settings</h3>
18569
+ <p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Configure basic application settings and preferences.</p>
18570
+ </div>
18571
+
18572
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
18573
+ <div class="space-y-4">
18574
+ <div>
18575
+ <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Site Name</label>
18576
+ <input
18577
+ type="text"
18578
+ name="siteName"
18579
+ value="${settings?.siteName || "SonicJS AI"}"
18580
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
18581
+ placeholder="Enter site name"
18582
+ />
18583
+ </div>
18584
+
18585
+ <div>
18586
+ <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Admin Email</label>
18587
+ <input
18588
+ type="email"
18589
+ name="adminEmail"
18590
+ value="${settings?.adminEmail || "admin@example.com"}"
18591
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
18592
+ placeholder="admin@example.com"
18593
+ />
18594
+ </div>
18595
+
18596
+ <div>
18597
+ <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Timezone</label>
18598
+ <select
18599
+ name="timezone"
18600
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
18601
+ >
18602
+ <option value="UTC" ${settings?.timezone === "UTC" ? "selected" : ""}>UTC</option>
18603
+ <option value="America/New_York" ${settings?.timezone === "America/New_York" ? "selected" : ""}>Eastern Time</option>
18604
+ <option value="America/Chicago" ${settings?.timezone === "America/Chicago" ? "selected" : ""}>Central Time</option>
18605
+ <option value="America/Denver" ${settings?.timezone === "America/Denver" ? "selected" : ""}>Mountain Time</option>
18606
+ <option value="America/Los_Angeles" ${settings?.timezone === "America/Los_Angeles" ? "selected" : ""}>Pacific Time</option>
18607
+ </select>
18608
+ </div>
18609
+ </div>
18610
+
18611
+ <div class="space-y-4">
18612
+ <div>
18613
+ <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Site Description</label>
18614
+ <textarea
18615
+ name="siteDescription"
18616
+ rows="3"
18617
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 placeholder:text-zinc-500 dark:placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
18618
+ placeholder="Describe your site..."
18619
+ >${settings?.siteDescription || ""}</textarea>
18620
+ </div>
18621
+
18622
+ <div>
18623
+ <label class="block text-sm/6 font-medium text-zinc-950 dark:text-white mb-2">Language</label>
18624
+ <select
18625
+ name="language"
18626
+ class="w-full rounded-lg bg-white dark:bg-white/5 px-3 py-2 text-sm/6 text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 dark:focus:ring-indigo-400"
18627
+ >
18628
+ <option value="en" ${settings?.language === "en" ? "selected" : ""}>English</option>
18629
+ <option value="es" ${settings?.language === "es" ? "selected" : ""}>Spanish</option>
18630
+ <option value="fr" ${settings?.language === "fr" ? "selected" : ""}>French</option>
18631
+ <option value="de" ${settings?.language === "de" ? "selected" : ""}>German</option>
18632
+ </select>
18633
+ </div>
18634
+
18635
+ <div class="flex gap-3">
18636
+ <div class="flex h-6 shrink-0 items-center">
18637
+ <div class="group grid size-4 grid-cols-1">
18638
+ <input
18639
+ type="checkbox"
18640
+ id="maintenanceMode"
18641
+ name="maintenanceMode"
18642
+ ${settings?.maintenanceMode ? "checked" : ""}
18643
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
18644
+ />
18645
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
18646
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
18647
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
18648
+ </svg>
18649
+ </div>
18650
+ </div>
18651
+ <div class="text-sm/6">
18652
+ <label for="maintenanceMode" class="font-medium text-zinc-950 dark:text-white">
18653
+ Enable maintenance mode
18654
+ </label>
18655
+ </div>
18656
+ </div>
18657
+ </div>
18658
+ </div>
18659
+ </div>
18660
+ `;
18661
+ }
18662
+ function renderAppearanceSettings(settings) {
18663
+ return `
18664
+ <div class="space-y-6">
18665
+ <!-- WIP Notice -->
18666
+ <div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
18667
+ <div class="flex items-start space-x-3">
18668
+ <svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18669
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
18670
+ </svg>
18671
+ <div class="flex-1">
18672
+ <h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
18673
+ <p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
18674
+ This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
18675
+ </p>
18676
+ </div>
18677
+ </div>
18678
+ </div>
18679
+
18680
+ <div>
18681
+ <h3 class="text-lg font-semibold text-white mb-4">Appearance Settings</h3>
18682
+ <p class="text-gray-300 mb-6">Customize the look and feel of your application.</p>
18683
+ </div>
18684
+
18685
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
18686
+ <div class="space-y-4">
18687
+ <div>
18688
+ <label class="block text-sm font-medium text-gray-300 mb-2">Theme</label>
18689
+ <div class="grid grid-cols-3 gap-3">
18690
+ <label class="flex items-center space-x-2 p-3 bg-white/10 rounded-lg border border-white/20 cursor-pointer hover:bg-white/20 transition-colors">
18691
+ <input
18692
+ type="radio"
18693
+ name="theme"
18694
+ value="light"
18695
+ ${settings?.theme === "light" ? "checked" : ""}
18696
+ class="text-blue-600"
18697
+ />
18698
+ <span class="text-sm text-gray-300">Light</span>
18699
+ </label>
18700
+ <label class="flex items-center space-x-2 p-3 bg-white/10 rounded-lg border border-white/20 cursor-pointer hover:bg-white/20 transition-colors">
18701
+ <input
18702
+ type="radio"
18703
+ name="theme"
18704
+ value="dark"
18705
+ ${settings?.theme === "dark" || !settings?.theme ? "checked" : ""}
18706
+ class="text-blue-600"
18707
+ />
18708
+ <span class="text-sm text-gray-300">Dark</span>
18709
+ </label>
18710
+ <label class="flex items-center space-x-2 p-3 bg-white/10 rounded-lg border border-white/20 cursor-pointer hover:bg-white/20 transition-colors">
18711
+ <input
18712
+ type="radio"
18713
+ name="theme"
18714
+ value="auto"
18715
+ ${settings?.theme === "auto" ? "checked" : ""}
18716
+ class="text-blue-600"
18717
+ />
18718
+ <span class="text-sm text-gray-300">Auto</span>
18719
+ </label>
18720
+ </div>
18721
+ </div>
18722
+
18723
+ <div>
18724
+ <label class="block text-sm font-medium text-gray-300 mb-2">Primary Color</label>
18725
+ <div class="flex items-center space-x-3">
18726
+ <input
18727
+ type="color"
18728
+ name="primaryColor"
18729
+ value="${settings?.primaryColor || "#465FFF"}"
18730
+ class="w-12 h-10 bg-white/10 border border-white/20 rounded-lg cursor-pointer"
18731
+ />
18732
+ <input
18733
+ type="text"
18734
+ value="${settings?.primaryColor || "#465FFF"}"
18735
+ class="flex-1 px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
18736
+ placeholder="#465FFF"
18737
+ />
18738
+ </div>
18739
+ </div>
18740
+
18741
+ <div>
18742
+ <label class="block text-sm font-medium text-gray-300 mb-2">Logo URL</label>
18743
+ <input
18744
+ type="url"
18745
+ name="logoUrl"
18746
+ value="${settings?.logoUrl || ""}"
18747
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
18748
+ placeholder="https://example.com/logo.png"
18749
+ />
18750
+ </div>
18751
+ </div>
18752
+
18753
+ <div class="space-y-4">
18754
+ <div>
18755
+ <label class="block text-sm font-medium text-gray-300 mb-2">Favicon URL</label>
18756
+ <input
18757
+ type="url"
18758
+ name="favicon"
18759
+ value="${settings?.favicon || ""}"
18760
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
18761
+ placeholder="https://example.com/favicon.ico"
18762
+ />
18763
+ </div>
18764
+
18765
+ <div>
18766
+ <label class="block text-sm font-medium text-gray-300 mb-2">Custom CSS</label>
18767
+ <textarea
18768
+ name="customCSS"
18769
+ rows="6"
18770
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
18771
+ placeholder="/* Add your custom CSS here */"
18772
+ >${settings?.customCSS || ""}</textarea>
18773
+ </div>
18774
+ </div>
18775
+ </div>
18776
+ </div>
18777
+ `;
18778
+ }
18779
+ function renderSecuritySettings(settings) {
18780
+ return `
18781
+ <div class="space-y-6">
18782
+ <!-- WIP Notice -->
18783
+ <div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
18784
+ <div class="flex items-start space-x-3">
18785
+ <svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18786
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
18787
+ </svg>
18788
+ <div class="flex-1">
18789
+ <h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
18790
+ <p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
18791
+ This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
18792
+ </p>
18793
+ </div>
18794
+ </div>
18795
+ </div>
18796
+
18797
+ <div>
18798
+ <h3 class="text-lg font-semibold text-white mb-4">Security Settings</h3>
18799
+ <p class="text-gray-300 mb-6">Configure security and authentication settings.</p>
18800
+ </div>
18801
+
18802
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
18803
+ <div class="space-y-5">
18804
+ <div class="flex gap-3">
18805
+ <div class="flex h-6 shrink-0 items-center">
18806
+ <div class="group grid size-4 grid-cols-1">
18807
+ <input
18808
+ type="checkbox"
18809
+ id="twoFactorEnabled"
18810
+ name="twoFactorEnabled"
18811
+ ${settings?.twoFactorEnabled ? "checked" : ""}
18812
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
18813
+ />
18814
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
18815
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
18816
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
18817
+ </svg>
18818
+ </div>
18819
+ </div>
18820
+ <div class="text-sm/6">
18821
+ <label for="twoFactorEnabled" class="font-medium text-zinc-950 dark:text-white">
18822
+ Enable Two-Factor Authentication
18823
+ </label>
18824
+ </div>
18825
+ </div>
18826
+
18827
+ <div>
18828
+ <label class="block text-sm font-medium text-gray-300 mb-2">Session Timeout (minutes)</label>
18829
+ <input
18830
+ type="number"
18831
+ name="sessionTimeout"
18832
+ value="${settings?.sessionTimeout || 30}"
18833
+ min="5"
18834
+ max="1440"
18835
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
18836
+ />
18837
+ </div>
18838
+
18839
+ <div>
18840
+ <label class="block text-sm font-medium text-gray-300 mb-2">Password Requirements</label>
18841
+ <div class="space-y-3">
18842
+ <div class="flex gap-3">
18843
+ <div class="flex h-6 shrink-0 items-center">
18844
+ <div class="group grid size-4 grid-cols-1">
18845
+ <input
18846
+ type="checkbox"
18847
+ id="requireUppercase"
18848
+ name="requireUppercase"
18849
+ ${settings?.passwordRequirements?.requireUppercase ? "checked" : ""}
18850
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
18851
+ />
18852
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
18853
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
18854
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
18855
+ </svg>
18856
+ </div>
18857
+ </div>
18858
+ <div class="text-sm/6">
18859
+ <label for="requireUppercase" class="font-medium text-zinc-950 dark:text-white">Require uppercase letters</label>
18860
+ </div>
18861
+ </div>
18862
+ <div class="flex gap-3">
18863
+ <div class="flex h-6 shrink-0 items-center">
18864
+ <div class="group grid size-4 grid-cols-1">
18865
+ <input
18866
+ type="checkbox"
18867
+ id="requireNumbers"
18868
+ name="requireNumbers"
18869
+ ${settings?.passwordRequirements?.requireNumbers ? "checked" : ""}
18870
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
18871
+ />
18872
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
18873
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
18874
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
18875
+ </svg>
18876
+ </div>
18877
+ </div>
18878
+ <div class="text-sm/6">
18879
+ <label for="requireNumbers" class="font-medium text-zinc-950 dark:text-white">Require numbers</label>
18880
+ </div>
18881
+ </div>
18882
+ <div class="flex gap-3">
18883
+ <div class="flex h-6 shrink-0 items-center">
18884
+ <div class="group grid size-4 grid-cols-1">
18885
+ <input
18886
+ type="checkbox"
18887
+ id="requireSymbols"
18888
+ name="requireSymbols"
18889
+ ${settings?.passwordRequirements?.requireSymbols ? "checked" : ""}
18890
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
18891
+ />
18892
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
18893
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
18894
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
18895
+ </svg>
18896
+ </div>
18897
+ </div>
18898
+ <div class="text-sm/6">
18899
+ <label for="requireSymbols" class="font-medium text-zinc-950 dark:text-white">Require symbols</label>
18900
+ </div>
18901
+ </div>
18902
+ </div>
18903
+ </div>
18904
+ </div>
18905
+
18906
+ <div class="space-y-4">
18907
+ <div>
18908
+ <label class="block text-sm font-medium text-gray-300 mb-2">Minimum Password Length</label>
18909
+ <input
18910
+ type="number"
18911
+ name="minPasswordLength"
18912
+ value="${settings?.passwordRequirements?.minLength || 8}"
18913
+ min="6"
18914
+ max="128"
18915
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
18916
+ />
18917
+ </div>
18918
+
18919
+ <div>
18920
+ <label class="block text-sm font-medium text-gray-300 mb-2">IP Whitelist</label>
18921
+ <textarea
18922
+ name="ipWhitelist"
18923
+ rows="4"
18924
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
18925
+ placeholder="Enter IP addresses (one per line)&#10;192.168.1.1&#10;10.0.0.1"
18926
+ >${settings?.ipWhitelist?.join("\n") || ""}</textarea>
18927
+ <p class="text-xs text-gray-400 mt-1">Leave empty to allow all IPs</p>
18928
+ </div>
18929
+ </div>
18930
+ </div>
18931
+ </div>
18932
+ `;
18933
+ }
18934
+ function renderNotificationSettings(settings) {
18935
+ return `
18936
+ <div class="space-y-6">
18937
+ <!-- WIP Notice -->
18938
+ <div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
18939
+ <div class="flex items-start space-x-3">
18940
+ <svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18941
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
18942
+ </svg>
18943
+ <div class="flex-1">
18944
+ <h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
18945
+ <p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
18946
+ This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
18947
+ </p>
18948
+ </div>
18949
+ </div>
18950
+ </div>
18951
+
18952
+ <div>
18953
+ <h3 class="text-lg font-semibold text-white mb-4">Notification Settings</h3>
18954
+ <p class="text-gray-300 mb-6">Configure how and when you receive notifications.</p>
18955
+ </div>
18956
+
18957
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
18958
+ <div class="space-y-4">
18959
+ <div>
18960
+ <h4 class="text-md font-medium text-white mb-3">Email Notifications</h4>
18961
+ <div class="space-y-5">
18962
+ <div class="flex gap-3">
18963
+ <div class="flex h-6 shrink-0 items-center">
18964
+ <div class="group grid size-4 grid-cols-1">
18965
+ <input
18966
+ type="checkbox"
18967
+ id="emailNotifications"
18968
+ name="emailNotifications"
18969
+ ${settings?.emailNotifications ? "checked" : ""}
18970
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
18971
+ />
18972
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
18973
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
18974
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
18975
+ </svg>
18976
+ </div>
18977
+ </div>
18978
+ <div class="text-sm/6">
18979
+ <label for="emailNotifications" class="font-medium text-zinc-950 dark:text-white">Enable email notifications</label>
18980
+ </div>
18981
+ </div>
18982
+
18983
+ <div class="flex gap-3">
18984
+ <div class="flex h-6 shrink-0 items-center">
18985
+ <div class="group grid size-4 grid-cols-1">
18986
+ <input
18987
+ type="checkbox"
18988
+ id="contentUpdates"
18989
+ name="contentUpdates"
18990
+ ${settings?.contentUpdates ? "checked" : ""}
18991
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
18992
+ />
18993
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
18994
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
18995
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
18996
+ </svg>
18997
+ </div>
18998
+ </div>
18999
+ <div class="text-sm/6">
19000
+ <label for="contentUpdates" class="font-medium text-zinc-950 dark:text-white">Content updates</label>
19001
+ </div>
19002
+ </div>
19003
+
19004
+ <div class="flex gap-3">
19005
+ <div class="flex h-6 shrink-0 items-center">
19006
+ <div class="group grid size-4 grid-cols-1">
19007
+ <input
19008
+ type="checkbox"
19009
+ id="systemAlerts"
19010
+ name="systemAlerts"
19011
+ ${settings?.systemAlerts ? "checked" : ""}
19012
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
19013
+ />
19014
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
19015
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
19016
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
19017
+ </svg>
19018
+ </div>
19019
+ </div>
19020
+ <div class="text-sm/6">
19021
+ <label for="systemAlerts" class="font-medium text-zinc-950 dark:text-white">System alerts</label>
19022
+ </div>
19023
+ </div>
19024
+
19025
+ <div class="flex gap-3">
19026
+ <div class="flex h-6 shrink-0 items-center">
19027
+ <div class="group grid size-4 grid-cols-1">
19028
+ <input
19029
+ type="checkbox"
19030
+ id="userRegistrations"
19031
+ name="userRegistrations"
19032
+ ${settings?.userRegistrations ? "checked" : ""}
19033
+ class="col-start-1 row-start-1 appearance-none rounded border border-zinc-950/10 dark:border-white/10 bg-white dark:bg-white/5 checked:border-indigo-500 checked:bg-indigo-500 indeterminate:border-indigo-500 indeterminate:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:border-zinc-950/5 dark:disabled:border-white/5 disabled:bg-zinc-950/10 dark:disabled:bg-white/10 disabled:checked:bg-zinc-950/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
19034
+ />
19035
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-zinc-950/25 dark:group-has-[:disabled]:stroke-white/25">
19036
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
19037
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
19038
+ </svg>
19039
+ </div>
19040
+ </div>
19041
+ <div class="text-sm/6">
19042
+ <label for="userRegistrations" class="font-medium text-zinc-950 dark:text-white">New user registrations</label>
19043
+ </div>
19044
+ </div>
19045
+ </div>
19046
+ </div>
19047
+ </div>
19048
+
19049
+ <div class="space-y-4">
19050
+ <div>
19051
+ <label class="block text-sm font-medium text-gray-300 mb-2">Email Frequency</label>
19052
+ <select
19053
+ name="emailFrequency"
19054
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
19055
+ >
19056
+ <option value="immediate" ${settings?.emailFrequency === "immediate" ? "selected" : ""}>Immediate</option>
19057
+ <option value="daily" ${settings?.emailFrequency === "daily" ? "selected" : ""}>Daily Digest</option>
19058
+ <option value="weekly" ${settings?.emailFrequency === "weekly" ? "selected" : ""}>Weekly Digest</option>
19059
+ </select>
19060
+ </div>
19061
+
19062
+ <div class="p-4 bg-blue-500/20 border border-blue-500/30 rounded-lg">
19063
+ <div class="flex items-start space-x-3">
19064
+ <svg class="w-5 h-5 text-blue-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19065
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
19066
+ </svg>
19067
+ <div>
19068
+ <h5 class="text-sm font-medium text-blue-300">Notification Preferences</h5>
19069
+ <p class="text-xs text-blue-200 mt-1">
19070
+ Critical system alerts will always be sent immediately regardless of your frequency setting.
19071
+ </p>
19072
+ </div>
19073
+ </div>
19074
+ </div>
19075
+ </div>
19076
+ </div>
19077
+ </div>
19078
+ `;
19079
+ }
19080
+ function renderStorageSettings(settings) {
19081
+ return `
19082
+ <div class="space-y-6">
19083
+ <!-- WIP Notice -->
19084
+ <div class="rounded-lg bg-blue-50 dark:bg-blue-950/20 p-6 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-500/30">
19085
+ <div class="flex items-start space-x-3">
19086
+ <svg class="w-6 h-6 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19087
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
19088
+ </svg>
19089
+ <div class="flex-1">
19090
+ <h4 class="text-base/7 font-semibold text-blue-900 dark:text-blue-300">Work in Progress</h4>
19091
+ <p class="mt-1 text-sm/6 text-blue-700 dark:text-blue-200">
19092
+ This settings section is currently under development and provided for reference and design feedback only. Changes made here will not be saved.
19093
+ </p>
19094
+ </div>
19095
+ </div>
19096
+ </div>
19097
+
19098
+ <div>
19099
+ <h3 class="text-lg font-semibold text-white mb-4">Storage Settings</h3>
19100
+ <p class="text-gray-300 mb-6">Configure file storage and backup settings.</p>
19101
+ </div>
19102
+
19103
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
19104
+ <div class="space-y-4">
19105
+ <div>
19106
+ <label class="block text-sm font-medium text-gray-300 mb-2">Max File Size (MB)</label>
19107
+ <input
19108
+ type="number"
19109
+ name="maxFileSize"
19110
+ value="${settings?.maxFileSize || 10}"
19111
+ min="1"
19112
+ max="100"
19113
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
19114
+ />
19115
+ </div>
19116
+
19117
+ <div>
19118
+ <label class="block text-sm font-medium text-gray-300 mb-2">Storage Provider</label>
19119
+ <select
19120
+ name="storageProvider"
19121
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
19122
+ >
19123
+ <option value="local" ${settings?.storageProvider === "local" ? "selected" : ""}>Local Storage</option>
19124
+ <option value="cloudflare" ${settings?.storageProvider === "cloudflare" ? "selected" : ""}>Cloudflare R2</option>
19125
+ <option value="s3" ${settings?.storageProvider === "s3" ? "selected" : ""}>Amazon S3</option>
19126
+ </select>
19127
+ </div>
19128
+
19129
+ <div>
19130
+ <label class="block text-sm font-medium text-gray-300 mb-2">Backup Frequency</label>
19131
+ <select
19132
+ name="backupFrequency"
19133
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
19134
+ >
19135
+ <option value="daily" ${settings?.backupFrequency === "daily" ? "selected" : ""}>Daily</option>
19136
+ <option value="weekly" ${settings?.backupFrequency === "weekly" ? "selected" : ""}>Weekly</option>
19137
+ <option value="monthly" ${settings?.backupFrequency === "monthly" ? "selected" : ""}>Monthly</option>
19138
+ </select>
19139
+ </div>
19140
+ </div>
19141
+
19142
+ <div class="space-y-4">
19143
+ <div>
19144
+ <label class="block text-sm font-medium text-gray-300 mb-2">Allowed File Types</label>
19145
+ <textarea
19146
+ name="allowedFileTypes"
19147
+ rows="3"
19148
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
19149
+ placeholder="jpg, jpeg, png, gif, pdf, docx"
19150
+ >${settings?.allowedFileTypes?.join(", ") || "jpg, jpeg, png, gif, pdf, docx"}</textarea>
19151
+ </div>
19152
+
19153
+ <div>
19154
+ <label class="block text-sm font-medium text-gray-300 mb-2">Backup Retention (days)</label>
19155
+ <input
19156
+ type="number"
19157
+ name="retentionPeriod"
19158
+ value="${settings?.retentionPeriod || 30}"
19159
+ min="7"
19160
+ max="365"
19161
+ class="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
19162
+ />
19163
+ </div>
19164
+
19165
+ <div class="p-4 bg-green-500/20 border border-green-500/30 rounded-lg">
19166
+ <div class="flex items-start space-x-3">
19167
+ <svg class="w-5 h-5 text-green-400 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19168
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
19169
+ </svg>
19170
+ <div>
19171
+ <h5 class="text-sm font-medium text-green-300">Storage Status</h5>
19172
+ <p class="text-xs text-green-200 mt-1">
19173
+ Current usage: 2.4 GB / 10 GB available
19174
+ </p>
19175
+ </div>
19176
+ </div>
19177
+ </div>
19178
+ </div>
19179
+ </div>
19180
+ </div>
19181
+ `;
19182
+ }
19183
+ function renderMigrationSettings(settings) {
19184
+ return `
19185
+ <div class="space-y-6">
19186
+ <div>
19187
+ <h3 class="text-lg font-semibold text-white mb-4">Database Migrations</h3>
19188
+ <p class="text-gray-300 mb-6">View and manage database migrations to keep your schema up to date.</p>
19189
+ </div>
19190
+
19191
+ <!-- Migration Status Overview -->
19192
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
19193
+ <div class="backdrop-blur-md bg-blue-500/20 rounded-lg border border-blue-500/30 p-4">
19194
+ <div class="flex items-center justify-between">
19195
+ <div>
19196
+ <p class="text-sm text-blue-300">Total Migrations</p>
19197
+ <p id="total-migrations" class="text-2xl font-bold text-white">${settings?.totalMigrations || "0"}</p>
19198
+ </div>
19199
+ <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19200
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
19201
+ </svg>
19202
+ </div>
19203
+ </div>
19204
+
19205
+ <div class="backdrop-blur-md bg-green-500/20 rounded-lg border border-green-500/30 p-4">
19206
+ <div class="flex items-center justify-between">
19207
+ <div>
19208
+ <p class="text-sm text-green-300">Applied</p>
19209
+ <p id="applied-migrations" class="text-2xl font-bold text-white">${settings?.appliedMigrations || "0"}</p>
19210
+ </div>
19211
+ <svg class="w-8 h-8 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19212
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
19213
+ </svg>
19214
+ </div>
19215
+ </div>
19216
+
19217
+ <div class="backdrop-blur-md bg-orange-500/20 rounded-lg border border-orange-500/30 p-4">
19218
+ <div class="flex items-center justify-between">
19219
+ <div>
19220
+ <p class="text-sm text-orange-300">Pending</p>
19221
+ <p id="pending-migrations" class="text-2xl font-bold text-white">${settings?.pendingMigrations || "0"}</p>
19222
+ </div>
19223
+ <svg class="w-8 h-8 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19224
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
19225
+ </svg>
19226
+ </div>
19227
+ </div>
19228
+ </div>
19229
+
19230
+ <!-- Migration Actions -->
19231
+ <div class="flex items-center space-x-4 mb-6">
19232
+ <button
19233
+ onclick="window.refreshMigrationStatus()"
19234
+ class="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
19235
+ >
19236
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19237
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
19238
+ </svg>
19239
+ Refresh Status
19240
+ </button>
19241
+
19242
+ <button
19243
+ onclick="window.runPendingMigrations()"
19244
+ id="run-migrations-btn"
19245
+ class="inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors"
19246
+ ${(settings?.pendingMigrations || 0) === 0 ? "disabled" : ""}
19247
+ >
19248
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19249
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1.586a1 1 0 01.707.293l2.414 2.414a1 1 0 00.707.293H15M9 10v4.586a1 1 0 00.293.707l2.414 2.414a1 1 0 00.707.293H15M9 10V9a2 2 0 012-2h2a2 2 0 012 2v1"/>
19250
+ </svg>
19251
+ Run Pending
19252
+ </button>
19253
+
19254
+ <button
19255
+ onclick="window.validateSchema()"
19256
+ class="inline-flex items-center px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors"
19257
+ >
19258
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19259
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
19260
+ </svg>
19261
+ Validate Schema
19262
+ </button>
19263
+ </div>
19264
+
19265
+ <!-- Migrations List -->
19266
+ <div class="backdrop-blur-md bg-white/10 rounded-lg border border-white/20 overflow-hidden">
19267
+ <div class="px-6 py-4 border-b border-white/10">
19268
+ <h4 class="text-lg font-medium text-white">Migration History</h4>
19269
+ <p class="text-sm text-gray-300 mt-1">List of all available database migrations</p>
19270
+ </div>
19271
+
19272
+ <div id="migrations-list" class="divide-y divide-white/10">
19273
+ <div class="px-6 py-8 text-center">
19274
+ <svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19275
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
19276
+ </svg>
19277
+ <p class="text-gray-300">Loading migration status...</p>
19278
+ </div>
19279
+ </div>
19280
+ </div>
19281
+ </div>
19282
+
19283
+ <script>
19284
+ // Load migration status when tab becomes active
19285
+ if (typeof refreshMigrationStatus === 'undefined') {
19286
+ window.refreshMigrationStatus = async function() {
19287
+ try {
19288
+ const response = await fetch('/admin/api/migrations/status');
19289
+ const result = await response.json();
19290
+
19291
+ if (result.success) {
19292
+ updateMigrationUI(result.data);
19293
+ } else {
19294
+ console.error('Failed to refresh migration status');
19295
+ }
19296
+ } catch (error) {
19297
+ console.error('Error loading migration status:', error);
19298
+ }
19299
+ };
19300
+
19301
+ window.runPendingMigrations = async function() {
19302
+ const btn = document.getElementById('run-migrations-btn');
19303
+ if (!btn || btn.disabled) return;
19304
+
19305
+ showConfirmDialog('run-migrations-confirm');
19306
+ };
19307
+
19308
+ window.performRunMigrations = async function() {
19309
+ const btn = document.getElementById('run-migrations-btn');
19310
+ if (!btn) return;
19311
+
19312
+ btn.disabled = true;
19313
+ btn.innerHTML = 'Running...';
19314
+
19315
+ try {
19316
+ const response = await fetch('/admin/api/migrations/run', {
19317
+ method: 'POST'
19318
+ });
19319
+ const result = await response.json();
19320
+
19321
+ if (result.success) {
19322
+ alert(result.message);
19323
+ setTimeout(() => window.refreshMigrationStatus(), 1000);
19324
+ } else {
19325
+ alert(result.error || 'Failed to run migrations');
19326
+ }
19327
+ } catch (error) {
19328
+ alert('Error running migrations');
19329
+ } finally {
19330
+ btn.disabled = false;
19331
+ btn.innerHTML = 'Run Pending';
19332
+ }
19333
+ };
19334
+
19335
+ window.validateSchema = async function() {
19336
+ try {
19337
+ const response = await fetch('/admin/api/migrations/validate');
19338
+ const result = await response.json();
19339
+
19340
+ if (result.success) {
19341
+ if (result.data.valid) {
19342
+ alert('Database schema is valid');
19343
+ } else {
19344
+ alert(\`Schema validation failed: \${result.data.issues.join(', ')}\`);
19345
+ }
19346
+ } else {
19347
+ alert('Failed to validate schema');
19348
+ }
19349
+ } catch (error) {
19350
+ alert('Error validating schema');
19351
+ }
19352
+ };
19353
+
19354
+ window.updateMigrationUI = function(data) {
19355
+ const totalEl = document.getElementById('total-migrations');
19356
+ const appliedEl = document.getElementById('applied-migrations');
19357
+ const pendingEl = document.getElementById('pending-migrations');
19358
+
19359
+ if (totalEl) totalEl.textContent = data.totalMigrations;
19360
+ if (appliedEl) appliedEl.textContent = data.appliedMigrations;
19361
+ if (pendingEl) pendingEl.textContent = data.pendingMigrations;
19362
+
19363
+ const runBtn = document.getElementById('run-migrations-btn');
19364
+ if (runBtn) {
19365
+ runBtn.disabled = data.pendingMigrations === 0;
19366
+ }
19367
+
19368
+ // Update migrations list
19369
+ const listContainer = document.getElementById('migrations-list');
19370
+ if (listContainer && data.migrations && data.migrations.length > 0) {
19371
+ listContainer.innerHTML = data.migrations.map(migration => \`
19372
+ <div class="px-6 py-4 flex items-center justify-between">
19373
+ <div class="flex-1">
19374
+ <div class="flex items-center space-x-3">
19375
+ <div class="flex-shrink-0">
19376
+ \${migration.applied
19377
+ ? '<svg class="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
19378
+ : '<svg class="w-5 h-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
19379
+ }
19380
+ </div>
19381
+ <div>
19382
+ <h5 class="text-white font-medium">\${migration.name}</h5>
19383
+ <p class="text-sm text-gray-300">\${migration.filename}</p>
19384
+ \${migration.description ? \`<p class="text-xs text-gray-400 mt-1">\${migration.description}</p>\` : ''}
19385
+ </div>
19386
+ </div>
19387
+ </div>
19388
+
19389
+ <div class="flex items-center space-x-4 text-sm">
19390
+ \${migration.size ? \`<span class="text-gray-400">\${(migration.size / 1024).toFixed(1)} KB</span>\` : ''}
19391
+ <span class="px-2 py-1 rounded-full text-xs font-medium \${
19392
+ migration.applied
19393
+ ? 'bg-green-100 text-green-800'
19394
+ : 'bg-orange-100 text-orange-800'
19395
+ }">
19396
+ \${migration.applied ? 'Applied' : 'Pending'}
19397
+ </span>
19398
+ \${migration.appliedAt ? \`<span class="text-gray-400">\${new Date(migration.appliedAt).toLocaleDateString()}</span>\` : ''}
19399
+ </div>
19400
+ </div>
19401
+ \`).join('');
19402
+ }
19403
+ };
19404
+ }
19405
+
19406
+ // Auto-load when tab becomes active
19407
+ if (currentTab === 'migrations') {
19408
+ setTimeout(refreshMigrationStatus, 500);
19409
+ }
19410
+ </script>
19411
+ `;
19412
+ }
19413
+ function renderDatabaseToolsSettings(settings) {
19414
+ return `
19415
+ <div class="space-y-6">
19416
+ <div>
19417
+ <h3 class="text-lg/7 font-semibold text-zinc-950 dark:text-white">Database Tools</h3>
19418
+ <p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Manage database operations including backup, restore, and maintenance.</p>
19419
+ </div>
19420
+
19421
+ <!-- Database Statistics -->
19422
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
19423
+ <div class="rounded-lg bg-white dark:bg-white/5 p-6 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10">
19424
+ <div class="flex items-center justify-between">
19425
+ <div>
19426
+ <p class="text-sm/6 font-medium text-zinc-500 dark:text-zinc-400">Total Tables</p>
19427
+ <p id="total-tables" class="mt-2 text-3xl/8 font-semibold text-zinc-950 dark:text-white">${settings?.totalTables || "0"}</p>
19428
+ </div>
19429
+ <div class="rounded-lg bg-indigo-500/10 p-3">
19430
+ <svg class="w-8 h-8 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19431
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
19432
+ </svg>
19433
+ </div>
19434
+ </div>
19435
+ </div>
19436
+
19437
+ <div class="rounded-lg bg-white dark:bg-white/5 p-6 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10">
19438
+ <div class="flex items-center justify-between">
19439
+ <div>
19440
+ <p class="text-sm/6 font-medium text-zinc-500 dark:text-zinc-400">Total Rows</p>
19441
+ <p id="total-rows" class="mt-2 text-3xl/8 font-semibold text-zinc-950 dark:text-white">${settings?.totalRows?.toLocaleString() || "0"}</p>
19442
+ </div>
19443
+ <div class="rounded-lg bg-green-500/10 p-3">
19444
+ <svg class="w-8 h-8 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19445
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
19446
+ </svg>
19447
+ </div>
19448
+ </div>
19449
+ </div>
19450
+ </div>
19451
+
19452
+ <!-- Database Operations -->
19453
+ <div class="space-y-4">
19454
+ <!-- Safe Operations -->
19455
+ <div class="rounded-lg bg-white dark:bg-white/5 p-6 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10">
19456
+ <h4 class="text-base/7 font-semibold text-zinc-950 dark:text-white mb-4">Safe Operations</h4>
19457
+ <div class="flex flex-wrap gap-3">
19458
+ <button
19459
+ onclick="window.refreshDatabaseStats()"
19460
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors shadow-sm"
19461
+ >
19462
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19463
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
19464
+ </svg>
19465
+ Refresh Stats
19466
+ </button>
19467
+
19468
+ <button
19469
+ onclick="window.createDatabaseBackup()"
19470
+ id="create-backup-btn"
19471
+ class="inline-flex items-center justify-center rounded-lg bg-indigo-600 dark:bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-indigo-500 dark:hover:bg-indigo-400 transition-colors shadow-sm"
19472
+ >
19473
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19474
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
19475
+ </svg>
19476
+ Create Backup
19477
+ </button>
19478
+
19479
+ <button
19480
+ onclick="window.validateDatabase()"
19481
+ class="inline-flex items-center justify-center rounded-lg bg-green-600 dark:bg-green-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-green-500 dark:hover:bg-green-400 transition-colors shadow-sm"
19482
+ >
19483
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19484
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
19485
+ </svg>
19486
+ Validate Database
19487
+ </button>
19488
+ </div>
19489
+ </div>
19490
+ </div>
19491
+
19492
+ <!-- Tables List -->
19493
+ <div class="rounded-lg bg-white dark:bg-white/5 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 overflow-hidden">
19494
+ <div class="px-6 py-4 border-b border-zinc-950/10 dark:border-white/10">
19495
+ <h4 class="text-base/7 font-semibold text-zinc-950 dark:text-white">Database Tables</h4>
19496
+ <p class="mt-1 text-sm/6 text-zinc-500 dark:text-zinc-400">Click on a table to view its data</p>
19497
+ </div>
19498
+
19499
+ <div id="tables-list" class="p-6 space-y-2">
19500
+ <div class="text-center py-8">
19501
+ <svg class="w-12 h-12 text-zinc-400 dark:text-zinc-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19502
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
19503
+ </svg>
19504
+ <p class="text-zinc-500 dark:text-zinc-400">Loading database statistics...</p>
19505
+ </div>
19506
+ </div>
19507
+ </div>
19508
+
19509
+ <!-- Danger Zone -->
19510
+ <div class="rounded-lg bg-red-50 dark:bg-red-950/20 p-6 ring-1 ring-inset ring-red-600/20 dark:ring-red-500/30">
19511
+ <div class="flex items-start space-x-3">
19512
+ <svg class="w-6 h-6 text-red-600 dark:text-red-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19513
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.268 16.5c-.77.833.192 2.5 1.732 2.5z"/>
19514
+ </svg>
19515
+ <div class="flex-1">
19516
+ <h4 class="text-base/7 font-semibold text-red-900 dark:text-red-400">Danger Zone</h4>
19517
+ <p class="mt-1 text-sm/6 text-red-700 dark:text-red-300">
19518
+ These operations are destructive and cannot be undone.
19519
+ <strong>Your admin account will be preserved</strong>, but all other data will be permanently deleted.
19520
+ </p>
19521
+ <div class="mt-4">
19522
+ <button
19523
+ onclick="window.truncateDatabase()"
19524
+ id="truncate-db-btn"
19525
+ class="inline-flex items-center justify-center rounded-lg bg-red-600 dark:bg-red-500 px-3.5 py-2.5 text-sm font-semibold text-white hover:bg-red-500 dark:hover:bg-red-400 transition-colors shadow-sm"
19526
+ >
19527
+ <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19528
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
19529
+ </svg>
19530
+ Truncate All Data
19531
+ </button>
19532
+ </div>
19533
+ </div>
19534
+ </div>
19535
+ </div>
19536
+ </div>
19537
+ `;
19538
+ }
19539
+
19540
+ // src/routes/admin-settings.ts
19541
+ var adminSettingsRoutes = new Hono();
19542
+ function getMockSettings(user) {
19543
+ return {
19544
+ general: {
19545
+ siteName: "SonicJS AI",
19546
+ siteDescription: "A modern headless CMS powered by AI",
19547
+ adminEmail: user?.email || "admin@example.com",
19548
+ timezone: "UTC",
19549
+ language: "en",
19550
+ maintenanceMode: false
19551
+ },
19552
+ appearance: {
19553
+ theme: "dark",
19554
+ primaryColor: "#465FFF",
19555
+ logoUrl: "",
19556
+ favicon: "",
19557
+ customCSS: ""
19558
+ },
19559
+ security: {
19560
+ twoFactorEnabled: false,
19561
+ sessionTimeout: 30,
19562
+ passwordRequirements: {
19563
+ minLength: 8,
19564
+ requireUppercase: true,
19565
+ requireNumbers: true,
19566
+ requireSymbols: false
19567
+ },
19568
+ ipWhitelist: []
19569
+ },
19570
+ notifications: {
19571
+ emailNotifications: true,
19572
+ contentUpdates: true,
19573
+ systemAlerts: true,
19574
+ userRegistrations: false,
19575
+ emailFrequency: "immediate"
19576
+ },
19577
+ storage: {
19578
+ maxFileSize: 10,
19579
+ allowedFileTypes: ["jpg", "jpeg", "png", "gif", "pdf", "docx"],
19580
+ storageProvider: "cloudflare",
19581
+ backupFrequency: "daily",
19582
+ retentionPeriod: 30
19583
+ },
19584
+ migrations: {
19585
+ totalMigrations: 0,
19586
+ appliedMigrations: 0,
19587
+ pendingMigrations: 0,
19588
+ lastApplied: void 0,
19589
+ migrations: []
19590
+ },
19591
+ databaseTools: {
19592
+ totalTables: 0,
19593
+ totalRows: 0,
19594
+ lastBackup: void 0,
19595
+ databaseSize: "0 MB",
19596
+ tables: []
19597
+ }
19598
+ };
19599
+ }
19600
+ adminSettingsRoutes.get("/settings", (c) => {
19601
+ return c.redirect("/admin/settings/general");
19602
+ });
19603
+ adminSettingsRoutes.get("/settings/general", (c) => {
19604
+ const user = c.get("user");
19605
+ const pageData = {
19606
+ user: user ? {
19607
+ name: user.email,
19608
+ email: user.email,
19609
+ role: user.role
19610
+ } : void 0,
19611
+ settings: getMockSettings(user),
19612
+ activeTab: "general",
19613
+ version: c.get("appVersion")
19614
+ };
19615
+ return c.html(renderSettingsPage(pageData));
19616
+ });
19617
+ adminSettingsRoutes.get("/settings/appearance", (c) => {
19618
+ const user = c.get("user");
19619
+ const pageData = {
19620
+ user: user ? {
19621
+ name: user.email,
19622
+ email: user.email,
19623
+ role: user.role
19624
+ } : void 0,
19625
+ settings: getMockSettings(user),
19626
+ activeTab: "appearance",
19627
+ version: c.get("appVersion")
19628
+ };
19629
+ return c.html(renderSettingsPage(pageData));
19630
+ });
19631
+ adminSettingsRoutes.get("/settings/security", (c) => {
19632
+ const user = c.get("user");
19633
+ const pageData = {
19634
+ user: user ? {
19635
+ name: user.email,
19636
+ email: user.email,
19637
+ role: user.role
19638
+ } : void 0,
19639
+ settings: getMockSettings(user),
19640
+ activeTab: "security",
19641
+ version: c.get("appVersion")
19642
+ };
19643
+ return c.html(renderSettingsPage(pageData));
19644
+ });
19645
+ adminSettingsRoutes.get("/settings/notifications", (c) => {
19646
+ const user = c.get("user");
19647
+ const pageData = {
19648
+ user: user ? {
19649
+ name: user.email,
19650
+ email: user.email,
19651
+ role: user.role
19652
+ } : void 0,
19653
+ settings: getMockSettings(user),
19654
+ activeTab: "notifications",
19655
+ version: c.get("appVersion")
19656
+ };
19657
+ return c.html(renderSettingsPage(pageData));
19658
+ });
19659
+ adminSettingsRoutes.get("/settings/storage", (c) => {
19660
+ const user = c.get("user");
19661
+ const pageData = {
19662
+ user: user ? {
19663
+ name: user.email,
19664
+ email: user.email,
19665
+ role: user.role
19666
+ } : void 0,
19667
+ settings: getMockSettings(user),
19668
+ activeTab: "storage",
19669
+ version: c.get("appVersion")
19670
+ };
19671
+ return c.html(renderSettingsPage(pageData));
19672
+ });
19673
+ adminSettingsRoutes.get("/settings/migrations", (c) => {
19674
+ const user = c.get("user");
19675
+ const pageData = {
19676
+ user: user ? {
19677
+ name: user.email,
19678
+ email: user.email,
19679
+ role: user.role
19680
+ } : void 0,
19681
+ settings: getMockSettings(user),
19682
+ activeTab: "migrations",
19683
+ version: c.get("appVersion")
19684
+ };
19685
+ return c.html(renderSettingsPage(pageData));
19686
+ });
19687
+ adminSettingsRoutes.get("/settings/database-tools", (c) => {
19688
+ const user = c.get("user");
19689
+ const pageData = {
19690
+ user: user ? {
19691
+ name: user.email,
19692
+ email: user.email,
19693
+ role: user.role
19694
+ } : void 0,
19695
+ settings: getMockSettings(user),
19696
+ activeTab: "database-tools",
19697
+ version: c.get("appVersion")
19698
+ };
19699
+ return c.html(renderSettingsPage(pageData));
19700
+ });
19701
+ adminSettingsRoutes.get("/api/migrations/status", async (c) => {
19702
+ try {
19703
+ const db = c.env.DB;
19704
+ const migrationService = new MigrationService(db);
19705
+ const status = await migrationService.getMigrationStatus();
19706
+ return c.json({
19707
+ success: true,
19708
+ data: status
19709
+ });
19710
+ } catch (error) {
19711
+ console.error("Error fetching migration status:", error);
19712
+ return c.json({
19713
+ success: false,
19714
+ error: "Failed to fetch migration status"
19715
+ }, 500);
19716
+ }
19717
+ });
19718
+ adminSettingsRoutes.post("/api/migrations/run", async (c) => {
19719
+ try {
19720
+ const user = c.get("user");
19721
+ if (!user || user.role !== "admin") {
19722
+ return c.json({
19723
+ success: false,
19724
+ error: "Unauthorized. Admin access required."
19725
+ }, 403);
19726
+ }
19727
+ const db = c.env.DB;
19728
+ const migrationService = new MigrationService(db);
19729
+ const result = await migrationService.runPendingMigrations();
19730
+ return c.json({
19731
+ success: result.success,
19732
+ message: result.message,
19733
+ applied: result.applied
19734
+ });
19735
+ } catch (error) {
19736
+ console.error("Error running migrations:", error);
19737
+ return c.json({
19738
+ success: false,
19739
+ error: "Failed to run migrations"
19740
+ }, 500);
19741
+ }
19742
+ });
19743
+ adminSettingsRoutes.get("/api/migrations/validate", async (c) => {
19744
+ try {
19745
+ const db = c.env.DB;
19746
+ const migrationService = new MigrationService(db);
19747
+ const validation = await migrationService.validateSchema();
19748
+ return c.json({
19749
+ success: true,
19750
+ data: validation
19751
+ });
19752
+ } catch (error) {
19753
+ console.error("Error validating schema:", error);
19754
+ return c.json({
19755
+ success: false,
19756
+ error: "Failed to validate schema"
19757
+ }, 500);
19758
+ }
19759
+ });
19760
+ adminSettingsRoutes.post("/settings", async (c) => {
19761
+ try {
19762
+ const formData = await c.req.formData();
19763
+ return c.html(html`
19764
+ <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
19765
+ Settings saved successfully!
19766
+ <script>
19767
+ setTimeout(() => {
19768
+ showNotification('Settings saved successfully!', 'success');
19769
+ }, 100);
19770
+ </script>
19771
+ </div>
19772
+ `);
19773
+ } catch (error) {
19774
+ console.error("Error saving settings:", error);
19775
+ return c.html(html`
19776
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
19777
+ Failed to save settings. Please try again.
19778
+ </div>
19779
+ `);
19780
+ }
19781
+ });
19782
+
15756
19783
  // src/routes/index.ts
15757
19784
  var ROUTES_INFO = {
15758
19785
  message: "Core routes available",
@@ -15772,12 +19799,15 @@ var ROUTES_INFO = {
15772
19799
  "adminCheckboxRoutes",
15773
19800
  "adminFAQRoutes",
15774
19801
  "adminTestimonialsRoutes",
15775
- "adminCodeExamplesRoutes"
19802
+ "adminCodeExamplesRoutes",
19803
+ "adminDashboardRoutes",
19804
+ "adminCollectionsRoutes",
19805
+ "adminSettingsRoutes"
15776
19806
  ],
15777
19807
  status: "Core package routes ready",
15778
19808
  reference: "https://github.com/sonicjs/sonicjs"
15779
19809
  };
15780
19810
 
15781
- export { ROUTES_INFO, adminCheckboxRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_faq_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, userRoutes };
15782
- //# sourceMappingURL=chunk-JETM2U2D.js.map
15783
- //# sourceMappingURL=chunk-JETM2U2D.js.map
19811
+ export { ROUTES_INFO, adminCheckboxRoutes, adminCollectionsRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_api_default, admin_code_examples_default, admin_content_default, admin_faq_default, admin_testimonials_default, api_content_crud_default, api_default, api_media_default, api_system_default, auth_default, router, userRoutes };
19812
+ //# sourceMappingURL=chunk-QSF34IYQ.js.map
19813
+ //# sourceMappingURL=chunk-QSF34IYQ.js.map