# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The operator's switches: which commands are on, and how fast they may go. One row per command that has been touched from the management page. A command without a row is on and keeps the cooldown the deployment was started with; a row overrides exactly what it spells out and nothing else. The bot reads these on every command, so a flip on the page is live before the browser has finished repainting -- no reload, no restart, nothing to forget to do. "Off" comes in two shapes and they are read together: ``enabled`` is the hard switch, ``disabled_until`` a timed one that lifts itself. A command is off while either says so. Shares the Store's connection, the same way stats.events.EventStore does. """ from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING if TYPE_CHECKING: import sqlite3 @dataclass(frozen=True, slots=True) class CommandSetting: """What the operator decided about one command.""" command: str enabled: bool = True disabled_until: float | None = None cooldown_seconds: float | None = None def off_at(self, now: float) -> bool: """Whether the command is switched off at this moment. The timed switch is exclusive at its deadline, the same way a mute is: "off until 18:00" is on again at 18:00 sharp. """ if not self.enabled: return True return self.disabled_until is not None and now < self.disabled_until class CommandSettingStore: """Reads and writes the switches. Shares the Store's connection.""" def __init__(self, conn: sqlite3.Connection) -> None: """Wrap an open connection whose schema has already been applied.""" self._conn = conn def get(self, command: str) -> CommandSetting: """The setting for one command; the defaults when it was never touched.""" row = self._conn.execute( "SELECT command, enabled, disabled_until, cooldown_seconds " "FROM command_settings WHERE command = ?", (command,), ).fetchone() return CommandSetting(command) if row is None else _setting(row) def all(self) -> dict[str, CommandSetting]: """Every command that has a row, by name.""" rows = self._conn.execute( "SELECT command, enabled, disabled_until, cooldown_seconds FROM command_settings" ).fetchall() return {str(row["command"]): _setting(row) for row in rows} def put(self, setting: CommandSetting) -> None: """Store one command's setting, replacing whatever was there.""" self._conn.execute( "INSERT INTO command_settings (command, enabled, disabled_until, cooldown_seconds) " "VALUES (?, ?, ?, ?) " "ON CONFLICT(command) DO UPDATE SET " "enabled = excluded.enabled, " "disabled_until = excluded.disabled_until, " "cooldown_seconds = excluded.cooldown_seconds", ( setting.command, int(setting.enabled), setting.disabled_until, setting.cooldown_seconds, ), ) self._conn.commit() def cooldown_for(self, command: str, default: float) -> float: """The cooldown to enforce: the operator's if set, else the deployment's.""" override = self.get(command).cooldown_seconds return default if override is None else override def _setting(row: sqlite3.Row) -> CommandSetting: """One row, typed.""" until = row["disabled_until"] cooldown = row["cooldown_seconds"] return CommandSetting( command=str(row["command"]), enabled=bool(row["enabled"]), disabled_until=None if until is None else float(until), cooldown_seconds=None if cooldown is None else float(cooldown), )