adjusted session stats, split up css

Signed-off-by: wiebesteven <wiebesteven@freed.nl>
This commit is contained in:
wsvdmeer
2026-02-20 21:59:16 +01:00
parent 82a6f4e237
commit 462d91a19b
12 changed files with 664 additions and 465 deletions

View File

@@ -248,7 +248,7 @@ DNS_PTTRN = r"^\s*((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*[ ,;]\s*)+((\d{1,3}\.\
class BTTether(plugins.Plugin):
__author__ = "Jayofelony, modified my fmatray"
__author__ = "Jayofelony, modified by fmatray and wsvdmeer"
__version__ = "1.4"
__license__ = "GPL3"
__description__ = "A new BT-Tether plugin"

View File

@@ -127,6 +127,8 @@ TEMPLATE = """
padding: 1rem;
box-shadow: var(--shadow-md);
transition: all 0.3s ease;
overflow-x: auto;
overflow-y: hidden;
}
div.chart:hover {
@@ -134,6 +136,20 @@ TEMPLATE = """
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.1);
}
div.chart canvas {
max-height: 250px;
display: block;
min-width: 100%;
}
.chart-hint {
font-size: 0.75rem;
color: var(--text-muted);
text-align: center;
margin-top: 0.5rem;
font-family: var(--font-main);
}
/* Responsive Design */
@media (max-width: 768px) {
.stats-container {
@@ -249,19 +265,31 @@ TEMPLATE = """
borderWidth: 2,
fill: true,
tension: 0.1,
pointRadius: 3,
pointHoverRadius: 6
pointRadius: 1,
pointHoverRadius: 4
};
});
const canvas = container.querySelector('canvas') || document.createElement('canvas');
if (!container.querySelector('canvas')) container.appendChild(canvas);
let canvas = container.querySelector('canvas');
if (!canvas) {
canvas = document.createElement('canvas');
container.appendChild(canvas);
}
// Calculate required width based on number of data points
const dataPointCount = labels.length;
const minPixelsPerPoint = 50; // minimum pixels for each data point
const calculatedWidth = Math.max(container.clientWidth, dataPointCount * minPixelsPerPoint);
// Set canvas dimensions explicitly
canvas.width = calculatedWidth;
canvas.height = 250;
charts[elementId] = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
responsive: true,
responsive: false,
maintainAspectRatio: false,
plugins: {
title: {
@@ -297,7 +325,8 @@ TEMPLATE = """
},
ticks: {
color: '#fff',
font: { family: 'var(--font-main)', size: 11, weight: 'bold' }
font: { family: 'var(--font-main)', size: 11, weight: 'bold' },
maxTicksLimit: 8
}
},
y: {
@@ -313,6 +342,14 @@ TEMPLATE = """
}
}
});
// Add hint text if it doesn't exist
if (!container.querySelector('.chart-hint')) {
const hint = document.createElement('div');
hint.className = 'chart-hint';
hint.textContent = 'Scroll left/right to view more data';
container.appendChild(hint);
}
}
function getChartColor(index) {
@@ -430,14 +467,16 @@ TEMPLATE = """
class SessionStats(plugins.Plugin):
__author__ = "33197631+dadav@users.noreply.github.com"
__author__ = "33197631+dadav@users.noreply.github.com modified by wsvdmeer"
__version__ = "0.2.0"
__license__ = "GPL3"
__description__ = (
"Displays WiFi capture stats including networks, handshakes, and deauths."
)
DEFAULT_UPDATE_INTERVAL = 15 # RPi-friendly: 15 sec = 4 disk writes/min
DEFAULT_SAVE_PATH = "/home/pi/pwnagotchi/sessions/" # Standard location for user data
DEFAULT_SAVE_PATH = (
"/home/pi/pwnagotchi/sessions/" # Standard location for user data
)
def __init__(self):
self.lock = threading.Lock()
@@ -459,10 +498,8 @@ class SessionStats(plugins.Plugin):
os.path.join(save_dir, self.session_name),
data_format="json",
)
logging.info(
f"Session-stats plugin loaded. Saving to: {save_dir}"
)
logging.info(f"Session-stats plugin loaded. Saving to: {save_dir}")
# Try to load historical data from the most recent previous session
try:
@@ -477,8 +514,7 @@ class SessionStats(plugins.Plugin):
last_session_file = session_files[
-2
] # Second to last is the previous session
last_session_path = os.path.join(
save_dir, last_session_file
last_session_path = os.path.join(save_dir, last_session_file)
last_session = StatusFile(last_session_path, data_format="json")
historical_data = last_session.data_field_or("data", default=dict())
if historical_data:
@@ -537,7 +573,9 @@ class SessionStats(plugins.Plugin):
def _realtime_loop(self):
"""Background thread that collects stats periodically without waiting for epochs"""
update_interval = self.options.get("update_interval", self.DEFAULT_UPDATE_INTERVAL)
update_interval = self.options.get(
"update_interval", self.DEFAULT_UPDATE_INTERVAL
)
agent_acquired = False
while self.running:

View File

@@ -710,7 +710,7 @@ def serializer(obj):
class WebConfig(plugins.Plugin):
__author__ = "33197631+dadav@users.noreply.github.com"
__author__ = "33197631+dadav@users.noreply.github.com modified by wsvdmeer"
__version__ = "1.0.0"
__license__ = "GPL3"
__description__ = "This plugin allows the user to make runtime changes."

View File

@@ -9,8 +9,8 @@ from functools import wraps
import flask
# https://stackoverflow.com/questions/14888799/disable-console-messages-in-flask-server
logging.getLogger('werkzeug').setLevel(logging.ERROR)
os.environ['WERKZEUG_RUN_MAIN'] = 'false'
logging.getLogger("werkzeug").setLevel(logging.ERROR)
os.environ["WERKZEUG_RUN_MAIN"] = "false"
import pwnagotchi
import pwnagotchi.grid as grid
@@ -32,78 +32,120 @@ class Handler:
self._agent = agent
self._app = app
self._app.add_url_rule('/', 'index', self.with_auth(self.index))
self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui))
self._app.add_url_rule("/", "index", self.with_auth(self.index))
self._app.add_url_rule("/ui", "ui", self.with_auth(self.ui))
self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST'])
self._app.add_url_rule('/reboot', 'reboot', self.with_auth(self.reboot), methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
self._app.add_url_rule(
"/shutdown", "shutdown", self.with_auth(self.shutdown), methods=["POST"]
)
self._app.add_url_rule(
"/reboot", "reboot", self.with_auth(self.reboot), methods=["POST"]
)
self._app.add_url_rule(
"/restart", "restart", self.with_auth(self.restart), methods=["POST"]
)
# inbox
self._app.add_url_rule('/inbox', 'inbox', self.with_auth(self.inbox))
self._app.add_url_rule('/inbox/profile', 'inbox_profile', self.with_auth(self.inbox_profile))
self._app.add_url_rule('/inbox/peers', 'inbox_peers', self.with_auth(self.inbox_peers))
self._app.add_url_rule('/inbox/<id>', 'show_message', self.with_auth(self.show_message))
self._app.add_url_rule('/inbox/<id>/<mark>', 'mark_message', self.with_auth(self.mark_message))
self._app.add_url_rule('/inbox/new', 'new_message', self.with_auth(self.new_message))
self._app.add_url_rule('/inbox/send', 'send_message', self.with_auth(self.send_message), methods=['POST'])
self._app.add_url_rule("/inbox", "inbox", self.with_auth(self.inbox))
self._app.add_url_rule(
"/inbox/profile", "inbox_profile", self.with_auth(self.inbox_profile)
)
self._app.add_url_rule(
"/inbox/peers", "inbox_peers", self.with_auth(self.inbox_peers)
)
self._app.add_url_rule(
"/inbox/<id>", "show_message", self.with_auth(self.show_message)
)
self._app.add_url_rule(
"/inbox/<id>/<mark>", "mark_message", self.with_auth(self.mark_message)
)
self._app.add_url_rule(
"/inbox/new", "new_message", self.with_auth(self.new_message)
)
self._app.add_url_rule(
"/inbox/send",
"send_message",
self.with_auth(self.send_message),
methods=["POST"],
)
# plugins
plugins_with_auth = self.with_auth(self.plugins)
self._app.add_url_rule('/plugins', 'plugins', plugins_with_auth, strict_slashes=False,
defaults={'name': None, 'subpath': None})
self._app.add_url_rule('/plugins/<name>', 'plugins', plugins_with_auth, strict_slashes=False,
methods=['GET', 'POST'], defaults={'subpath': None})
self._app.add_url_rule('/plugins/<name>/<path:subpath>', 'plugins', plugins_with_auth, methods=['GET', 'POST'])
self._app.add_url_rule(
"/plugins",
"plugins",
plugins_with_auth,
strict_slashes=False,
defaults={"name": None, "subpath": None},
)
self._app.add_url_rule(
"/plugins/<name>",
"plugins",
plugins_with_auth,
strict_slashes=False,
methods=["GET", "POST"],
defaults={"subpath": None},
)
self._app.add_url_rule(
"/plugins/<name>/<path:subpath>",
"plugins",
plugins_with_auth,
methods=["GET", "POST"],
)
def _check_creds(self, u, p):
# trying to be timing attack safe
return secrets.compare_digest(u, self._config['username']) and \
secrets.compare_digest(p, self._config['password'])
return secrets.compare_digest(
u, self._config["username"]
) and secrets.compare_digest(p, self._config["password"])
def with_auth(self, f):
@wraps(f)
def wrapper(*args, **kwargs):
if not self._config['auth']:
if not self._config["auth"]:
return f(*args, **kwargs)
else:
auth = request.authorization
if not auth or not auth.username or not auth.password or not self._check_creds(auth.username,
auth.password):
return Response('Unauthorized', 401, {'WWW-Authenticate': 'Basic realm="Unauthorized"'})
if (
not auth
or not auth.username
or not auth.password
or not self._check_creds(auth.username, auth.password)
):
return Response(
"Unauthorized",
401,
{"WWW-Authenticate": 'Basic realm="Unauthorized"'},
)
return f(*args, **kwargs)
return wrapper
def index(self):
return render_template('index.html',
title=pwnagotchi.name(),
other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU',
fingerprint=self._agent.fingerprint())
return render_template(
"index.html",
title=pwnagotchi.name(),
other_mode="AUTO" if self._agent.mode == "manual" else "MANU",
fingerprint=self._agent.fingerprint(),
)
def inbox(self):
page = request.args.get("p", default=1, type=int)
inbox = {
"pages": 1,
"records": 0,
"messages": []
}
inbox = {"pages": 1, "records": 0, "messages": []}
error = None
try:
if not grid.is_connected():
raise Exception('not connected')
raise Exception("not connected")
inbox = grid.inbox(page, with_pager=True)
except Exception as e:
logging.exception('error while reading pwnmail inbox')
logging.exception("error while reading pwnmail inbox")
error = str(e)
return render_template('inbox.html',
name=pwnagotchi.name(),
page=page,
error=error,
inbox=inbox)
return render_template(
"inbox.html", name=pwnagotchi.name(), page=page, error=error, inbox=inbox
)
def inbox_profile(self):
data = {}
@@ -112,14 +154,16 @@ class Handler:
try:
data = grid.get_advertisement_data()
except Exception as e:
logging.exception('error while reading pwngrid data')
logging.exception("error while reading pwngrid data")
error = str(e)
return render_template('profile.html',
name=pwnagotchi.name(),
fingerprint=self._agent.fingerprint(),
data=json.dumps(data, indent=2),
error=error)
return render_template(
"profile.html",
name=pwnagotchi.name(),
fingerprint=self._agent.fingerprint(),
data=json.dumps(data, indent=2),
error=error,
)
def inbox_peers(self):
peers = {}
@@ -128,13 +172,12 @@ class Handler:
try:
peers = grid.memory()
except Exception as e:
logging.exception('error while reading pwngrid peers')
logging.exception("error while reading pwngrid peers")
error = str(e)
return render_template('peers.html',
name=pwnagotchi.name(),
peers=peers,
error=error)
return render_template(
"peers.html", name=pwnagotchi.name(), peers=peers, error=error
)
def show_message(self, id):
message = {}
@@ -142,23 +185,22 @@ class Handler:
try:
if not grid.is_connected():
raise Exception('not connected')
raise Exception("not connected")
message = grid.inbox_message(id)
if message['data']:
message['data'] = base64.b64decode(message['data']).decode("utf-8")
if message["data"]:
message["data"] = base64.b64decode(message["data"]).decode("utf-8")
except Exception as e:
logging.exception('error while reading pwnmail message %d' % int(id))
logging.exception("error while reading pwnmail message %d" % int(id))
error = str(e)
return render_template('message.html',
name=pwnagotchi.name(),
error=error,
message=message)
return render_template(
"message.html", name=pwnagotchi.name(), error=error, message=message
)
def new_message(self):
to = request.args.get("to", default="")
return render_template('new_message.html', to=to)
return render_template("new_message.html", to=to)
def send_message(self):
to = request.form["to"]
@@ -167,7 +209,7 @@ class Handler:
try:
if not grid.is_connected():
raise Exception('not connected')
raise Exception("not connected")
grid.send_message(to, message)
except Exception as e:
@@ -185,18 +227,41 @@ class Handler:
def plugins(self, name, subpath):
if name is None:
return render_template('plugins.html', loaded=plugins.loaded, database=plugins.database)
# Determine which plugins are from the default folder
default_plugins = set()
default_path = os.path.join(
os.path.dirname(os.path.realpath(plugins.__file__)), "default"
)
for plugin_name, plugin_path in plugins.database.items():
if plugin_path.startswith(default_path):
default_plugins.add(plugin_name)
return render_template(
"plugins.html",
loaded=plugins.loaded,
database=plugins.database,
default_plugins=default_plugins,
)
if name == 'toggle' and request.method == 'POST':
checked = True if 'enabled' in request.form else False
return 'success' if plugins.toggle_plugin(request.form['plugin'], checked) else 'failed'
if name == "toggle" and request.method == "POST":
checked = True if "enabled" in request.form else False
return (
"success"
if plugins.toggle_plugin(request.form["plugin"], checked)
else "failed"
)
if name == 'upgrade' and request.method == 'POST':
if name == "upgrade" and request.method == "POST":
logging.info(f"Upgrading plugin: {request.form['plugin']}")
os.system(f"pwnagotchi plugins update && pwnagotchi plugins upgrade {request.form['plugin']}")
os.system(
f"pwnagotchi plugins update && pwnagotchi plugins upgrade {request.form['plugin']}"
)
return redirect("/plugins")
if name in plugins.loaded and plugins.loaded[name] is not None and hasattr(plugins.loaded[name], 'on_webhook'):
if (
name in plugins.loaded
and plugins.loaded[name] is not None
and hasattr(plugins.loaded[name], "on_webhook")
):
try:
return plugins.loaded[name].on_webhook(subpath, request)
except Exception:
@@ -207,32 +272,44 @@ class Handler:
# serve a message and shuts down the unit
def shutdown(self):
try:
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
message='Shutting down ...')
return render_template(
"status.html",
title=pwnagotchi.name(),
go_back_after=60,
message="Shutting down ...",
)
finally:
_thread.start_new_thread(pwnagotchi.shutdown, ())
# serve a message and reboot the unit
def reboot(self):
try:
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
message='Rebooting ...')
finally:
_thread.start_new_thread(pwnagotchi.reboot, ())
try:
return render_template(
"status.html",
title=pwnagotchi.name(),
go_back_after=60,
message="Rebooting ...",
)
finally:
_thread.start_new_thread(pwnagotchi.reboot, ())
# serve a message and restart the unit in the other mode
def restart(self):
mode = request.form['mode']
if mode not in ('AUTO', 'MANU'):
mode = 'MANU'
mode = request.form["mode"]
if mode not in ("AUTO", "MANU"):
mode = "MANU"
try:
return render_template('status.html', title=pwnagotchi.name(), go_back_after=30,
message='Restarting in %s mode ...' % mode)
return render_template(
"status.html",
title=pwnagotchi.name(),
go_back_after=30,
message="Restarting in %s mode ..." % mode,
)
finally:
_thread.start_new_thread(pwnagotchi.restart, (mode,))
# serve the PNG file with the display image
def ui(self):
with web.frame_lock:
return send_file(web.frame_path, mimetype='image/png')
return send_file(web.frame_path, mimetype="image/png")

