From 6675e73b0f49cdaa15e15ff446ba78d9c50d4e13 Mon Sep 17 00:00:00 2001 From: DonVoo Date: Sun, 12 Jul 2026 23:29:09 +0200 Subject: [PATCH] Add mail transfer submenu modes --- backend/00-router.go | 176 ++++++++++++++++++++++++++++++++++--- frontend-js/dist/style.css | 23 ++++- 2 files changed, 185 insertions(+), 14 deletions(-) diff --git a/backend/00-router.go b/backend/00-router.go index f6be991..15315e6 100644 --- a/backend/00-router.go +++ b/backend/00-router.go @@ -25,6 +25,8 @@ func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/archives", archivesHandler) mux.HandleFunc("/archives/create", archiveCreateHandler) mux.HandleFunc("/archives/delete", archiveDeleteHandler) + mux.HandleFunc("/transfer", transferHandler) + mux.HandleFunc("/transfer/", transferHandler) mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll) @@ -55,15 +57,15 @@ func renderShell(w http.ResponseWriter, active, readingPane string) { fmt.Fprintf(w, ` Mail-Graveyard - + - +
Mail-Graveyard
@@ -73,7 +75,10 @@ func renderShell(w http.ResponseWriter, active, readingPane string) { Import/Export Konten-Verwaltung Archiv-mbox Verwaltung - Mail-Transfer + Mail-Transfer + Postfach <--> Archiv-MBox + Archiv-MBox <--> Postfach + Postfach <--> Postfach Einstellungen @@ -186,6 +191,20 @@ func archivesHandler(w http.ResponseWriter, r *http.Request) { renderArchivesPage(w, archiveMailboxes(accounts), accounts, r.URL.Query().Get("msg"), r.URL.Query().Get("err")) } +func transferHandler(w http.ResponseWriter, r *http.Request) { + mode := strings.TrimPrefix(r.URL.Path, "/transfer/") + if mode == "" || mode == "/transfer" { + http.Redirect(w, r, "/transfer/postfach-archiv", http.StatusSeeOther) + return + } + accounts, err := ListAccounts() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + renderTransferPage(w, transferModeBySlug(mode), accounts, archiveMailboxes(accounts)) +} + func archiveCreateHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -293,15 +312,15 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account, fmt.Fprintf(w, ` Konten-Verwaltung - Mail-Graveyard - + - +
Mail-Graveyard
@@ -311,7 +330,10 @@ func renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account, Import/Export Konten-Verwaltung Archiv-mbox Verwaltung - Mail-Transfer + Mail-Transfer + Postfach <--> Archiv-MBox + Archiv-MBox <--> Postfach + Postfach <--> Postfach Einstellungen @@ -334,15 +356,15 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc fmt.Fprintf(w, ` Archiv-mbox Verwaltung - Mail-Graveyard - + - +
Mail-Graveyard
@@ -352,7 +374,10 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc Import/Export Konten-Verwaltung Archiv-mbox Verwaltung - Mail-Transfer + Mail-Transfer + Postfach <--> Archiv-MBox + Archiv-MBox <--> Postfach + Postfach <--> Postfach Einstellungen @@ -370,6 +395,133 @@ func renderArchivesPage(w http.ResponseWriter, archives []string, accounts []Acc fmt.Fprint(w, `
bereit
`) } +type transferMode struct { + Slug string + Title string + LeftTitle string + RightTitle string + LeftKind string + RightKind string +} + +func transferModeBySlug(slug string) transferMode { + modes := []transferMode{ + {Slug: "postfach-archiv", Title: "Postfach <--> Archiv-MBox", LeftTitle: "Postfach", RightTitle: "Archiv-MBox", LeftKind: "source", RightKind: "archive"}, + {Slug: "archiv-postfach", Title: "Archiv-MBox <--> Postfach", LeftTitle: "Archiv-MBox", RightTitle: "Postfach", LeftKind: "archive", RightKind: "target"}, + {Slug: "postfach-postfach", Title: "Postfach <--> Postfach", LeftTitle: "Quell-Postfach", RightTitle: "Ziel-Postfach", LeftKind: "source", RightKind: "target"}, + } + for _, mode := range modes { + if mode.Slug == slug { + return mode + } + } + return modes[0] +} + +func renderTransferPage(w http.ResponseWriter, mode transferMode, accounts []Account, archives []string) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, ` + +%s - Mail-Graveyard + + + + +
+ + +
Mail-Graveyard
+
+ + +
+
+
%s
%s
+
%s
%s
+
+
+
bereit
`, + html.EscapeString(mode.Title), transferMenuItems(mode.Slug), + html.EscapeString(mode.LeftTitle), renderTransferPicker(mode.LeftKind, accounts, archives), + html.EscapeString(mode.RightTitle), renderTransferPicker(mode.RightKind, accounts, archives)) +} + +func transferMenuItems(active string) string { + items := []transferMode{ + {Slug: "postfach-archiv", Title: "Postfach <--> Archiv-MBox"}, + {Slug: "archiv-postfach", Title: "Archiv-MBox <--> Postfach"}, + {Slug: "postfach-postfach", Title: "Postfach <--> Postfach"}, + } + var b strings.Builder + for _, item := range items { + class := "backstage-item transfer-sub" + if item.Slug == active { + class += " active" + } + fmt.Fprintf(&b, `%s`, class, item.Slug, html.EscapeString(item.Title)) + } + return b.String() +} + +func renderTransferPicker(kind string, accounts []Account, archives []string) string { + var b strings.Builder + b.WriteString(`
`) + switch kind { + case "archive": + fmt.Fprintf(&b, ``, transferSelect("archive", archives)) + case "target": + fmt.Fprintf(&b, ``, transferSelect("target", uniqueAccountValues(accounts, "target"))) + default: + fmt.Fprintf(&b, ``, transferSelect("source", uniqueAccountValues(accounts, "source"))) + } + b.WriteString(`
`) + return b.String() +} + +func transferSelect(name string, values []string) string { + if len(values) == 0 { + return `` + } + var b strings.Builder + fmt.Fprintf(&b, ``) + return b.String() +} + +func uniqueAccountValues(accounts []Account, field string) []string { + seen := map[string]bool{} + var out []string + for _, a := range accounts { + value := a.SrcUser + if field == "target" { + value = a.DstUser + } + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + sortStrings(out) + return out +} + func renderAccountsTable(w http.ResponseWriter, accounts []Account, editName string) { if len(accounts) == 0 { fmt.Fprint(w, `
Noch keine Postfaecher eingetragen.
`) diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css index d824577..18aa4d5 100644 --- a/frontend-js/dist/style.css +++ b/frontend-js/dist/style.css @@ -120,6 +120,10 @@ body.ol2013 { color: var(--ol-blue); border-left-color: #fff; } +.backstage-item.transfer-sub { + padding-left: 38px; + font-size: 12px; +} .backstage-shade { position: fixed; inset: 44px 0 22px 0; @@ -351,18 +355,28 @@ a.tree-node { display: block; color: inherit; text-decoration: none; } .archive-page { background: #fff; } +.transfer-page { + background: #fff; +} .archive-page-layout { display: grid; grid-template-columns: minmax(520px, 1fr) minmax(360px, 460px); min-height: 100%; } +.transfer-layout { + display: grid; + grid-template-columns: minmax(380px, 1fr) minmax(380px, 1fr); + min-height: 100%; +} .archive-list-panel, -.archive-form-panel { +.archive-form-panel, +.transfer-panel { min-width: 0; border-right: 1px solid var(--ol-border); background: #fff; } -.archive-form-panel { +.archive-form-panel, +.transfer-panel:last-child { border-right: 0; } .account-table { @@ -491,6 +505,10 @@ a.tree-node { display: block; color: inherit; text-decoration: none; } color: var(--ol-blue-dark); font-weight: 700; } +.transfer-picker { + padding: 14px; + border-bottom: 1px solid var(--ol-border); +} .muted { color: var(--ol-muted); } /* Statusleiste */ @@ -527,6 +545,7 @@ a.tree-node { display: block; color: inherit; text-decoration: none; } .searchbar { top: 30px; } .account-layout, .archive-page-layout, + .transfer-layout, .form-columns, .form-section { grid-template-columns: 1fr;