mirror of
https://github.com/jayofelony/pwnagotchi.git
synced 2026-03-12 21:02:52 -07:00
adjusted session stats, split up css
Signed-off-by: wiebesteven <wiebesteven@freed.nl>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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")
|
||||
|
||||
228
pwnagotchi/ui/web/static/css/inbox.css
Normal file
228
pwnagotchi/ui/web/static/css/inbox.css
Normal 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;
|
||||
}
|
||||
105
pwnagotchi/ui/web/static/css/plugins.css
Normal file
105
pwnagotchi/ui/web/static/css/plugins.css
Normal 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;
|
||||
}
|
||||
51
pwnagotchi/ui/web/static/css/profile.css
Normal file
51
pwnagotchi/ui/web/static/css/profile.css
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
============================================ */
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 }}" />
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user