Initial implementation of Sahsa Clock

Pygame framebuffer clock for Le Potato (ARM Debian) with aiohttp webhook server.
Renders 12-hour clock directly to /dev/fb0 (no X11/Wayland). Supports full-screen
message overlays pushed via a browser dashboard or Bearer-token API. Includes
first-run setup wizard, session-based dashboard auth, bcrypt password storage,
per-IP rate limiting, and systemd service unit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 16:22:10 -06:00
commit 0c2392b2b3
13 changed files with 1587 additions and 0 deletions

44
state.py Normal file
View File

@@ -0,0 +1,44 @@
import threading
import time
from dataclasses import dataclass, field
@dataclass
class MessageState:
_text: str | None = field(default=None)
_expires_at: float | None = field(default=None) # monotonic timestamp
_lock: threading.Lock = field(default_factory=threading.Lock)
def set(self, text: str, duration: float | None) -> None:
"""Set message. duration=None means persistent (no expiry)."""
with self._lock:
self._text = text
self._expires_at = time.monotonic() + duration if duration else None
def clear(self) -> None:
with self._lock:
self._text = None
self._expires_at = None
def get(self) -> tuple[str | None, float | None]:
"""Return (text, remaining_seconds). Both None if no active message."""
with self._lock:
if self._text is None:
return None, None
if self._expires_at is not None and time.monotonic() >= self._expires_at:
self._text = None
self._expires_at = None
return None, None
remaining: float | None = None
if self._expires_at is not None:
remaining = max(0.0, self._expires_at - time.monotonic())
return self._text, remaining
def to_dict(self) -> dict:
text, remaining = self.get()
return {
"active": text is not None,
"text": text,
"remaining_seconds": round(remaining, 1) if remaining is not None else None,
"persistent": text is not None and remaining is None,
}