View File

@@ -0,0 +1,228 @@
/* ============================================
Inbox/Messages Page Styling
============================================ */
/* ============================================
Message Display
============================================ */
.messagebody {
padding: 1rem;
background-color: #161616;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
border: 1px solid #333;
}
.messagebody-meta {
color: var(--text-muted);
font-size: 0.875rem;
margin-bottom: 1rem;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.messagebody h1,
.messagebody h2,
.messagebody h3,
.messagebody h4,
.messagebody h5,
.messagebody h6 {
word-wrap: break-word;
overflow-wrap: break-word;
}
.message-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.message-actions a {
padding: 0.5rem 1rem;
background-color: var(--accent);
color: #000;
border-radius: 50px;
transition: var(--transition);
font-weight: 400;
text-transform: uppercase;
font-size: 0.85rem;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
min-height: 36px;
white-space: nowrap;
text-decoration: none;
}
.message-actions a:hover {
background-color: var(--accent-hover);
text-decoration: none;
transform: translateY(-2px);
}
.message-actions a.btn.danger {
background-color: var(--danger);
color: #fff;
}
.message-actions a.btn.danger:hover {
background-color: var(--danger-hover);
}
/* ============================================
Inbox List
============================================ */
.inbox,
.peers {
list-style: none;
padding: 0;
margin: 0 0 1rem 0;
}
.inbox-item,
.peer,
.message {
display: flex;
padding: 1rem;
background-color: var(--card-bg);
border-bottom: 1px solid #333;
border-radius: 8px;
transition: var(--transition);
cursor: pointer;
}
.inbox-item:first-child,
.peer:first-child,
.message:first-child {
border-top: 1px solid #333;
}
.inbox-item:hover,
.peer:hover,
.message:hover {
background-color: #252525;
border-left: 3px solid var(--accent);
padding-left: calc(1rem - 3px);
}
.inbox-item a,
.peer a,
.message a {
display: block;
color: inherit;
text-decoration: none;
flex: 1;
min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.inbox-item a:hover,
.peer a:hover,
.message a:hover {
text-decoration: none;
}
.inbox-item h2,
.peer h2,
.message h2 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--accent);
word-wrap: break-word;
overflow-wrap: break-word;
}
.inbox-item p,
.peer p,
.message p {
margin: 0;
font-size: 0.9rem;
color: var(--text-muted);
word-wrap: break-word;
overflow-wrap: break-word;
}
.unread {
font-weight: 600;
color: var(--text-bright);
background-color: rgba(76, 175, 80, 0.15);
border-left: 3px solid var(--accent);
padding-left: calc(1rem - 3px);
}
/* ============================================
Search & Filter
============================================ */
.search-box {
margin-bottom: 1rem;
padding: 0.75rem;
background-color: var(--card-bg);
border: 1px solid #333;
border-radius: 6px;
position: sticky;
top: 0;
z-index: 10;
}
.search-box input {
width: 100%;
margin: 0;
background-color: #0a0a0a;
}
/* ============================================
Pagination
============================================ */
.pagination {
display: flex;
list-style: none;
padding: 0;
margin: 1rem 0;
gap: 0.25rem;
justify-content: center;
flex-wrap: wrap;
}
.pagination .page-item {
display: inline-block;
}
.pagination .page-link {
display: block;
padding: 0.5rem 0.75rem;
background-color: var(--card-bg);
border: 1px solid #333;
color: var(--text-main);
transition: var(--transition);
border-radius: 4px;
font-weight: 400;
}
.pagination .page-link:hover {
background-color: var(--accent);
color: #000;
text-decoration: none;
border-color: var(--accent);
}
.pagination .page-item.active .page-link {
background-color: var(--accent);
color: #000;
border-color: var(--accent);
}
.pagination .page-item.disabled .page-link {
opacity: 0.4;
cursor: not-allowed;
}

