Skip to content

Commit df50bda

Browse files
committed
feat: add admin Database Explorer page with D3 schema diagram
New /admin/database page with: - Interactive D3.js force-directed schema diagram showing all tables colored by plugin ownership (core, calendar, maps, sessions, etc.) - Foreign key relationship lines with arrowheads between tables - Click-to-inspect table detail panel (columns, types, keys, sizes) - Plugin migration status cards showing current/latest versions - "Apply Pending Migrations" button (fixes sessions table issue) - Zoom/pan/drag, auto-fit, dark mode support, plugin color legend Also: - Remove "Manual DB Record (Advanced)" form from Features page - Add LatestMigrationVersion() to database package - Add Database card to admin dashboard - Add Database link to admin sidebar nav https://claude.ai/code/session_01QJLkgjQDu5qohzJKGV4hj9
1 parent 24ff70f commit df50bda

13 files changed

Lines changed: 1008 additions & 78 deletions

File tree

.ai/status.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
<!-- ====================================================================== -->
99

1010
## Last Updated
11+
2026-03-11 -- **Sprint W-0.5: Visual Customization + Admin DB Explorer (IN PROGRESS).**
12+
13+
26. Admin Database Explorer: New `/admin/database` page with interactive D3.js schema diagram showing all tables, FK relationships, plugin grouping, and migration status. Table detail panel on click. "Apply Pending Migrations" button (fixes sessions table missing issue). Removed "Manual DB Record (Advanced)" form from Features page. New files: `database_service.go` (info_schema introspection), `database.templ`, `db_explorer.js` widget. Added `LatestMigrationVersion()` to database package. Dashboard card + sidebar link.
14+
15+
### Previous Update
1116
2026-03-11 -- **Sprint W-0.5: Visual Customization (IN PROGRESS).**
1217

1318
25. Starting W-0.5: Per-campaign brand name/logo, topbar color/gradient/image customization, visual editor Appearance tab in Customization Hub. Also fixing 3 bugs from W-0 (event listener leak in sidebar_tree.js, touch listener cleanup in sidebar_reorg.js, ES2020 optional chaining compat) and updating stale entity/campaign documentation.

