Rename to stealergram, add pyproject.toml, purge em-dashes

- Rename project to stealergram throughout
- Add pyproject.toml (replaces requirements.txt split, folds pytest.ini)
- Replace all em-dashes with hyphens across all source files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 10:06:30 -04:00
parent 4c104cddd2
commit 741e6bb0d3
46 changed files with 244 additions and 191 deletions

View File

@@ -1 +1 @@
"""tui Textual TUI frontend and event bus."""
"""tui - Textual TUI frontend and event bus."""

View File

@@ -34,8 +34,8 @@ MonitorApp (App)
### Threading model
- **Bot backend** → `threading.Thread(daemon=True)` with its own `asyncio.new_event_loop()`
Runs `_bot_main()` Telethon is completely isolated from Textual's loop.
- **TUI drain** → `set_interval(0.1, _drain_bus)` polls `queue.Queue` every 100ms on Textual's loop.
Runs `_bot_main()` - Telethon is completely isolated from Textual's loop.
- **TUI drain** → `set_interval(0.1, _drain_bus)` - polls `queue.Queue` every 100ms on Textual's loop.
### Key methods
@@ -105,7 +105,7 @@ Changes apply immediately (handler re-registered). Not persisted to `config.py`
- Validates regex before adding
- On change: rebuilds `utils.scorer.EMPLOYEE_DOMAINS` and `ORG_DOMAINS`
- Bot handler recompiles patterns on the next incoming message automatically
- **Changes are in-memory only** copy to `config.py` to persist
- **Changes are in-memory only** - copy to `config.py` to persist
---

View File

