Add archive mailbox selector

This commit is contained in:
DonVoo 2026-07-12 17:18:56 +02:00
parent 422308e7e6
commit cff776dc8c
3 changed files with 94 additions and 34 deletions

View file

@ -10,12 +10,12 @@ func RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/login", loginHandler) mux.HandleFunc("/login", loginHandler)
// Umzug (Konten pflegen + starten/beobachten) // Umzug (Konten pflegen + starten/beobachten)
mux.HandleFunc("/accounts", accountsHandler) // TODO Codex (02/07) mux.HandleFunc("/accounts", accountsHandler) // TODO Codex (02/07)
mux.HandleFunc("/accounts/save", accountSaveHandler) // TODO Codex mux.HandleFunc("/accounts/save", accountSaveHandler) // TODO Codex
mux.HandleFunc("/accounts/delete", accountDeleteHandler) // TODO Codex mux.HandleFunc("/accounts/delete", accountDeleteHandler) // TODO Codex
mux.HandleFunc("/accounts/test", accountTestHandler) // TODO Codex: Quelle+Ziel-Login pruefen mux.HandleFunc("/accounts/test", accountTestHandler) // TODO Codex: Quelle+Ziel-Login pruefen
mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund
mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll) mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll)
// Viewer (lokale mbox betrachten + weiterleiten) // Viewer (lokale mbox betrachten + weiterleiten)
mux.HandleFunc("/view", viewerHandler) mux.HandleFunc("/view", viewerHandler)
@ -36,20 +36,21 @@ func homeHandler(w http.ResponseWriter, r *http.Request) {
} }
// renderShell ist das Grundgeruest im Outlook-2013-Look: // renderShell ist das Grundgeruest im Outlook-2013-Look:
// [ Ribbon: Archiv-Boxen | Import/Export | Postfach-Verwaltung | Mail-Transfer | Einstellungen | Mail-Graveyard ] //
// [ Archiv-mbox | Nachrichtenliste | Quelle/Target ] // [ Ribbon: Archiv-Boxen | Import/Export | Postfach-Verwaltung | Mail-Transfer | Einstellungen | Mail-Graveyard ]
// [ Archiv-mbox | Nachrichtenliste | Quelle/Target ]
func renderShell(w http.ResponseWriter, active, readingPane string) { func renderShell(w http.ResponseWriter, active, readingPane string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8"> fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mail-Graveyard</title> <title>Mail-Graveyard</title>
<link rel="stylesheet" href="/static/style.css?v=20260712-1"> <link rel="stylesheet" href="/static/style.css?v=20260712-2">
<script src="/static/htmx.min.js"></script> <script src="/static/htmx.min.js"></script>
<script src="/static/app.js?v=20260712-1" defer></script></head> <script src="/static/app.js?v=20260712-2" defer></script></head>
<body class="ol2013"> <body class="ol2013">
<header class="ribbon"> <header class="ribbon">
<nav class="ribbon-tabs"> <nav class="ribbon-tabs">
<a class="tab %s" href="/view">Archiv-Boxen</a> <a class="tab %s" href="/view"><span class="mail-logo" aria-hidden="true"></span><span>Archiv-Boxen</span></a>
<a class="tab" href="/view">Import/Export</a> <a class="tab" href="/view">Import/Export</a>
<a class="tab" href="/accounts">Postfach-Verwaltung</a> <a class="tab" href="/accounts">Postfach-Verwaltung</a>
<a class="tab" href="/view">Mail-Transfer</a> <a class="tab" href="/view">Mail-Transfer</a>
@ -60,7 +61,7 @@ func renderShell(w http.ResponseWriter, active, readingPane string) {
<div class="three-pane"> <div class="three-pane">
<aside class="pane pane-tree" id="tree" <aside class="pane pane-tree" id="tree"
hx-get="/view" hx-trigger="load" hx-target="#tree" hx-swap="innerHTML"> hx-get="/view" hx-trigger="load" hx-target="#tree" hx-swap="innerHTML">
<div class="pane-head">Postf&auml;cher</div> <div class="pane-head">Archiv-Mailbox</div>
</aside> </aside>
<div class="pane-resizer" data-resize-index="0"></div> <div class="pane-resizer" data-resize-index="0"></div>
<section class="pane pane-list" id="list"> <section class="pane pane-list" id="list">

View file

@ -28,7 +28,7 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
if r.Header.Get("HX-Request") == "true" { if r.Header.Get("HX-Request") == "true" {
renderViewerTree(w) renderViewerTree(w, account)
return return
} }
renderShell(w, "", `<div class="ef-empty">Backup-Postfach links waehlen.</div>`) renderShell(w, "", `<div class="ef-empty">Backup-Postfach links waehlen.</div>`)
@ -106,34 +106,47 @@ func forwardHandler(w http.ResponseWriter, r *http.Request) {
// selbst nutzt IMAP APPEND. // selbst nutzt IMAP APPEND.
} }
func renderViewerTree(w http.ResponseWriter) { func renderViewerTree(w http.ResponseWriter, selected string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, `<div class="pane-head">Originale</div>`) accounts := viewerAccounts()
entries, err := os.ReadDir(Cfg.MboxRoot) if len(accounts) == 0 {
if err != nil { fmt.Fprint(w, `<div class="pane-head archive-head"><span>Archiv-Mailbox</span></div><div class="ef-empty">Noch keine mbox-Backups.</div>`)
fmt.Fprintf(w, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
return return
} }
found := false selected = resolveViewerAccount(selected, selected)
for _, entry := range entries { if selected == "" {
if !entry.IsDir() { selected = accounts[0].Name
continue } else {
found := false
for _, account := range accounts {
if account.Name == selected {
found = true
break
}
} }
account := entry.Name() if !found {
mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, account, "*.mbox")) selected = accounts[0].Name
if len(mboxes) == 0 {
continue
}
found = true
source := sourceDisplayLabel(account)
fmt.Fprintf(w, `<div class="tree-node account">%s</div>`, html.EscapeString(sourceDisplayLabel(account)))
for _, folder := range archiveFolderInfos(mboxes) {
fmt.Fprintf(w, `<a class="tree-node folder" href="#" hx-get="/view?source=%s&amp;folder=%s" hx-target="#list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%d</span></a>`,
urlEsc(source), urlEsc(folder.Name), html.EscapeString(folder.Label), folder.Count)
} }
} }
if !found { fmt.Fprint(w, `<div class="pane-head archive-head"><span>Archiv-Mailbox</span><form class="archive-select-form" hx-get="/view" hx-target="#tree" hx-swap="innerHTML" hx-trigger="change from:#archive-mailbox-select"><select id="archive-mailbox-select" class="archive-select" name="source">`)
fmt.Fprint(w, `<div class="ef-empty">Noch keine mbox-Backups.</div>`) for _, account := range accounts {
attr := ""
if account.Name == selected {
attr = ` selected`
}
fmt.Fprintf(w, `<option value="%s"%s>%s</option>`, html.EscapeString(account.DisplayLabel), attr, html.EscapeString(account.SourceLabel))
}
fmt.Fprint(w, `</select></form></div>`)
mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, selected, "*.mbox"))
if len(mboxes) == 0 {
fmt.Fprint(w, `<div class="ef-empty">Keine Ordner in dieser Archiv-Mailbox.</div>`)
return
}
source := sourceDisplayLabel(selected)
for _, folder := range archiveFolderInfos(mboxes) {
fmt.Fprintf(w, `<a class="tree-node folder" href="#" hx-get="/view?source=%s&amp;folder=%s" hx-target="#list" hx-swap="innerHTML"><span class="tree-label">%s</span><span class="tree-count">%d</span></a>`,
urlEsc(source), urlEsc(folder.Name), html.EscapeString(folder.Label), folder.Count)
} }
} }