internal/app/routes.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,10 @@ func (a *App) RegisterRoutes() {
861861
hygieneScanner := admin.NewHygieneService(a.DB, mediaRepo, mediaService, a.Config.Upload.MediaPath, securityRepo)
862862
adminHandler.SetHygieneScanner(hygieneScanner)
863863

864+
// Database explorer: schema visualization and migration management.
865+
dbExplorer := admin.NewDatabaseExplorer(a.DB, a.PluginHealth)
866+
adminHandler.SetDatabaseExplorer(dbExplorer)
867+
864868
// Wire security event logging into the auth handler so logins, logouts,
865869
// failed attempts, and password resets are recorded automatically.
866870
authHandler.SetSecurityLogger(securityService)

internal/database/plugin_schema.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,42 @@ func splitPluginStatements(sql string) []string {
323323
return stmts
324324
}
325325

326+
// LatestMigrationVersion returns the highest migration version number found
327+
// in a plugin's migrations directory. Returns 0 if no migrations exist.
328+
func LatestMigrationVersion(migrationsDir string) (int, error) {
329+
if _, err := os.Stat(migrationsDir); err != nil {
330+
if os.IsNotExist(err) {
331+
return 0, nil
332+
}
333+
return 0, err
334+
}
335+
336+
entries, err := os.ReadDir(migrationsDir)
337+
if err != nil {
338+
return 0, err
339+
}
340+
341+
highest := 0
342+
for _, entry := range entries {
343+
if entry.IsDir() {
344+
continue
345+
}
346+
matches := pluginMigrationFileRe.FindStringSubmatch(entry.Name())
347+
if len(matches) < 2 {
348+
continue
349+
}
350+
v, err := strconv.Atoi(matches[1])
351+
if err != nil {
352+
continue
353+
}
354+
if v > highest {
355+
highest = v
356+
}
357+
}
358+
359+
return highest, nil
360+
}
361+
326362
// RegisteredPlugins returns the list of built-in plugins that have schema
327363
// migrations. The MigrationsDir paths are relative to the working directory.
328364
func RegisteredPlugins() []PluginSchema {

internal/plugins/addons/admin_addons.templ

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -68,80 +68,6 @@ templ AdminAddonsPageTempl(addons []Addon, csrfToken string) {
6868
</div>
6969
}
7070

71-
// Direct DB record creation — admin failover only.
72-
<details class="group">
73-
<summary class="card p-4 cursor-pointer border-2 border-red-300 dark:border-red-800 bg-red-50/50 dark:bg-red-950/20 list-none">
74-
<div class="flex items-center gap-3">
75-
<span class="w-8 h-8 rounded-lg bg-red-100 dark:bg-red-900/40 flex items-center justify-center shrink-0">
76-
<i class="fa-solid fa-triangle-exclamation text-red-600 dark:text-red-400"></i>
77-
</span>
78-
<div>
79-
<h3 class="text-sm font-semibold text-red-700 dark:text-red-400">Manual DB Record (Advanced)</h3>
80-
<p class="text-xs text-red-600/70 dark:text-red-400/60">
81-
Creates a raw addon database record. Use only as a failover — features should come from plugins or extensions.
82-
</p>
83-
</div>
84-
<i class="fa-solid fa-chevron-down text-red-400 ml-auto transition-transform group-open:rotate-180"></i>
85-
</div>
86-
</summary>
87-
<div class="card p-6 mt-1 border-2 border-red-300 dark:border-red-800">
88-
<div class="flex items-start gap-3 p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 mb-4">
89-
<i class="fa-solid fa-exclamation-circle text-red-500 mt-0.5"></i>
90-
<p class="text-xs text-red-700 dark:text-red-300 leading-relaxed">
91-
This creates a metadata-only record with no backing code. The record cannot be activated
92-
until matching plugin code is deployed. Prefer installing extensions via the Content Packs
93-
page instead.
94-
</p>
95-
</div>
96-
<form
97-
method="POST"
98-
action="/admin/addons"
99-
hx-post="/admin/addons"
100-
class="grid grid-cols-1 md:grid-cols-2 gap-4"
101-
>
102-
<input type="hidden" name="csrf_token" value={ csrfToken }/>
103-
<div>
104-
<label for="slug" class="block text-sm font-medium text-fg-body mb-1">Slug</label>
105-
<input type="text" id="slug" name="slug" required class="input w-full" placeholder="my-addon"/>
106-
</div>
107-
<div>
108-
<label for="name" class="block text-sm font-medium text-fg-body mb-1">Name</label>
109-
<input type="text" id="name" name="name" required class="input w-full" placeholder="My Addon"/>
110-
</div>
111-
<div class="md:col-span-2">
112-
<label for="description" class="block text-sm font-medium text-fg-body mb-1">Description</label>
113-
<textarea id="description" name="description" class="input w-full h-20" placeholder="What does this addon do?"></textarea>
114-
</div>
115-
<div>
116-
<label for="version" class="block text-sm font-medium text-fg-body mb-1">Version</label>
117-
<input type="text" id="version" name="version" class="input w-full" placeholder="1.0.0"/>
118-
</div>
119-
<div>
120-
<label for="category" class="block text-sm font-medium text-fg-body mb-1">Category</label>
121-
<select id="category" name="category" required class="input w-full">
122-
<option value="plugin">Feature</option>
123-
<option value="widget">Widget</option>
124-
<option value="integration">Integration</option>
125-
</select>
126-
</div>
127-
<div>
128-
<label for="icon" class="block text-sm font-medium text-fg-body mb-1">Icon</label>
129-
<input type="text" id="icon" name="icon" class="input w-full" placeholder="fa-puzzle-piece"/>
130-
</div>
131-
<div>
132-
<label for="author" class="block text-sm font-medium text-fg-body mb-1">Author</label>
133-
<input type="text" id="author" name="author" class="input w-full" placeholder="Chronicle"/>
134-
</div>
135-
<div class="md:col-span-2 flex justify-end">
136-
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors">
137-
<i class="fa-solid fa-database text-xs"></i>
138-
Create DB Record
139-
</button>
140-
</div>
141-
</form>
142-
</div>
143-
</details>
144-
14571
// Info panel.
14672
<div class="card p-6">
14773
<div class="flex items-start gap-4">

internal/plugins/admin/.ai.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ Plugin (Infrastructure -- admin-only)
2121

2222
| File | Purpose |
2323
|------|---------|
24-
| handler.go | Dashboard, Users, ToggleAdmin, Campaigns, DeleteCampaign, JoinCampaign, LeaveCampaign, Modules |
24+
| handler.go | Dashboard, Users, ToggleAdmin, Campaigns, DeleteCampaign, JoinCampaign, LeaveCampaign, Modules, Database, DatabaseSchemaAPI, ApplyMigrationsAPI |
2525
| routes.go | /admin group with auth + admin middleware, delegates SMTP/settings/storage routes |
26-
| dashboard.templ | Overview stats (user count, campaign count, SMTP status, modules) |
26+
| database_service.go | DatabaseExplorer interface + info_schema introspection + migration status |
27+
| dashboard.templ | Overview stats (user count, campaign count, SMTP status, modules, database) |
2728
| users.templ | Paginated user list with admin toggle buttons |
2829
| campaigns.templ | All campaigns with join/leave/delete actions |
2930
| modules.templ | Module management page (card grid, status badges, content categories) |
3031
| storage.templ | Combined storage usage + storage settings (tabbed page) |
32+
| database.templ | Schema explorer with migration cards + D3 widget mount |
3133

3234
## Dependencies
3335

@@ -50,6 +52,9 @@ Plugin (Infrastructure -- admin-only)
5052
| POST | /admin/smtp/test | (SMTP handler) | Test SMTP connection |
5153
| GET | /admin/modules | Modules | Module management page |
5254
| GET | /admin/storage | (Storage handler) | Combined storage + settings page |
55+
| GET | /admin/database | Database | Schema explorer + migration status |
56+
| GET | /admin/database/schema | DatabaseSchemaAPI | Schema JSON for D3 widget |
57+
| POST | /admin/database/migrations/apply | ApplyMigrationsAPI | Run pending plugin migrations |
5358

5459
## Current State
5560

@@ -62,4 +67,5 @@ Plugin (Infrastructure -- admin-only)
6267
- [x] Module registry (`internal/modules/registry.go`) with D&D 5e, Pathfinder 2e, Draw Steel
6368
- [x] Dashboard uses semantic color tokens for dark mode
6469
- [x] Collapsible sidebar with modules section
70+
- [x] Database explorer (D3.js schema diagram, migration status, apply migrations)
6571
- [ ] Tests written

internal/plugins/admin/dashboard.templ

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ templ AdminDashboardPage(userCount, campaignCount, mediaFileCount int, totalStor
108108
<p class="text-lg font-semibold text-fg mt-2">Orphan Cleanup</p>
109109
<p class="text-xs text-fg-muted mt-1 group-hover:text-accent transition-colors">Scan &amp; clean &rarr;</p>
110110
</a>
111+
<!-- Database -->
112+
<a href="/admin/database" class="card hover:shadow-md transition-shadow group">
113+
<div class="flex items-center justify-between">
114+
<p class="text-sm font-medium text-fg-secondary">Database</p>
115+
<span class="w-10 h-10 rounded-lg bg-slate-50 dark:bg-slate-900/30 flex items-center justify-center">
116+
<i class="fa-solid fa-database text-slate-500"></i>
117+
</span>
118+
</div>
119+
<p class="text-lg font-semibold text-fg mt-2">Schema Explorer</p>
120+
<p class="text-xs text-fg-muted mt-1 group-hover:text-accent transition-colors">Tables &amp; migrations &rarr;</p>
121+
</a>
111122
<!-- SMTP -->
112123
<a href="/admin/smtp" class="card hover:shadow-md transition-shadow group">
113124
<div class="flex items-center justify-between">
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// database.templ renders the admin database explorer page with migration
2+
// status cards, an interactive D3.js schema diagram, and a table detail panel.
3+
4+
package admin
5+
6+
import (
7+
"fmt"
8+
"github.com/keyxmakerx/chronicle/internal/templates/layouts"
9+
)
10+
11+
// AdminDatabasePage renders the database explorer with migration status and schema visualization.
12+
templ AdminDatabasePage(statuses []PluginMigrationStatus, tableCount int, csrfToken string) {
13+
@layouts.App("Database - Admin") {
14+
<div class="max-w-7xl mx-auto space-y-6">
15+
<div class="flex items-center justify-between">
16+
<div>
17+
<h1 class="text-2xl font-bold text-fg">Database Explorer</h1>
18+
<p class="text-sm text-fg-secondary mt-1">
19+
{ fmt.Sprintf("%d", tableCount) } tables &middot; Schema visualization and migration management.
20+
</p>
21+
</div>
22+
<a href="/admin" class="text-sm text-fg-muted hover:text-accent">
23+
<i class="fa-solid fa-arrow-left mr-1"></i> Back to Dashboard
24+
</a>
25+
</div>
26+
27+
<!-- Migration Status -->
28+
if len(statuses) > 0 {
29+
<div>
30+
<div class="flex items-center justify-between mb-3">
31+
<h2 class="section-header">Plugin Migrations</h2>
32+
if hasPendingMigrations(statuses) {
33+
<button
34+
hx-post="/admin/database/migrations/apply"
35+
hx-confirm="Apply all pending plugin migrations? This will create new database tables."
36+
hx-headers={ fmt.Sprintf(`{"X-CSRF-Token":"%s"}`, csrfToken) }
37+
class="btn btn-sm btn-primary"
38+
>
39+
<i class="fa-solid fa-play mr-1"></i> Apply Pending Migrations
40+
</button>
41+
}
42+
</div>
43+
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
44+
for _, s := range statuses {
45+
@migrationStatusCard(s)
46+
}
47+
</div>
48+
</div>
49+
}
50+
51+
<!-- Schema Diagram -->
52+
<div class="card p-0 overflow-hidden">
53+
<div class="px-5 py-3 border-b border-edge flex items-center justify-between">
54+
<h2 class="text-sm font-semibold text-fg">Schema Diagram</h2>
55+
<span class="text-xs text-fg-muted">Scroll to zoom &middot; Drag to pan &middot; Click a table for details</span>
56+
</div>
57+
<div
58+
data-widget="db-explorer"
59+
data-api-url="/admin/database/schema"
60+
style="min-height: 600px;"
61+
></div>
62+
</div>
63+
64+
<!-- Table Detail Panel (populated by JS on node click) -->
65+
<div id="table-detail" class="card hidden">
66+
<p class="text-sm text-fg-muted p-6">Click a table in the diagram above to see its details.</p>
67+
</div>
68+
</div>
69+
}
70+
}
71+
72+
// migrationStatusCard renders a card for a single plugin's migration status.
73+
templ migrationStatusCard(s PluginMigrationStatus) {
74+
<div class="card p-4">
75+
<div class="flex items-center justify-between mb-2">
76+
<span class="text-sm font-semibold text-fg capitalize">{ s.Slug }</span>
77+
if s.Healthy {
78+
<span class="w-2.5 h-2.5 rounded-full bg-emerald-500" title="Healthy"></span>
79+
} else {
80+
<span class="w-2.5 h-2.5 rounded-full bg-red-500" title="Unhealthy"></span>
81+
}
82+
</div>
83+
<div class="text-xs text-fg-secondary">
84+
<span>v{ fmt.Sprintf("%d", s.CurrentVersion) }</span>
85+
<span class="text-fg-muted"> / { fmt.Sprintf("%d", s.LatestVersion) }</span>
86+
</div>
87+
if s.Pending > 0 {
88+
<div class="mt-2">
89+
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400">
90+
{ fmt.Sprintf("%d", s.Pending) } pending
91+
</span>
92+
</div>
93+
}
94+
if s.Error != "" {
95+
<p class="text-xs text-red-500 mt-2 truncate" title={ s.Error }>{ s.Error }</p>
96+
}
97+
</div>
98+
}
99+
100+
// hasPendingMigrations returns true if any plugin has pending migrations.
101+
func hasPendingMigrations(statuses []PluginMigrationStatus) bool {
102+
for _, s := range statuses {
103+
if s.Pending > 0 {
104+
return true
105+
}
106+
}
107+
return false
108+
}

0 commit comments

Comments
 (0)