@@ -1,5 +1,5 @@
"""
tui.py Textual TUI for the ULP credential monitor.
tui.py - Textual TUI for the ULP credential monitor.
Layout (main screen):
┌──────────────────────────────────┬──────────────────────────────────┐
@@ -14,13 +14,13 @@ Layout (main screen):
└─────────────────────────────────────────────────────────────────────┘
Additional screens (push/pop via keybindings):
• SearchScreen full-text search across hits DB [s]
• HitsDBScreen paginated recent / severity viewer [h]
• KeywordsScreen live-edit TARGET_KEYWORDS regex list [k]
• SearchScreen - full-text search across hits DB [s]
• HitsDBScreen - paginated recent / severity viewer [h]
• KeywordsScreen - live-edit TARGET_KEYWORDS regex list [k]
Architecture:
- The entire bot backend runs as a Textual Worker (asyncio task inside the
TUI event loop no threading needed).
TUI event loop - no threading needed).
- A second Worker runs _bus_consumer(), reading events from tui_events.queue
and dispatching to the right panel.
- Channel add/remove from the UI immediately re-registers Telethon handlers
@@ -29,7 +29,7 @@ Architecture:
into the download panel's RichLog.
- StatsPanel polls database.stats() every 10 s via set_interval().
- Keyword changes are applied in-memory immediately (scorer caches rebuilt);
NOT auto-persisted to config.py a notice banner reminds the user.
NOT auto-persisted to config.py - a notice banner reminds the user.
- Live patterns are recompiled from config.TARGET_KEYWORDS on every message
so keyword changes take effect without a handler restart.
"""
@@ -88,7 +88,7 @@ def _now() -> str:
class DownloadPanel(Vertical):
"""
Left panel two sub-logs stacked vertically:
Left panel - two sub-logs stacked vertically:
• top: tdl raw output (stripped ANSI), scrolling
• bottom: our own structured status entries
"""
@@ -158,7 +158,7 @@ class DownloadPanel(Vertical):
# ─── Hits panel ───────────────────────────────────────────────────────────────
class HitsPanel(Vertical):
"""Right panel scrollable color-coded hit log with live counter badge."""
"""Right panel - scrollable color-coded hit log with live counter badge."""
hit_count: reactive[int] = reactive(0)
@@ -208,7 +208,7 @@ class HitsPanel(Vertical):
class StatsPanel(Horizontal):
"""
Slim bar shows live DB stats, refreshed every 10 s.
Slim bar - shows live DB stats, refreshed every 10 s.
Also refreshed immediately whenever a new hit arrives.
"""
@@ -233,14 +233,14 @@ class StatsPanel(Horizontal):
def compose(self) -> ComposeResult:
yield Static("📊 DB Stats", id="stat-label")
yield Static("🔴 ", classes="stat-critical", id="stat-critical")
yield Static("🟠 ", classes="stat-high", id="stat-high")
yield Static("🟡 ", classes="stat-medium", id="stat-medium")
yield Static("🟢 ", classes="stat-low", id="stat-low")
yield Static("total: ", id="stat-total")
yield Static("unique: ", id="stat-unique")
yield Static("dupes: ", id="stat-dupes")
yield Static("sources: ", id="stat-sources")
yield Static("🔴 - ", classes="stat-critical", id="stat-critical")
yield Static("🟠 - ", classes="stat-high", id="stat-high")
yield Static("🟡 - ", classes="stat-medium", id="stat-medium")
yield Static("🟢 - ", classes="stat-low", id="stat-low")
yield Static("total: - ", id="stat-total")
yield Static("unique: - ", id="stat-unique")
yield Static("dupes: - ", id="stat-dupes")
yield Static("sources: - ", id="stat-sources")
def on_mount(self) -> None:
self.set_interval(10, self.refresh_stats)
@@ -266,7 +266,7 @@ class StatsPanel(Horizontal):
class ChannelPanel(Vertical):
"""
Bottom panel live-editable channel list.
Bottom panel - live-editable channel list.
Changes are applied immediately (Telethon handlers are re-registered).
To make them permanent, edit config.py's WATCHED_CHANNELS manually.
@@ -314,7 +314,7 @@ class ChannelPanel(Vertical):
def compose(self) -> ComposeResult:
yield Label(
"📡 Channels changes apply immediately | edit config.py to persist",
"📡 Channels - changes apply immediately | edit config.py to persist",
classes="panel-title",
)
with Horizontal(classes="controls"):
@@ -524,7 +524,7 @@ class HitsDBScreen(Screen):
status,
)
self.query_one("#db-status", Label).update(
f" {len(rows)} row(s) {label}"
f" {len(rows)} row(s) - {label}"
)
def _load_recent(self) -> None:
@@ -560,7 +560,7 @@ class KeywordsScreen(Screen):
• scorer's domain caches are rebuilt
• The bot handler recompiles patterns on the next message automatically
Changes are NOT written back to config.py a notice banner says so.
Changes are NOT written back to config.py - a notice banner says so.
"""
BINDINGS = [Binding("escape", "dismiss", "Back")]
@@ -601,7 +601,7 @@ class KeywordsScreen(Screen):
yield Header()
yield Label("🔑 Keyword / Pattern Editor", classes="screen-title")
yield Label(
"⚠ Changes are in-memory only copy patterns to config.py to persist across restarts.",
"⚠ Changes are in-memory only - copy patterns to config.py to persist across restarts.",
classes="notice",
)
with Horizontal(id="kw-controls"):
@@ -671,7 +671,7 @@ class KeywordsScreen(Screen):
except Exception as e:
log.warning(f"Could not rebuild scorer caches: {e}")
bus.post(bus.EvStatus(
f"Keywords updated {len(config.TARGET_KEYWORDS)} pattern(s) active"
f"Keywords updated - {len(config.TARGET_KEYWORDS)} pattern(s) active"
))
def action_dismiss(self) -> None:
@@ -721,7 +721,7 @@ class MonitorApp(App):
# The bot backend runs in its own thread with its own asyncio event
# loop, completely isolated from Textual. Telethon spawns background
# tasks via asyncio.ensure_future() and calls connect() which returns
# only after its receiver loop is scheduled both of these deadlock
# only after its receiver loop is scheduled - both of these deadlock
# inside Textual's managed loop. Running in a dedicated thread
# sidesteps all of that.
#
@@ -767,7 +767,7 @@ class MonitorApp(App):
"""
Called every 100 ms by set_interval(). Drains all pending events
from the thread-safe queue and dispatches them to the right widget.
Runs on Textual's event loop safe to call widget methods directly.
Runs on Textual's event loop - safe to call widget methods directly.
"""
q = bus.get_bus()
if q is None:
@@ -854,7 +854,7 @@ class MonitorApp(App):
async def _bot_main(self) -> None:
"""
Full bot backend runs inside the bot thread's own event loop.
Full bot backend - runs inside the bot thread's own event loop.
Telethon is free to schedule background tasks without interfering
with Textual's loop.
"""
@@ -870,7 +870,7 @@ class MonitorApp(App):
patterns = compile_patterns(config.TARGET_KEYWORDS)
bus.post(bus.EvStatus(
f"Starting {len(config.WATCHED_CHANNELS)} channel(s), "
f"Starting - {len(config.WATCHED_CHANNELS)} channel(s), "
f"{len(patterns)} pattern(s)"
))
@@ -894,9 +894,9 @@ class MonitorApp(App):
await user_client.connect()
log.info("[bot] user_client connected, checking auth...")
if not await user_client.is_user_authorized():
log.error("[bot] user_client not authorized run: python main.py --no-tui")
log.error("[bot] user_client not authorized - run: python main.py --no-tui")
bus.post(bus.EvStatus(
"Not authorized run --no-tui once to complete login",
"Not authorized - run --no-tui once to complete login",
level="error",
))
return
@@ -962,7 +962,7 @@ class MonitorApp(App):
log.info(f"[bot] Handler registered for {len(channels)} channel(s)")
bus.post(bus.EvStatus(f"Watching {len(channels)} channel(s)"))
# Channel-change event lives on this (bot) loop.
# Channel-change event - lives on this (bot) loop.
# Textual signals it thread-safely via _signal_channel_changed().
_ch_changed = asyncio.Event()
self._bot_loop_channel_event = _ch_changed
@@ -971,7 +971,7 @@ class MonitorApp(App):
bus.post(bus.EvStatus("Live listener active"))
await backfill_all(user_client, bot_client, patterns)
bus.post(bus.EvStatus("Backfill complete monitoring live"))
bus.post(bus.EvStatus("Backfill complete - monitoring live"))
async def _watch_channels():
while True:
@@ -1009,7 +1009,7 @@ class MonitorApp(App):
# ─── Entry point ──────────────────────────────────────────────────────────────
def run_tui() -> None:
# Do NOT call bus.init_bus() here the Queue must be created inside
# Do NOT call bus.init_bus() here - the Queue must be created inside
# Textual's event loop (see MonitorApp.on_mount). Calling it here
# would bind the Queue to the outer loop which is discarded when
# App.run() creates a new one.

