Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions fenn/dashboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def _require_login():


@app.errorhandler(CSRFError)
def _csrf_failed(e):
def _csrf_failed(_e):
return render_template(
"connect.html",
error_message="Form expired. Please try again.",
Expand Down Expand Up @@ -233,7 +233,7 @@ def api_sessions():
default 0), sort (field, optionally ``-`` prefixed for descending).
"""
try:
project = request.args.get("project") or None
project_name = request.args.get("project") or None
status = request.args.get("status") or None
sort = request.args.get("sort") or "-started"
limit = _parse_int_arg(
Expand All @@ -243,7 +243,7 @@ def api_sessions():

try:
result = scanner.list_sessions(
project=project,
project=project_name,
status=status,
limit=limit,
offset=offset,
Expand All @@ -262,7 +262,7 @@ def api_sessions():


@app.errorhandler(404)
def not_found(e):
def not_found(_e):
return render_template("404.html", **scanner.get_overview()), 404


Expand Down Expand Up @@ -332,6 +332,7 @@ def logout():
# --------------------------------------------------------------------------- #


# TODO: Use 'debug' in logger
def run(
host: str = "127.0.0.1", port: int = 5000, debug: bool = False, log_dirs=None
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion fenn/dashboard/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The dashboard is a localhost-only Flask app. To authenticate, the user
generates a "dashboard token" on pyfenn.com, pastes it into the
``/connect`` page, and the server validates it once against
``https://pyfenn.com/api/dashboard/me``. On success we store
``https://pyfenn.com/api/dashboard/me``. On success, we store
``{user_id, email}`` in a signed Flask session cookie and discard the
token — it is never written to disk or kept in memory.
"""
Expand Down
35 changes: 19 additions & 16 deletions fenn/dashboard/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import time
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from xml.etree import ElementTree as ET
from xml.etree import ElementTree

# Default directories to scan (resolved at runtime)
_DEFAULT_DIRS = [
Expand Down Expand Up @@ -106,7 +106,7 @@ def find_fn_files(self) -> List[Path]:
if f not in seen:
seen.add(f)
unique.append(f)
ordered = sorted(unique, key=lambda f: f.stat().st_mtime, reverse=True)
ordered = sorted(unique, key=lambda path: path.stat().st_mtime, reverse=True)
self._files_cache = (now, ordered)
return ordered

Expand Down Expand Up @@ -137,25 +137,23 @@ def parse_fn_file(self, path: Path) -> Optional[Dict[str, Any]]:
self._parse_cache[key] = (mtime, parsed)
return self._refresh_running_status(parsed, mtime)

def _parse_uncached(
self, path: Path, stat: os.stat_result
) -> Optional[Dict[str, Any]]:
@staticmethod
def _parse_uncached(path: Path, stat: os.stat_result) -> Optional[Dict[str, Any]]:
try:
content = path.read_text(encoding="utf-8")
except (OSError, PermissionError):
return None

root = None
status = "completed"

try:
root = ET.fromstring(content)
except ET.ParseError:
root = ElementTree.fromstring(content)
except ElementTree.ParseError:
# Session may still be running — try appending the closing tag
try:
root = ET.fromstring(content + "\n</fenn-log>")
root = ElementTree.fromstring(content + "\n</fenn-log>")
status = "running"
except ET.ParseError:
except ElementTree.ParseError:
return None

# Override status from <meta> if present
Expand Down Expand Up @@ -261,7 +259,9 @@ def _build_projects_list(sessions: List[Dict[str, Any]]) -> List[Dict[str, Any]]
p["running_count"] += 1
elif s["status"] == "crashed":
p["crashed_count"] += 1
return sorted(projects.values(), key=lambda p: p["last_active"], reverse=True)
return sorted(
projects.values(), key=lambda project: project["last_active"], reverse=True
)

def get_overview(self) -> Dict[str, Any]:
"""Aggregate stats for the dashboard home page."""
Expand Down Expand Up @@ -360,11 +360,12 @@ def list_sessions(
if status:
sessions = [s for s in sessions if s["status"] == status]

# Sentinel keeps None values consistently last (asc) / first (desc)
# without crashing the comparison on mixed types.
# Sorts None values last (ascending) / first (descending) without comparing None to
# non-None values, but may raise TypeError if non-None values are of different types
# (e.g., int vs str).
def sort_key(s: Dict[str, Any]):
v = s.get(field)
return (v is None, v if v is not None else "")
return v is None, v

sessions.sort(key=sort_key, reverse=descending)

Expand All @@ -380,7 +381,8 @@ def sort_key(s: Dict[str, Any]):
"offset": offset,
}

def format_duration(self, seconds: Optional[int]) -> str:
@staticmethod
def format_duration(seconds: Optional[int]) -> str:
if seconds is None:
return "—"
if seconds < 60:
Expand All @@ -391,7 +393,8 @@ def format_duration(self, seconds: Optional[int]) -> str:
m = (seconds % 3600) // 60
return f"{h}h {m}m"

def format_size(self, size_bytes: int) -> str:
@staticmethod
def format_size(size_bytes: int) -> str:
if size_bytes < 1024:
return f"{size_bytes} B"
if size_bytes < 1024 * 1024:
Expand Down