View File

@@ -0,0 +1,105 @@
/* ============================================
Plugins Page Styling
============================================ */
/* Plugin-specific note: Badge styles (.badge, .badge.version, .badge.default)
are now in style.css as reusable components */
/* Plugins styling */
.plugins-box h4 {
font-size: 1.4rem;
margin: 0 0 0.25rem 0 !important;
color: #ffffff;
}
.plugins-box h4 a {
color: var(--accent);
}
.plugin-header {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 0.5rem;
width: 100%;
position: relative;
}
.plugin-header h4 {
flex: 0 1 auto;
text-align: center;
}
.plugin-header .badge.version {
position: absolute;
right: 0;
top: 0;
}
.tooltip {
display: flex;
flex-direction: column;
}
.plugin-badges {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: auto;
margin-bottom: 0.75rem;
}
.author {
font-size: 0.75rem;
color: #666;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
max-width: 85%;
margin: 0 auto 0.5rem auto;
text-align: center;
line-height: 1.2;
}
.plugins-box {
background-color: var(--card-bg);
border: 2px solid #333;
border-radius: 16px;
padding: 12px;
margin: 0;
text-align: center;
transition: var(--transition);
display: flex;
flex-direction: column;
min-width: 150px;
flex: 1 1 calc(33.333% - 16px);
height: 100%;
}
.plugins-box > div:first-child {
flex: 0 0 auto;
}
.plugins-box > div:last-child {
margin-top: auto;
}
.plugins-box:hover {
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
border-color: var(--accent);
transform: translateY(-5px) scale(1.02);
}
#container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
max-width: 1200px;
margin: 0 auto;
padding: 0;
}

