CLI and Desktop¶
Personal Finance ETL has two operating surfaces and one financial engine.
That distinction is the most important thing to understand about the presentation layer:
CLI and Desktop choose how I control the application. They do not select different finance logic.
Both surfaces terminate in PersonalFinanceEngine, which exposes configuration, pipeline execution, snapshot, restore, and preference-facing workflows to presentation code.
flowchart TB
CLI["Rich CLI"]
GUI["DesktopApp"]
ENGINE["PersonalFinanceEngine"]
PIPE["Pipeline orchestration"]
BACKUP["SystemBackupManager"]
PREF["PreferencesManager"]
CLI --> ENGINE
GUI --> ENGINE
ENGINE --> PIPE
ENGINE --> BACKUP
ENGINE --> PREF The launcher contract¶
The canonical launcher is presentation.launcher:main.
Its parser defines:
cli_parser = subparsers.add_parser("cli", help="Launch the terminal CLI")
cli_parser.add_argument("--config", type=str, help="Path to config.toml", default=None)
cli_parser.add_argument("--rules", type=str, help="Path to rules.toml", default=None)
cli_parser.add_argument("--snapshot", action="store_true", ...)
cli_parser.add_argument("--restore", type=str, ...)
cli_parser.add_argument("--auto", action="store_true", ...)
cli_parser.add_argument("--cron", action="store_true", ...)
cli_parser.add_argument("--docs", action="store_true", ...)
Source: src/personal_finance_etl/presentation/launcher.py
No subcommand defaults to the Desktop application.
Entry-point matrix¶
| Command | Behavior |
|---|---|
shan-fin | Desktop |
shan-fin tkinter | Desktop |
shan-fin-gui | Desktop directly |
shan-fin cli | Rich terminal CLI |
shan-fin cli --docs | open integrated documentation |
The installed scripts are declared in pyproject.toml.
CLI operating modes¶
The CLI supports several distinct workflows.
Interactive pipeline run¶
The CLI can prompt for configuration/rules and retain recent selections.
Automatic run¶
--auto skips the explicit ignite confirmation.
It does not make invalid configuration acceptable.
Unattended / cron-style run¶
--cron implies automatic execution and exits after completion rather than entering the interactive restart menu.
Snapshot¶
Restore¶
Documentation portal¶
Interactive lifecycle¶
stateDiagram-v2
[*] --> SelectConfig
SelectConfig --> SelectRules
SelectRules --> Confirm
Confirm --> Running: approved / auto
Confirm --> [*]: abort
Running --> Complete
Complete --> RestartSame
Complete --> RestartNew
Complete --> [*]
RestartSame --> Confirm
RestartNew --> SelectConfig Interface state is not pipeline state.
Each pipeline execution receives its own operational run lifecycle regardless of whether the UI reused the same configuration.
Asynchronous execution¶
The façade exposes:
def run_pipeline_async(
self,
config_path: str,
rules_path: str,
on_status: Callable[[EngineStatusMessage], None],
) -> None:
status_queue: multiprocessing.Queue[EngineStatusMessage] = multiprocessing.Queue()
process = WorkerPoolCoordinator.spawn_process(status_queue, config_path, rules_path)
WorkerPoolCoordinator.monitor_queue(process, status_queue, on_status)
Source: src/personal_finance_etl/application/facade.py
The presentation surface does not run the heavy ETL directly in its event loop.
sequenceDiagram
participant UI as CLI/Desktop
participant E as PersonalFinanceEngine
participant W as WorkerPoolCoordinator
participant P as ETL process
participant Q as status queue
UI->>E: run_pipeline_async(...)
E->>W: spawn_process(...)
W->>P: create worker process
P->>Q: EngineStatusMessage
W->>Q: monitor
W-->>UI: callback(status) That separation matters most for Desktop responsiveness and process isolation.
Typed status messages¶
Presentation does not need to infer machine state from arbitrary strings.
Status crosses the boundary through EngineStatusMessage and logging/status contracts.
The UI can still render human-friendly messages, but machine meaning remains structured.
This is the same principle used throughout the system: logs explain state; typed contracts carry state.
Desktop responsibilities¶
The Desktop application owns human interaction:
- configuration/rules selection;
- running the pipeline;
- progress/status rendering;
- snapshot/restore actions;
- integrated docs access;
- tabs/widgets/theme behavior.
It does not own:
- FIFO;
- Tax logic;
- FIRE simulation;
- warehouse publication;
- recovery semantics.
A visually convenient place to put logic is still the wrong place if it changes financial meaning.
Preferences are not financial configuration¶
Recent config/rules paths are persisted through PreferencesManager.
That is convenience state.
It is intentionally separate from:
A recently opened file path should not become part of the financial model merely because the UI remembers it.
Snapshot behavior¶
PersonalFinanceEngine.snapshot_database_detailed() loads Settings, constructs SystemBackupManager, and creates a coordinated snapshot for the configured DuckDB/SQLite pair.
backup_mgr = SystemBackupManager(
cfg.TARGET_DB_BASE_PATH,
sqlite_db_name=cfg.RAW_DOCUMENT_STORE_NAME,
duckdb_name=cfg.TARGET_DB_NAME,
)
return backup_mgr.create_snapshot()
Source: src/personal_finance_etl/application/facade.py
The presentation layer receives typed SnapshotResult / SnapshotOutcome semantics rather than implementing backup logic itself.
Restore behavior¶
Restore follows the same boundary.
The façade returns a typed RestoreResult containing:
- outcome;
- active database state;
- error message where relevant.
If the active database files are in use, the compatibility wrapper raises PermissionError so the interface can surface a specific operating problem.
Restore semantics are specified in Backup and Restore.
Integrated documentation¶
The application ships the docs tree and uses a manifest-driven catalog.
The docs presentation layer separates:
manifest/catalog
which documents exist and in what order
renderer
Markdown/HTML conversion
guides window
user-facing navigation
That means the docs portal is not a hard-coded list of filenames living inside a window class.
The documentation itself behaves like a product surface with its own catalog contract.
Choosing an interface¶
Use the CLI when:
- automating runs;
- profiling;
- diagnosing logs;
- scripting snapshots;
- running unattended;
- preferring terminal workflows.
Use Desktop when:
- selecting files interactively;
- watching status visually;
- browsing docs inside the application;
- operating the tool as a local desktop product.
Use synchronous engine execution from developer/test contexts when profiling or deterministic in-process execution is useful.
The financial outputs should not depend on which control surface started the run.
Failure behavior¶
Presentation should distinguish at least:
configuration validation failure
pipeline failure
abnormal worker process exit
snapshot failure
restore validation failure
restore install failure
permission/in-use failure
Do not collapse those into one “Something went wrong” modal if the backend already knows exactly what happened.
Useful systems preserve information all the way to the person who needs to act on it.
The interface boundary¶
The final mental model is simple:
presentation
asks
application façade
coordinates
domain / infrastructure
decide and perform
presentation
renders the result
That closes the operational journey. Once a reader can install, configure, run, recover, and explore the system, the next useful question is no longer operational.
It is contextual:
Why does this project exist, and what kind of engineering did it become?
That is the purpose of the About section.
Related specifications¶
Observability and UI Narrative Sinks¶
The application separates user-facing narrative from detailed forensic debugging:
- Concise Narrative Log: The CLI (
RichNarrativeSink) and CustomTkinter Desktop (DesktopNarrativeSink) render only significant milestones (>>,✓) and actionable failure summaries. Diagnostic chatter is dropped automatically. - Forensic JSONL Trace: Deep operational telemetry (ticker cache hits, table row counts, ISIN completion timings) is streamed to
outputs/logs/runs/trace_{run_id}.jsonl. - Single Error Summary: On fatal failures, the desktop interface presents a single actionable error diagnostic rather than multiple modal popups.