Files
mixandClaude Opus 5.5 c5edbd84c7 design: panel-words - the mockups and the panel say the same
The owner's "проверь, что документация актуальна с кодом" (2026-10-16):
the words the mockups put on screen against what the panel renders and
does, page by page (docs-audit, 2026-10-17). Where the code is right about
behaviour the mockup follows it; where the mockup is right the code
follows in the next commit.

The mockups now say: "Refreshes automatically" / "refreshes on its own"
instead of "every 10 s" (panel.js polls at 5 s, 30 s, up to 60 s) on
Health, Overview, System log and both queues; System log points at the
Outbound log, the one with a page per message; the Spam filter facts are
what the panel knows without the quarantine (Applies to, Filter, If the
filter is down), and Valid recipients takes one per line or comma; a DMARC
report's title has no year, as its own title line; the "same form, empty"
sentences go (a new application and a new user have their own heads); an
application's login is fixed; the auto limit's multiplier is 1.5-5,
default 2.5 (store/ratelimits.go); the user form's e-mail is for
notifications (no reset link yet) and All makes the single domains
ignored (nothing greys them); a user's deletion says "(outbound)"; a
message's history is the panel's own steps, with the queue's wait after
a deferral; the footer is the panel's, "Copyright © 2026 Mikhail
Yenuchenko".

Help, true to the code: an application's password is kept by the server
(sasldb2) so an export can carry it; a refusal for From:/Sender: is one
row of the Outbound log; every password check of a backup counts, right
or wrong, ten in 15 minutes; a password set on the user form signs that
user out everywhere, but your own keeps your session; the limit mail is
daily per sending domain (notify.go, "limit:"+domain).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 02:10:29 +03:00

148 lines
7.4 KiB
Python

