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(`
`)
+
+ // 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 |
-types | List deletable content types |
-stats | Index 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])
}
}