From 9cdbe37aadcd40424c7cb114b3d71ff6f53f0cb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 07:53:42 +0000 Subject: [PATCH] Fix admin console for mobile, remove Blocked Users from admin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Console rewritten: - No JavaScript at all. Plain form POST → redirect with results. - Works on every device. Type command, tap Run, see output. - Previous command stays in the input for easy editing. - Help shown as compact one-liner below the form. Admin dashboard: - Removed Blocked Users (it's a user feature in Account → Settings, not an admin function. Blocklist is the admin mail/IP block tool). https://claude.ai/code/session_01GRGLA9yj7BpqKiyi6xFwnm --- admin/admin.go | 1 - admin/console.go | 183 ++++++++++++++++------------------------------- 2 files changed, 60 insertions(+), 124 deletions(-) diff --git a/admin/admin.go b/admin/admin.go index ae2b80d3..a38de8cb 100644 --- a/admin/admin.go +++ b/admin/admin.go @@ -25,7 +25,6 @@ func AdminHandler(w http.ResponseWriter, r *http.Request) { AI Usage API Log Blocklist - Blocked Users Console Environment Mail Log diff --git a/admin/console.go b/admin/console.go index e8d3bc78..973e435d 100644 --- a/admin/console.go +++ b/admin/console.go @@ -1,9 +1,9 @@ package admin import ( - "encoding/json" "fmt" "net/http" + "net/url" "strings" "mu/internal/app" @@ -12,7 +12,7 @@ import ( "mu/wallet" ) -// ConsoleHandler provides an admin REPL for managing the system. +// ConsoleHandler provides an admin console for managing the system. func ConsoleHandler(w http.ResponseWriter, r *http.Request) { _, _, err := auth.RequireAdmin(r) if err != nil { @@ -20,187 +20,124 @@ func ConsoleHandler(w http.ResponseWriter, r *http.Request) { return } + // Handle command + cmd := "" + output := "" if r.Method == "POST" { - handleConsoleCommand(w, r) + r.ParseForm() + cmd = strings.TrimSpace(r.FormValue("cmd")) + if cmd != "" { + output = runCommand(cmd) + } + // Redirect with results to prevent form resubmission + http.Redirect(w, r, "/admin/console?cmd="+url.QueryEscape(cmd)+"&output="+url.QueryEscape(output), http.StatusSeeOther) return } - prevOutput := r.URL.Query().Get("output") + // GET — show form + results from redirect prevCmd := r.URL.Query().Get("cmd") + prevOutput := r.URL.Query().Get("output") var sb strings.Builder - sb.WriteString(`
-
- - -
`) + + // Form + sb.WriteString(`
`) + sb.WriteString(`
`) + sb.WriteString(fmt.Sprintf(``, htmlEsc(prevCmd))) + sb.WriteString(``) + sb.WriteString(`
`) + + // Output if prevOutput != "" { - sb.WriteString(fmt.Sprintf(`
> %s
-%s
`, prevCmd, prevOutput)) + sb.WriteString(fmt.Sprintf(`
%s
`, htmlEsc(prevOutput))) } - sb.WriteString(`

-
- -
-

Commands

- - - - - - - -
search <query>Search indexed content
delete <type> <id>Delete content by type and ID
user <id>View user details
wallet <id>View wallet balance
typesList deletable content types
statsIndex stats
-
- -`) + sb.WriteString(`
`) + + // Help + sb.WriteString(`
+

search <query> · delete <type> <id> · user <id> · wallet <id> · types · stats

+
`) html := app.RenderHTMLForRequest("Console", "Admin Console", sb.String(), r) w.Write([]byte(html)) } -func handleConsoleCommand(w http.ResponseWriter, r *http.Request) { - _, _, err := auth.RequireAdmin(r) - if err != nil { - w.WriteHeader(403) - return - } +func htmlEsc(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, `"`, """) + return s +} - var cmd string - if r.Header.Get("Content-Type") == "application/json" { - var req struct { - Cmd string `json:"cmd"` - } - json.NewDecoder(r.Body).Decode(&req) - cmd = req.Cmd - } else { - r.ParseForm() - cmd = r.FormValue("cmd") - } - cmd = strings.TrimSpace(cmd) +func runCommand(cmd string) string { parts := strings.Fields(cmd) if len(parts) == 0 { - app.RespondJSON(w, map[string]string{"output": ""}) - return + return "" } - var output string - switch parts[0] { case "search": if len(parts) < 2 { - output = "usage: search " - break + return "usage: search " } query := strings.Join(parts[1:], " ") results := data.Search(query, 20) if len(results) == 0 { - output = "No results." - } else { - var sb strings.Builder - for _, r := range results { - sb.WriteString(fmt.Sprintf("[%s] %s — %s\n", r.Type, r.ID, r.Title)) - } - output = sb.String() + return "No results." + } + var sb strings.Builder + for _, r := range results { + sb.WriteString(fmt.Sprintf("[%s] %s — %s\n", r.Type, r.ID, r.Title)) } + return sb.String() case "delete": if len(parts) < 3 { - output = "usage: delete " - break + return "usage: delete " } contentType := parts[1] id := strings.Join(parts[2:], " ") if err := data.Delete(contentType, id); err != nil { - output = "Error: " + err.Error() - } else { - output = fmt.Sprintf("Deleted %s %s", contentType, id) + return "Error: " + err.Error() } + return fmt.Sprintf("Deleted %s %s", contentType, id) case "user": if len(parts) < 2 { - output = "usage: user " - break + return "usage: user " } acc, err := auth.GetAccount(parts[1]) if err != nil { - output = "User not found" - } else { - output = fmt.Sprintf("ID: %s\nName: %s\nAdmin: %v\nCreated: %s", - acc.ID, acc.Name, acc.Admin, acc.Created.Format("2 Jan 2006 15:04")) + return "User not found" } + return fmt.Sprintf("ID: %s\nName: %s\nAdmin: %v\nCreated: %s", + acc.ID, acc.Name, acc.Admin, acc.Created.Format("2 Jan 2006 15:04")) case "wallet": if len(parts) < 2 { - output = "usage: wallet " - break + return "usage: wallet " } w := wallet.GetWallet(parts[1]) usage := wallet.GetDailyUsage(parts[1]) - output = fmt.Sprintf("Balance: %d credits\nDaily usage: %d / %d free", + return fmt.Sprintf("Balance: %d credits\nDaily usage: %d / %d free", w.Balance, usage.Used, wallet.FreeDailyQuota) case "types": types := data.DeleteTypes() if len(types) == 0 { - output = "No deletable types registered." - } else { - output = strings.Join(types, ", ") + return "No deletable types registered." } + return strings.Join(types, ", ") case "stats": stats := data.GetStats() - output = fmt.Sprintf("Index entries: %d\nSQLite: %v", stats.TotalEntries, stats.UsingSQLite) + return fmt.Sprintf("Index entries: %d\nSQLite: %v", stats.TotalEntries, stats.UsingSQLite) case "help": - output = "Commands: search, delete, user, wallet, types, stats, help" + return "search · delete · user · wallet · types · stats" default: - output = fmt.Sprintf("Unknown command: %s. Type 'help' for commands.", parts[0]) - } - - if r.Header.Get("Content-Type") == "application/json" { - app.RespondJSON(w, map[string]string{"output": output}) - } else { - // Regular form submit — redirect back with output - http.Redirect(w, r, "/admin/console?output="+strings.ReplaceAll(output, "\n", "%0A")+"&cmd="+cmd, http.StatusSeeOther) + return fmt.Sprintf("Unknown command: %s", parts[0]) } }