"""Wrap every fragment in src/ with the panel shell and write it next to this file.
The mockups are static HTML, but thirty pages share one navbar; keeping that
navbar in one place is the only reason this script exists. Run it from
anywhere: python docs/assets/panel-redesign/panel/build.py
A fragment starts with one meta comment:
<!-- title: Outbound domains | section: outbound | page: domains -->
section/page pick the highlighted entries; "shell: bare" skips the navbar
(sign-in, setup).
"""
import pathlib
import re
HERE = pathlib.Path(__file__).parent
NAV = [
("overview", "Overview", "overview.html", "ti-activity-heartbeat", []),
("outbound", "Outbound", "out-domains.html", "ti-send", [
("domains", "Domains", "out-domains.html", "ti-world-upload"),
("log", "Log", "out-log.html", "ti-list-details"),
("queue", "Queue", "out-queue.html", "ti-stack-2"),
("dmarc", "DMARC reports", "dmarc.html", "ti-report-analytics"),
]),
("inbound", "Inbound", "in-domains.html", "ti-inbox", [
("domains", "Domains", "in-domains.html", "ti-world-download"),
("log", "Log", "in-log.html", "ti-list-details"),
("queue", "Queue", "in-queue.html", "ti-stack-2"),
("quarantine", "Quarantine", "in-quarantine.html", "ti-mail-pause"),
("filter-lists", "Filter lists", "in-filter-lists.html", "ti-filter"),
]),
("server", "Server", "health.html", "ti-server-2", [
("health", "Health", "health.html", "ti-heartbeat"),
("preflight", "Preflight", "preflight.html", "ti-checklist"),
("system-log", "System log", "system-log.html", "ti-file-text"),
("backup", "Backup", "backup.html", "ti-archive"),
("users", "Users", "users.html", "ti-users"),
("settings", "Settings", "settings.html", "ti-settings"),
]),
]
HEAD = """<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} · SelfPost</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
<link rel="stylesheet" href="../shared/brand.css">
<link rel="stylesheet" href="theme.css">
</head>
"""
FOOT = ('<footer class="sp-foot">SelfPost 1.8.0 · Copyright © 2026 Mikhail Yenuchenko · <a href="#">License (AGPL-3.0)</a>'
' · <a href="#">Source</a> · No warranty</footer>')
def touchnav(section, page):
"""The navigation below 1024 px: a Menu that opens over the page, its
sections folded but the current one - details elements, no script."""
out = ['<details class="sp-touchnav"><summary>Menu</summary><div class="sp-touchnav-panel">']
for key, label, href, _icon, pages in NAV:
if not pages:
cur = ' aria-current="page"' if key == section else ""
out.append(f'<a href="{href}"{cur}>{label}</a>')
continue
opened = " open" if key == section else ""
out.append(f'<details class="sp-touchnav-group"{opened}><summary>{label}</summary>')
for pkey, plabel, phref, picon in pages:
cur = ' aria-current="page"' if key == section and pkey == page else ""
out.append(f'<a href="{phref}"{cur}><i class="ti {picon}"></i>{plabel}</a>')
out.append('</details>')
opened = " open" if section == "user" else ""
out += [f'<details class="sp-touchnav-group"{opened}><summary><i class="ti ti-user-circle"></i>admin</summary>',
'<a href="account.html"><i class="ti ti-key"></i>Account</a>',
'<a href="help.html"><i class="ti ti-help"></i>Help</a>',
'<form method="post" action="login.html"><button type="submit"><i class="ti ti-logout"></i>Sign out</button></form>',
'</details>', '</div></details>']
return "".join(out)
def navbar(section, page):
out = ['<nav class="navbar is-primary" aria-label="main navigation">', ' <div class="container">',
' <div class="navbar-brand"><a class="navbar-item" href="overview.html">'
'<img src="../../selfpost-wordmark.svg" alt="SelfPost"></a>' + touchnav(section, page) + '</div>',
' <div class="navbar-menu">', ' <div class="navbar-start">']
for key, label, href, _icon, pages in NAV:
cur = " sp-current" if key == section else ""
if not pages:
out.append(f' <a class="navbar-item{cur}" href="{href}">{label}</a>')
continue
out.append(f' <div class="navbar-item has-dropdown is-hoverable{cur}">')
out.append(f' <a class="navbar-link" href="{href}">{label}</a>')
out.append(' <div class="navbar-dropdown">')
for pkey, plabel, phref, picon in pages:
act = " is-active" if key == section and pkey == page else ""
out.append(f' <a class="navbar-item{act}" href="{phref}"><i class="ti {picon}"></i>{plabel}</a>')
out.append(' </div>')
out.append(' </div>')
out += [' </div>', ' <div class="navbar-end">',
' <div class="navbar-item has-dropdown is-hoverable%s">' % (" sp-current" if section == "user" else ""),
' <a class="navbar-link" href="account.html"><i class="ti ti-user-circle"></i>admin</a>',
' <div class="navbar-dropdown is-right">',
' <a class="navbar-item" href="account.html"><i class="ti ti-key"></i>Account</a>',
' <a class="navbar-item" href="help.html"><i class="ti ti-help"></i>Help</a>',
' <hr class="navbar-divider">',
# Signing out changes state, so it is a POST: a form with a button, not a link.
' <form method="post" action="login.html"><button type="submit" class="navbar-item">'
'<i class="ti ti-logout"></i>Sign out</button></form>',
' </div>', ' </div>', ' </div>', ' </div>', ' </div>', '</nav>',
'<div class="sp-perf" aria-hidden="true"></div>']
return "\n".join(out)
def subnav(section, page):
for key, _label, _href, _icon, pages in NAV:
if key == section and pages:
cur = ' aria-current="page"'
links = "".join(
f'<a href="{phref}"{cur if pkey == page else ""}><i class="ti {picon}"></i>{plabel}</a>'
for pkey, plabel, phref, picon in pages)
return f'<div class="sp-subnav"><div class="container">{links}</div></div>'
return ""
def build(path):
text = path.read_text(encoding="utf-8")
m = re.match(r"\s*<!--(.*?)-->\s*", text, re.S)
meta = dict(part.split(":", 1) for part in m.group(1).split("|"))
meta = {k.strip(): v.strip() for k, v in meta.items()}
body = text[m.end():]
html = HEAD.format(title=meta["title"])
if meta.get("shell") == "bare":
html += f"<body>\n{body}\n</body>\n</html>\n"
else:
section, page = meta.get("section", ""), meta.get("page", "")
html += (f"<body>\n{navbar(section, page)}\n{subnav(section, page)}\n<main>\n"
f' <div class="container sp-page">\n{body}\n {FOOT}\n </div>\n</main>\n</body>\n</html>\n')
(HERE / path.name).write_text(html, encoding="utf-8", newline="\n")
if __name__ == "__main__":
pages = sorted((HERE / "src").glob("*.html"))
for p in pages:
build(p)
print(f"built {len(pages)} pages")