Skip to content

Commit fac1bdf

Browse files
authored
Merge pull request #116 from keyxmakerx/claude/fix-navbar-features-page-8RZuE
Claude/fix navbar features page 8 r zu e
2 parents 77d0fae + 3134747 commit fac1bdf

14 files changed

Lines changed: 1220 additions & 81 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: 9 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)
@@ -1291,6 +1295,11 @@ func (a *App) RegisterRoutes() {
12911295
ctx = layouts.SetUserName(ctx, session.Name)
12921296
ctx = layouts.SetUserEmail(ctx, session.Email)
12931297
ctx = layouts.SetIsAdmin(ctx, session.IsAdmin)
1298+
1299+
// Inject degraded plugin count for admin sidebar badge.
1300+
if session.IsAdmin {
1301+
ctx = layouts.SetDegradedPluginCount(ctx, len(a.PluginHealth.DegradedPlugins()))
1302+
}
12941303
}
12951304

12961305
// Campaign info from campaign middleware.

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: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,33 @@ package admin
22

33
import (
44
"fmt"
5+
"strings"
56
"github.com/keyxmakerx/chronicle/internal/templates/layouts"
67
)
78

89
// AdminDashboardPage renders the admin overview with high-level stats.
9-
templ AdminDashboardPage(userCount, campaignCount, mediaFileCount int, totalStorageBytes int64, smtpConfigured bool, addonCount int, securityStats *SecurityStats) {
10+
templ AdminDashboardPage(userCount, campaignCount, mediaFileCount int, totalStorageBytes int64, smtpConfigured bool, addonCount int, securityStats *SecurityStats, degradedPlugins []string) {
1011
@layouts.App("Admin Dashboard") {
1112
<div class="max-w-5xl mx-auto space-y-6">
1213
<h1 class="text-2xl font-bold text-fg">Admin Dashboard</h1>
1314

15+
<!-- Degraded Plugin Alert Banner -->
16+
if len(degradedPlugins) > 0 {
17+
<a href="/admin/database" class="flex items-start gap-3 p-4 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 hover:bg-red-100 dark:hover:bg-red-950/50 transition-colors">
18+
<i class="fa-solid fa-triangle-exclamation text-red-500 mt-0.5 shrink-0"></i>
19+
<div>
20+
<p class="text-sm font-semibold text-red-700 dark:text-red-400">
21+
{ fmt.Sprintf("%d", len(degradedPlugins)) } plugin(s) need attention
22+
</p>
23+
<p class="text-xs text-red-600/80 dark:text-red-400/70 mt-0.5">
24+
{ strings.Join(degradedPlugins, ", ") } &mdash; pending migrations or schema errors.
25+
Click to view the Database Explorer.
26+
</p>
27+
</div>
28+
<i class="fa-solid fa-arrow-right text-red-400 mt-0.5 ml-auto shrink-0"></i>
29+
</a>
30+
}
31+
1432
<!-- Stats Cards -->
1533
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
1634
<!-- Users -->
@@ -108,6 +126,25 @@ templ AdminDashboardPage(userCount, campaignCount, mediaFileCount int, totalStor
108126
<p class="text-lg font-semibold text-fg mt-2">Orphan Cleanup</p>
109127
<p class="text-xs text-fg-muted mt-1 group-hover:text-accent transition-colors">Scan &amp; clean &rarr;</p>
110128
</a>
129+
<!-- Database -->
130+
<a href="/admin/database" class="card hover:shadow-md transition-shadow group">
131+
<div class="flex items-center justify-between">
132+
<p class="text-sm font-medium text-fg-secondary">Database</p>
133+
<span class="w-10 h-10 rounded-lg bg-slate-50 dark:bg-slate-900/30 flex items-center justify-center">
134+
<i class="fa-solid fa-database text-slate-500"></i>
135+
</span>
136+
</div>
137+
if len(degradedPlugins) > 0 {
138+
<p class="text-lg font-semibold text-amber-600 dark:text-amber-400 mt-2">
139+
{ fmt.Sprintf("%d", len(degradedPlugins)) } plugin(s) degraded
140+
</p>
141+
<p class="text-xs text-fg-muted mt-1">Pending migrations or errors</p>
142+
} else {
143+
<p class="text-lg font-semibold text-emerald-600 dark:text-emerald-400 mt-2">All Healthy</p>
144+
<p class="text-xs text-fg-muted mt-1">All migrations applied</p>
145+
}
146+
<p class="text-xs text-fg-muted group-hover:text-accent transition-colors">Schema explorer &rarr;</p>
147+
</a>
111148
<!-- SMTP -->
112149
<a href="/admin/smtp" class="card hover:shadow-md transition-shadow group">
113150
<div class="flex items-center justify-between">

0 commit comments

Comments
 (0)