2603.7 added dark/light mode
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
"""portspoof_py — asyncio Python rewrite of the portspoof TCP honeypot."""
|
||||
__version__ = '2603.6'
|
||||
__version__ = '2603.7'
|
||||
|
||||
@@ -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="/">← dashboard</a></p>
|
||||
<p class="sub"><a href="/">← 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="/">← dashboard</a></p>
|
||||
<p class="sub"><a href="/">← 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="/">← dashboard</a></p>
|
||||
<p class="sub"><a href="/">← 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 = '· <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 = """\
|
||||
· <a href="/settings">settings</a> ·
|
||||
<a href="/config">email alerts</a> ·
|
||||
<a href="/api/stats" target="_blank">JSON stats</a> ·
|
||||
<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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user