View File

@@ -0,0 +1,51 @@
/* ============================================
Profile Page Styling
============================================ */
/* Profile page styling */
#profile .card {
max-width: 600px;
margin: 0 auto;
}
#profile .card > div {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #333;
}
#profile .card > div:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
#profile label {
display: block;
font-size: 0.9rem;
color: var(--accent);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
font-family: var(--font-pixel);
}
#profile h4 {
font-size: 1.4rem;
margin: 0.5rem 0 0 0;
color: var(--accent);
}
#profile pre {
font-size: 0.8rem;
max-height: 200px;
overflow-y: auto;
}
#qrcode {
background-color: #fff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}

View File

@@ -555,11 +555,6 @@ textarea {
min-height: 6rem;
}
textarea {
resize: vertical;
min-height: 6rem;
}
/* ============================================
Buttons
============================================ */
@@ -589,27 +584,6 @@ input[type="reset"],
letter-spacing: 0.5px;
}
button:hover,
input[type="submit"]:hover,
input[type="button"]:hover,
input[type="reset"]:hover,
.btn:hover {
background: #fff;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.5);
}
button:disabled,
input[type="submit"]:disabled,
input[type="button"]:disabled,
input[type="reset"]:disabled,
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
button:hover,
input[type="submit"]:hover,
input[type="button"]:hover,
@@ -641,35 +615,35 @@ input[type="reset"]:disabled,
transform: none;
}
.btn-danger {
.btn.danger {
background-color: var(--danger);
box-shadow: 0 4px 12px rgba(255, 85, 85, 0.3);
color: #fff;
}
.btn-danger:hover {
.btn.danger:hover {
background-color: var(--danger-hover);
box-shadow: 0 6px 16px rgba(255, 85, 85, 0.4);
}
.btn-info {
.btn.info {
background-color: var(--info);
color: #000;
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.3);
}
.btn-info:hover {
.btn.info:hover {
background-color: #29b6f6;
box-shadow: 0 6px 16px rgba(79, 195, 247, 0.4);
}
.btn-secondary {
.btn.secondary {
background-color: var(--text-muted);
color: #000;
box-shadow: 0 4px 12px rgba(136, 136, 136, 0.3);
}
.btn-secondary:hover {
.btn.secondary:hover {
background-color: #aaa;
box-shadow: 0 6px 16px rgba(136, 136, 136, 0.4);
}
@@ -841,6 +815,7 @@ li {
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 999;
line-clamp: 999;
-webkit-box-orient: vertical;
flex-grow: 1;
background-color: var(--card-bg);
@@ -876,98 +851,32 @@ li {
gap: 0.5rem;
}
/* Profile page styling */
#profile .card {
max-width: 600px;
margin: 0 auto;
}
#profile .card > div {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #333;
}
#profile .card > div:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
#profile label {
display: block;
font-size: 0.9rem;
color: var(--accent);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
font-family: var(--font-pixel);
}
#profile h4 {
font-size: 1.4rem;
margin: 0.5rem 0 0 0;
color: var(--accent);
}
#profile pre {
font-size: 0.8rem;
max-height: 200px;
overflow-y: auto;
}
#qrcode {
background-color: #fff;
padding: 10px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
/* Plugins styling */
.plugins-box h4 {
font-size: 1.4rem;
margin: 0 0 0.5rem 0 !important;
color: #ffffff;
}
.plugins-box h4 a {
color: var(--accent);
}
.plugins-box {
background-color: var(--card-bg);
border: 2px solid #333;
border-radius: 16px;
padding: 12px;
margin: 0;
text-align: center;
transition: var(--transition);
display: flex;
flex-direction: column;
min-width: 150px;
flex: 1 1 calc(33.333% - 16px);
}
.plugins-box:hover {
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
border-color: var(--accent);
transform: translateY(-5px) scale(1.02);
}
#container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
max-width: 1200px;
margin: 0 auto;
padding: 0;
}
/* ============================================
Tooltips
Badges
============================================ */
.badge {
background: #2a2a2a;
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 4px;
color: #777;
font-family: var(--font-pixel);
text-transform: uppercase;
letter-spacing: 0.3px;
border: 1px solid #444;
}
.badge.version {
flex-shrink: 0;
}
.badge.default {
background: #2a3a2a;
color: #888;
border-color: #3a4a3a;
}
.tooltip {
position: relative;
display: inline-block;
@@ -1152,81 +1061,7 @@ code {
}
/* ============================================
Message Display
============================================ */
.messagebody {
padding: 1rem;
background-color: #161616;
border-radius: 6px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
border: 1px solid #333;
}
.messagebody-meta {
color: var(--text-muted);
font-size: 0.875rem;
margin-bottom: 1rem;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.messagebody h1,
.messagebody h2,
.messagebody h3,
.messagebody h4,
.messagebody h5,
.messagebody h6 {
word-wrap: break-word;
overflow-wrap: break-word;
}
.message-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.message-actions a {
padding: 0.5rem 1rem;
background-color: var(--accent);
color: #000;
border-radius: 50px;
transition: var(--transition);
font-weight: 400;
text-transform: uppercase;
font-size: 0.85rem;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
min-height: 36px;
white-space: nowrap;
text-decoration: none;
}
.message-actions a:hover {
background-color: var(--accent-hover);
text-decoration: none;
transform: translateY(-2px);
}
.message-actions a.btn-danger {
background-color: var(--danger);
color: #fff;
}
.message-actions a.btn-danger:hover {
background-color: var(--danger-hover);
}
/* ============================================
Button Styles
Tooltips
============================================ */
.btn {
@@ -1246,23 +1081,23 @@ code {
cursor: pointer;
}
.btn-primary {
.btn.primary {
background-color: var(--accent);
color: #000;
}
.btn-primary:hover {
.btn.primary:hover {
background-color: var(--accent-hover);
text-decoration: none;
transform: translateY(-2px);
}
.btn-danger {
.btn.danger {
background-color: var(--danger);
color: #fff;
}
.btn-danger:hover {
.btn.danger:hover {
background-color: var(--danger-hover);
text-decoration: none;
transform: translateY(-2px);
@@ -1272,162 +1107,6 @@ code {
color: var(--text-secondary) !important;
}
/* ============================================
Inbox List
============================================ */
.inbox,
.peers {
list-style: none;
padding: 0;
margin: 0 0 1rem 0;
}
.inbox-item,
.peer,
.message {
display: flex;
padding: 1rem;
background-color: var(--card-bg);
border-bottom: 1px solid #333;
border-radius: 8px;
transition: var(--transition);
cursor: pointer;
}
.inbox-item:first-child,
.peer:first-child,
.message:first-child {
border-top: 1px solid #333;
}
.inbox-item:last-child,
.peer:last-child,
.message:last-child {
}
.inbox-item:hover,
.peer:hover,
.message:hover {
background-color: #252525;
border-left: 3px solid var(--accent);
padding-left: calc(1rem - 3px);
}
.inbox-item a,
.peer a,
.message a {
display: block;
color: inherit;
text-decoration: none;
flex: 1;
min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.inbox-item a:hover,
.peer a:hover,
.message a:hover {
text-decoration: none;
}
.inbox-item h2,
.peer h2,
.message h2 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
color: var(--accent);
word-wrap: break-word;
overflow-wrap: break-word;
}
.inbox-item p,
.peer p,
.message p {
margin: 0;
font-size: 0.9rem;
color: var(--text-muted);
word-wrap: break-word;
overflow-wrap: break-word;
}
.unread {
font-weight: 600;
color: var(--text-bright);
background-color: rgba(76, 175, 80, 0.15);
border-left: 3px solid var(--accent);
padding-left: calc(1rem - 3px);
}
/* ============================================
Search & Filter
============================================ */
.search-box {
margin-bottom: 1rem;
padding: 0.75rem;
background-color: var(--card-bg);
border: 1px solid #333;
border-radius: 6px;
position: sticky;
top: 0;
z-index: 10;
}
.search-box input {
width: 100%;
margin: 0;
background-color: #0a0a0a;
}
/* ============================================
Pagination
============================================ */
.pagination {
display: flex;
list-style: none;
padding: 0;
margin: 1rem 0;
gap: 0.25rem;
justify-content: center;
flex-wrap: wrap;
}
.pagination .page-item {
display: inline-block;
}
.pagination .page-link {
display: block;
padding: 0.5rem 0.75rem;
background-color: var(--card-bg);
border: 1px solid #333;
color: var(--text-main);
transition: var(--transition);
border-radius: 4px;
font-weight: 400;
}
.pagination .page-link:hover {
background-color: var(--accent);
color: #000;
text-decoration: none;
border-color: var(--accent);
}
.pagination .page-item.active .page-link {
background-color: var(--accent);
color: #000;
border-color: var(--accent);
}
.pagination .page-item.disabled .page-link {
opacity: 0.4;
cursor: not-allowed;
}
/* ============================================
Utility Classes
============================================ */

View File

@@ -13,6 +13,13 @@
{% block styles %}
<link rel="stylesheet" type="text/css" href="/css/style.css" />
{% if active_page == 'profile' %}
<link rel="stylesheet" type="text/css" href="/css/profile.css" />
{% elif active_page in ['inbox', 'new'] %}
<link rel="stylesheet" type="text/css" href="/css/inbox.css" />
{% elif active_page == 'plugins' %}
<link rel="stylesheet" type="text/css" href="/css/plugins.css" />
{% endif %}
<link rel="apple-touch-icon" href="/images/pwnagotchi.png" />
<link rel="icon" type="image/png" href="/images/pwnagotchi.png" />
{% endblock %}

View File

@@ -11,7 +11,7 @@ block content %}
action="/shutdown"
onsubmit="return confirm('This will halt the unit, continue?');"
>
<button type="submit" class="btn btn-danger">Shutdown</button>
<button type="submit" class="btn danger">Shutdown</button>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>
<form
@@ -19,7 +19,7 @@ block content %}
action="/reboot"
onsubmit="return confirm('This will reboot the unit, continue?');"
>
<button type="submit" class="btn btn-secondary">Reboot</button>
<button type="submit" class="btn secondary">Reboot</button>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>
<form
@@ -27,7 +27,7 @@ block content %}
action="/restart"
onsubmit="return confirm('This will restart the service in {{ other_mode }} mode, continue?');"
>
<button type="submit" class="btn btn-primary">
<button type="submit" class="btn primary">
Restart {{ other_mode }}
</button>
<input type="hidden" name="mode" value="{{ other_mode }}" />

View File

@@ -30,7 +30,7 @@ content %}
>
<a
href="/inbox/{{ message.id }}/deleted"
class="btn btn-danger"
class="btn danger"
onclick="return confirm('Are you sure?');"
>Delete</a
>

View File

@@ -6,11 +6,25 @@ block script %}{% endblock %} {% block content %}
loaded[name].__description__ is defined %}
<div class="plugins-box">
<div class="tooltip">
<h4>
{% if name in loaded and loaded[name].on_webhook is defined %}
<a href="/plugins/{{ name | urlencode }}">{{ name }}</a>
{% else %}{{ name }}{% endif %}
</h4>
<div class="plugin-header">
<h4>
{% if name in loaded and loaded[name].on_webhook is defined %}
<a href="/plugins/{{ name | urlencode }}">{{ name }}</a>
{% else %}{{ name }}{% endif %}
</h4>
{% if name in loaded and loaded[name].__version__ is defined %}
<span class="badge version">{{ loaded[name].__version__ }}</span>
{% endif %}
</div>
<div class="author">
{% if name in loaded and loaded[name].__author__ is defined %} {{
loaded[name].__author__ }} {% endif %}
</div>
<div class="plugin-badges">
{% if name in default_plugins %}
<span class="badge default">DEFAULT</span>
{% endif %}
</div>
{% if has_info %}
<span class="tooltiptext">{{ loaded[name].__description__ }}</span>
{% else %}
@@ -44,7 +58,7 @@ block script %}{% endblock %} {% block content %}
<button
type="submit"
name="upgrade"
class="btn btn-primary plugin-upgrade-btn"
class="btn primary plugin-upgrade-btn"
>
Upgrade Plugin
</button>