diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3e98b63..e9a1ec7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -39,7 +39,7 @@ val projectNoticeText =
// A release workflow can derive the app version from its Git tag without editing the
// source tree. Local builds keep using the checked-in default.
-val defaultVersionName = "0.2.9"
+val defaultVersionName = "0.2.10"
val membyVersionName: String =
(project.findProperty("memby.versionName") as String?)
?.trim()
diff --git a/server/internal/api/admin.go b/server/internal/api/admin.go
index a9c4089..63cf468 100644
--- a/server/internal/api/admin.go
+++ b/server/internal/api/admin.go
@@ -32,6 +32,11 @@ func (s *Server) adminRoutes() http.Handler {
mux.HandleFunc("GET /admin/{$}", s.handleAdminRoot)
mux.HandleFunc("GET /admin/{page}", s.handleAdminPage)
mux.Handle("GET /admin/api/status", s.adminAuth(s.handleAdminStatus))
+ mux.Handle("GET /admin/api/accounts", s.adminAuth(s.handleAdminAccounts))
+ mux.Handle("PUT /admin/api/accounts/{userID}/devices/{deviceID}", s.adminAuth(s.handleAdminRenameDevice))
+ mux.Handle("DELETE /admin/api/accounts/{userID}/devices/{deviceID}", s.adminAuth(s.handleAdminDeleteDevice))
+ mux.Handle("DELETE /admin/api/accounts/{userID}/sessions", s.adminAuth(s.handleAdminDeleteAccount))
+ mux.Handle("DELETE /admin/api/accounts/{userID}/recommendations", s.adminAuth(s.handleAdminResetRecommendations))
mux.Handle("GET /admin/api/recommendations", s.adminAuth(s.handleAdminRecommendations))
mux.Handle("GET /admin/api/analytics", s.adminAuth(s.handleAdminAnalytics))
mux.Handle("GET /admin/api/events", s.adminAuth(s.handleAdminEvents))
@@ -50,7 +55,7 @@ func (s *Server) adminRoutes() http.Handler {
}
var adminPages = map[string]bool{
- "library": true, "recommendations": true, "requests": true,
+ "accounts": true, "library": true, "recommendations": true, "requests": true,
"features": true, "playback": true, "maintenance": true, "updates": true, "engagement": true,
"ratings": true, "imports": true, "logs": true,
}
diff --git a/server/internal/api/admin.html b/server/internal/api/admin.html
index 0d3ec04..430e388 100644
--- a/server/internal/api/admin.html
+++ b/server/internal/api/admin.html
@@ -138,6 +138,34 @@
.component.negative { color:#ffaaa3; background:#3a1d1c; }
.recommendation-meta { color:var(--muted); font-size:12px; margin-top:4px; }
.reason-text { margin-top:9px; color:#dce3e8; }
+ .account-summary {
+ display:grid; grid-template-columns:repeat(3,minmax(150px,1fr)); gap:12px; margin-bottom:16px;
+ }
+ .account-stat { padding:15px; border:1px solid var(--line); border-radius:10px; background:#10151a; }
+ .account-stat b { display:block; font-size:24px; }
+ .account-stat span { color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.06em; }
+ .account-list { display:grid; gap:12px; }
+ .account-card { border:1px solid var(--line); border-radius:11px; background:#10151a; overflow:hidden; }
+ .account-head { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; padding:17px; }
+ .account-name { display:flex; align-items:center; gap:12px; }
+ .account-avatar { display:grid; place-items:center; width:42px; height:42px; border-radius:50%; background:var(--accent-soft); color:#91df94; font-size:17px; font-weight:800; }
+ .account-name h3 { margin:0; font-size:17px; }
+ .account-name p { margin:2px 0 0; color:var(--muted); font-size:12px; }
+ .account-actions { display:flex; gap:8px; flex-wrap:wrap; justify-content:flex-end; }
+ .account-actions button,.device-actions button { padding:7px 10px; font-size:12px; }
+ .account-body { display:grid; grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr); border-top:1px solid var(--line); }
+ .account-pane { padding:16px 17px; min-width:0; }
+ .account-pane + .account-pane { border-left:1px solid var(--line); }
+ .account-pane h4 { margin:0 0 10px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.07em; }
+ .device-row { display:grid; grid-template-columns:minmax(150px,1fr) auto; gap:12px; align-items:center; padding:10px 0; border-top:1px solid #202830; }
+ .device-row:first-of-type { border-top:0; padding-top:0; }
+ .device-title { font-weight:650; }
+ .device-meta { color:var(--muted); font-size:12px; margin-top:2px; }
+ .device-actions { display:flex; gap:6px; }
+ .choice-list { display:flex; gap:6px; flex-wrap:wrap; }
+ .choice { padding:4px 8px; border-radius:6px; background:#202830; color:#dce3e8; font-size:12px; }
+ .choice.rating { background:#302b16; color:#f2d575; }
+ .account-note { padding:12px 14px; border:1px solid #2b4830; border-radius:8px; background:rgba(82,181,75,.07); color:#b9c6bc; font-size:13px; margin-bottom:16px; }
.feature-hero {
display:grid; grid-template-columns:minmax(0,1fr) auto; gap:22px; align-items:center;
padding:20px; margin-bottom:16px; border:1px solid #2b4830; border-radius:11px;
@@ -176,6 +204,8 @@
.event-attrs { grid-column:1 / -1; }
.recommendation-controls { grid-template-columns:repeat(2,minmax(150px,1fr)); }
.feature-grid { grid-template-columns:1fr; }
+ .account-body { grid-template-columns:1fr; }
+ .account-pane + .account-pane { border-left:0; border-top:1px solid var(--line); }
}
@media (max-width: 560px) {
input[type=text], input[type=password], input[type=number], input[type=datetime-local],
@@ -184,6 +214,9 @@
.stats { gap: 18px; }
.recommendation-controls { grid-template-columns:1fr; }
.feature-hero { grid-template-columns:1fr; }
+ .account-summary { grid-template-columns:1fr; }
+ .account-head,.device-row { grid-template-columns:1fr; display:grid; }
+ .account-actions { justify-content:flex-start; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
@@ -199,6 +232,9 @@
Manage