View file

@ -41,12 +41,41 @@ body.ol2013 {
.ribbon-tabs { display: flex; gap: 2px; height: 100%; } .ribbon-tabs { display: flex; gap: 2px; height: 100%; }
.tab { .tab {
display: flex; align-items: center; display: flex; align-items: center;
gap: 8px;
padding: 0 16px; height: 100%; padding: 0 16px; height: 100%;
color: #eaf3fb; text-decoration: none; color: #eaf3fb; text-decoration: none;
border-bottom: 3px solid transparent; border-bottom: 3px solid transparent;
} }
.tab:hover { background: var(--ol-blue-dark); color: #fff; } .tab:hover { background: var(--ol-blue-dark); color: #fff; }
.tab.active { background: #fff; color: var(--ol-blue); border-bottom-color: #fff; } .tab.active { background: #fff; color: var(--ol-blue); border-bottom-color: #fff; }
.mail-logo {
position: relative;
width: 17px;
height: 12px;
border: 2px solid currentColor;
border-radius: 2px;
display: inline-block;
flex: 0 0 auto;
}
.mail-logo::before,
.mail-logo::after {
content: "";
position: absolute;
top: 1px;
width: 10px;
height: 2px;
background: currentColor;
}
.mail-logo::before {
left: -1px;
transform: rotate(32deg);
transform-origin: left center;
}
.mail-logo::after {
right: -1px;
transform: rotate(-32deg);
transform-origin: right center;
}
/* Viewer */ /* Viewer */
.three-pane, .three-pane,
@ -107,6 +136,23 @@ body.resizing { cursor: col-resize; user-select: none; }
color: var(--ol-text); color: var(--ol-text);
background: #fff; background: #fff;
} }
.archive-head {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 8px;
}
.archive-select-form {
margin: 0;
min-width: 0;
}
.archive-select {
width: 100%;
height: 24px;
padding: 2px 6px;
color: var(--ol-text);
background: #fff;
}
.searchbar { .searchbar {
position: sticky; top: 30px; z-index: 2; position: sticky; top: 30px; z-index: 2;