2603.7 added dark/light mode

This commit is contained in:
2026-03-13 16:44:12 -04:00
parent 1c35d604e3
commit f90613da16
3 changed files with 100 additions and 44 deletions

View File

@@ -1,2 +1,2 @@
"""portspoof_py — asyncio Python rewrite of the portspoof TCP honeypot."""
__version__ = '2603.6'
__version__ = '2603.7'

View File

@@ -82,10 +82,11 @@ _PASSWD_HTML = """\
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>portspoof — change password</title>
<style>{css}</style>
{theme_script}
</head>
<body>
<h1>portspoof admin</h1>
<p class="sub"><a href="/">&larr; dashboard</a></p>
<p class="sub"><a href="/">&larr; dashboard</a> {theme_btn}</p>
{banner_html}
@@ -126,7 +127,8 @@ def _render_passwd(msg: str = '', msg_ok: bool = True) -> str:
if msg:
cls = 'ok' if msg_ok else 'err'
banner_html = f'<div class="banner {cls}">{html.escape(msg)}</div>'
return _PASSWD_HTML.format(css=_CONFIG_CSS, banner_html=banner_html)
return _PASSWD_HTML.format(css=_CONFIG_CSS, banner_html=banner_html,
theme_script=_THEME_SCRIPT, theme_btn=_THEME_BTN)
# ── settings page ─────────────────────────────────────────────────────────────
@@ -139,10 +141,11 @@ _SETTINGS_HTML = """\
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>portspoof — settings</title>
<style>{css}</style>
{theme_script}
</head>
<body>
<h1>portspoof admin</h1>
<p class="sub"><a href="/">&larr; dashboard</a></p>
<p class="sub"><a href="/">&larr; dashboard</a> {theme_btn}</p>
{banner_html}
@@ -182,45 +185,66 @@ def _render_settings(settings: Settings, msg: str = '', msg_ok: bool = True) ->
banner_html=banner_html,
delay_min=settings.delay_min,
delay_max=settings.delay_max,
theme_script=_THEME_SCRIPT,
theme_btn=_THEME_BTN,
)
# ── config page ───────────────────────────────────────────────────────────────
_CONFIG_CSS = """
:root {
--bg: #0d1117; --bg2: #161b22; --border: #30363d;
--text: #c9d1d9; --dim: #8b949e; --heading: #e6edf3; --blue: #58a6ff;
--green: #238636; --green2: #2ea043;
--btn-sec: #21262d; --btn-sec2: #30363d;
--ok-bg: #0f2a1a; --ok-border: #238636; --ok-text: #3fb950;
--err-bg: #2d1212; --err-border: #f85149; --err-text: #f85149;
}
html.light {
--bg: #f6f8fa; --bg2: #ffffff; --border: #d0d7de;
--text: #24292f; --dim: #57606a; --heading: #1f2328; --blue: #0969da;
--green: #1f883d; --green2: #2ea043;
--btn-sec: #f6f8fa; --btn-sec2: #eaeef2;
--ok-bg: #dafbe1; --ok-border: #1f883d; --ok-text: #1a7f37;
--err-bg: #fff0ee; --err-border: #ff8182; --err-text: #cf222e;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Courier New', monospace; background: #0d1117; color: #c9d1d9;
body { font-family: 'Courier New', monospace; background: var(--bg); color: var(--text);
padding: 24px; font-size: 13px; line-height: 1.6; }
a { color: #58a6ff; text-decoration: none; }
h1 { color: #58a6ff; font-size: 20px; margin-bottom: 4px; }
h2 { color: #e6edf3; font-size: 14px; margin: 24px 0 12px; }
.sub { color: #8b949e; font-size: 12px; margin-bottom: 24px; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 6px;
a { color: var(--blue); text-decoration: none; }
h1 { color: var(--blue); font-size: 20px; margin-bottom: 4px; }
h2 { color: var(--heading); font-size: 14px; margin: 24px 0 12px; }
.sub { color: var(--dim); font-size: 12px; margin-bottom: 24px; }
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px;
padding: 20px; max-width: 600px; }
.field { margin-bottom: 14px; }
label { display: block; color: #8b949e; font-size: 11px; text-transform: uppercase;
label { display: block; color: var(--dim); font-size: 11px; text-transform: uppercase;
letter-spacing: 1px; margin-bottom: 4px; }
input[type=text], input[type=number], input[type=password], input[type=email] {
width: 100%; background: #0d1117; border: 1px solid #30363d; color: #c9d1d9;
width: 100%; background: var(--bg); border: 1px solid var(--border); color: var(--text);
padding: 7px 10px; border-radius: 4px; font-family: inherit; font-size: 13px; }
input[type=text]:focus, input[type=number]:focus,
input[type=password]:focus, input[type=email]:focus {
outline: none; border-color: #58a6ff; }
.hint { color: #8b949e; font-size: 11px; margin-top: 3px; }
outline: none; border-color: var(--blue); }
.hint { color: var(--dim); font-size: 11px; margin-top: 3px; }
.row2 { display: flex; gap: 12px; }
.row2 .field { flex: 1; }
.check-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
input[type=checkbox] { width: 15px; height: 15px; accent-color: #238636; }
input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--green); }
.actions { display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap; }
button { background: #238636; border: 1px solid #2ea043; color: #fff;
button { background: var(--green); border: 1px solid var(--green2); color: #fff;
padding: 7px 16px; border-radius: 4px; cursor: pointer;
font-family: inherit; font-size: 13px; }
button:hover { background: #2ea043; }
button.secondary { background: #21262d; border-color: #30363d; color: #c9d1d9; }
button.secondary:hover { background: #30363d; }
button:hover { background: var(--green2); }
button.secondary { background: var(--btn-sec); border-color: var(--border); color: var(--text); }
button.secondary:hover { background: var(--btn-sec2); }
.banner { padding: 10px 14px; border-radius: 4px; margin-bottom: 16px; font-size: 12px; }
.banner.ok { background: #0f2a1a; border: 1px solid #238636; color: #3fb950; }
.banner.err { background: #2d1212; border: 1px solid #f85149; color: #f85149; }
.banner.ok { background: var(--ok-bg); border: 1px solid var(--ok-border); color: var(--ok-text); }
.banner.err { background: var(--err-bg); border: 1px solid var(--err-border); color: var(--err-text); }
.link-btn { background: none; border: none; color: var(--blue); padding: 0;
font-family: inherit; font-size: 13px; cursor: pointer; }
.link-btn:hover { text-decoration: underline; }
"""
_CONFIG_HTML = """\
@@ -231,10 +255,11 @@ _CONFIG_HTML = """\
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>portspoof — email alerts</title>
<style>{css}</style>
{theme_script}
</head>
<body>
<h1>portspoof admin</h1>
<p class="sub"><a href="/">&larr; dashboard</a></p>
<p class="sub"><a href="/">&larr; dashboard</a> {theme_btn}</p>
{banner_html}
@@ -338,6 +363,8 @@ def _render_config(notifier: Notifier, msg: str = '', msg_ok: bool = True) -> st
return _CONFIG_HTML.format(
css=_CONFIG_CSS,
banner_html=banner_html,
theme_script=_THEME_SCRIPT,
theme_btn=_THEME_BTN,
enabled_chk='checked' if cfg.get('enabled') else '',
smtp_host=html.escape(cfg.get('smtp_host', '')),
smtp_port=cfg.get('smtp_port', 587),
@@ -377,58 +404,84 @@ def _redirect(location: str) -> bytes:
)
# ── shared theme script ───────────────────────────────────────────────────────
_THEME_SCRIPT = """\
<script>
(function(){{if(localStorage.getItem('theme')==='light')document.documentElement.classList.add('light')}})();
function toggleTheme(){{var e=document.documentElement;e.classList.toggle('light');localStorage.setItem('theme',e.classList.contains('light')?'light':'dark');}}
</script>"""
_THEME_BTN = '&middot; <button type="button" class="link-btn" onclick="toggleTheme()">light/dark</button>'
# ── HTML template ─────────────────────────────────────────────────────────────
_CSS = """
:root {
--bg: #0d1117; --bg2: #161b22; --border: #30363d;
--text: #c9d1d9; --dim: #8b949e; --heading: #e6edf3; --blue: #58a6ff;
--red: #f78166; --ip-color: #79c0ff;
--hover: #1c2128; --badge: #21262d; --sep: #21262d; --footer: #484f58;
}
html.light {
--bg: #f6f8fa; --bg2: #ffffff; --border: #d0d7de;
--text: #24292f; --dim: #57606a; --heading: #1f2328; --blue: #0969da;
--red: #cf222e; --ip-color: #0550ae;
--hover: #f3f4f6; --badge: #eaeef2; --sep: #d8dee4; --footer: #8c959f;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Courier New', monospace;
background: #0d1117; color: #c9d1d9;
background: var(--bg); color: var(--text);
padding: 24px; font-size: 13px; line-height: 1.5;
}
a { color: #58a6ff; text-decoration: none; }
h1 { color: #58a6ff; font-size: 20px; margin-bottom: 4px; }
.sub { color: #8b949e; font-size: 12px; margin-bottom: 24px; }
a { color: var(--blue); text-decoration: none; }
h1 { color: var(--blue); font-size: 20px; margin-bottom: 4px; }
.sub { color: var(--dim); font-size: 12px; margin-bottom: 24px; }
.row { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 20px; }
.card {
background: #161b22; border: 1px solid #30363d;
background: var(--bg2); border: 1px solid var(--border);
border-radius: 6px; padding: 16px;
}
.card.stat { min-width: 155px; flex: 1; }
.card.half { flex: 1; min-width: 260px; }
.card.full { width: 100%; }
.card h3 {
color: #8b949e; font-size: 11px;
color: var(--dim); font-size: 11px;
text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px;
}
.card .val { font-size: 28px; color: #58a6ff; font-weight: bold; }
.card .val { font-size: 28px; color: var(--blue); font-weight: bold; }
.card .val.last { font-size: 13px; margin-top: 4px; }
h2 { color: #e6edf3; font-size: 14px; margin-bottom: 12px; }
h2 { color: var(--heading); font-size: 14px; margin-bottom: 12px; }
table { width: 100%; border-collapse: collapse; }
th {
color: #8b949e; font-size: 11px; text-transform: uppercase;
color: var(--dim); font-size: 11px; text-transform: uppercase;
letter-spacing: 0.5px; padding: 6px 10px; text-align: left;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid var(--sep);
}
td { padding: 5px 10px; border-bottom: 1px solid #21262d; }
td { padding: 5px 10px; border-bottom: 1px solid var(--sep); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: #1c2128; }
.port { color: #f78166; }
.ip { color: #79c0ff; }
.ts { color: #8b949e; font-size: 11px; }
.cc { color: #8b949e; font-size: 11px; }
tr:hover td { background: var(--hover); }
.port { color: var(--red); }
.ip { color: var(--ip-color); }
.ts { color: var(--dim); font-size: 11px; }
.cc { color: var(--dim); font-size: 11px; }
.badge {
display: inline-block; background: #21262d;
display: inline-block; background: var(--badge);
border-radius: 3px; padding: 1px 7px; font-size: 11px;
margin-left: 6px; vertical-align: middle;
}
.empty { color: #8b949e; font-style: italic; padding: 8px 0; }
.footer { color: #484f58; font-size: 11px; margin-top: 24px; }
.empty { color: var(--dim); font-style: italic; padding: 8px 0; }
.footer { color: var(--footer); font-size: 11px; margin-top: 24px; }
.link-btn {
background: none; border: none; color: #58a6ff; padding: 0;
background: none; border: none; color: var(--blue); padding: 0;
font-family: inherit; font-size: inherit; cursor: pointer;
}
.link-btn:hover { text-decoration: underline; }
html.light svg line { stroke: var(--sep); }
html.light svg text { fill: var(--dim); }
html.light svg polygon { fill: rgba(9,105,218,0.12); opacity: 1; }
html.light svg polyline { stroke: var(--blue); }
"""
_HTML = """\
@@ -440,6 +493,7 @@ _HTML = """\
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>portspoof admin</title>
<style>{css}</style>
{theme_script}
</head>
<body>
<h1>portspoof admin</h1>
@@ -451,7 +505,7 @@ _HTML = """\
&middot; <a href="/settings">settings</a> &middot;
<a href="/config">email alerts</a> &middot;
<a href="/api/stats" target="_blank">JSON stats</a> &middot;
<a href="/passwd">change password</a>
<a href="/passwd">change password</a> {theme_btn}
</p>
<!-- stat cards -->
@@ -627,6 +681,8 @@ async def _render_dashboard(stats: Stats, cfg: Config) -> str:
return _HTML.format(
css=_CSS,
theme_script=_THEME_SCRIPT,
theme_btn=_THEME_BTN,
total=stats.total,
cpm=stats.connections_per_minute(),
uptime=stats.uptime_str(),

View File

@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
[project]
name = "portspoof-py"
version = "2603.6"
version = "2603.7"
description = "Python asyncio rewrite of the portspoof TCP honeypot"
readme = "README.md"
requires-python = ">=3.11"