View File

@@ -14,11 +14,11 @@ from tui.events import set_bot_context, signal_channel_changed
```
### `init_bus() -> queue.Queue`
Creates the `queue.Queue`. Called inside `MonitorApp.on_mount()` **must run on Textual's event loop**, not before `App.run()`.
Creates the `queue.Queue`. Called inside `MonitorApp.on_mount()` - **must run on Textual's event loop**, not before `App.run()`.
### `post(event: Any) -> None`
Fire-and-forget from any thread. Delivers to the TUI queue **and** all subscriber queues.
Uses `queue.Queue.put_nowait()` never blocks.
Uses `queue.Queue.put_nowait()` - never blocks.
### `get_bus() -> queue.Queue | None`
Returns the TUI queue for `_drain_bus()` to consume.

View File

@@ -1,5 +1,5 @@
"""
tui_events.py Thread-safe event bus between the bot backend and the TUI.
tui_events.py - Thread-safe event bus between the bot backend and the TUI.
The bot backend runs in a dedicated thread with its own asyncio event loop
(completely isolated from Textual's loop). Events are posted via a standard
@@ -18,7 +18,7 @@ import threading
from dataclasses import dataclass, field
from typing import Any
# Thread-safe queue works across the bot thread and Textual's thread.
# Thread-safe queue - works across the bot thread and Textual's thread.
_queue: queue.Queue | None = None
_queue_lock = threading.Lock()