001 Notes

Desktop Local App Lessons

A desktop app is not just a window. It is a local runtime.

That runtime includes:

UI event loop
local storage
settings
worker jobs
parsers
schedulers
logs
generated files
browser sessions
tray lifecycle
shutdown behavior

The common failure mode is simple:

the app works for demo data
then freezes, hides errors, leaks files, or leaves background work running

Expense managers, job scrapers, Kanban boards, price trackers, CVE checkers, dashboards, browser-backed tools, and tray utilities all teach the same lesson: local desktop software becomes reliable only when runtime state is explicit.

1. The Desktop Runtime Mental Model

A desktop app has three different kinds of state:

visible state:
  what the user sees now

durable state:
  local database, config, user files, saved records

runtime state:
  workers, timers, browser sessions, sockets, caches, queues

A bug often happens when these are mixed.

Example:

UI shows "updated"
database write failed
worker is still running
timer will retry in the background

The user thinks the action completed, but the runtime disagrees.

The product should make state visible:

current file/database
last import status
active worker count
last parser error
next scheduled run
cache/log locations
shutdown status

The desktop lesson:

If local state exists, the user should be able to inspect it.
If background work exists, the user should know whether it is running.
If generated files exist, the user should be able to delete them.

2. Main Thread Discipline

The UI thread should do:

paint
layout
input handling
small state updates
signal dispatch
cheap model queries

It should not do:

network requests
HTML parsing
CSV imports
database migrations
large table filtering
file scans
image processing
AI/model inference
browser automation
long compression/export work

Bad case:

user clicks Import CSV
UI parses 200,000 rows on main thread
window freezes
OS says app is not responding

Better case:

flowchart LR
  A[User clicks Import] --> B[Create import job]
  B --> C[Worker parses file]
  C --> D[Progress signal]
  C --> E[Preview result]
  E --> F[UI applies result]
  F --> G[User confirms batch]

The rule is not “threads everywhere.” The rule is:

anything slow, blocking, or unbounded leaves the UI thread

Handlers that are especially dangerous:

paintEvent
resizeEvent
data()
selectionChanged
textChanged
timer callbacks
scroll handlers

These can fire often. They must be cheap.

3. Worker Job Contract

A worker should not just be a background function.

It needs a contract:

FieldPurpose
job idignore stale completions
input snapshotprevent live UI state mutation
progresskeep user informed
cancel flagstop wasted work
result objectapply safely on UI thread
error payloadshow actionable failure
cleanup hookclose files/processes/sessions

Bad worker:

worker reads current UI fields while running
worker mutates widgets directly
worker raises generic exception
worker cannot be cancelled

Better worker:

UI gathers current form values
UI creates immutable request
worker processes request
worker emits progress/result/error
UI thread applies result if job is still current

Stale result protection:

current_job_id = 42
result.job_id = 41

if result.job_id != current_job_id:
  ignore result

This matters for:

search filters
scraper runs
imports
dashboards
background refresh
browser automation
selection detail panels

Late results are normal in concurrent apps. Treat them as expected.

4. PyQt Virtualized Tables

For large tables, do not create one widget per row or cell.

Bad pattern:

for row in rows:
  table.addWidget(row_widget)

This fails when row count grows.

Better pattern:

QAbstractTableModel stores row data
QTableView asks only for visible cells
delegate paints visible cells
worker prepares expensive values

Visual:

flowchart TD
  A[Backing records] --> B[QAbstractTableModel]
  B --> C[QTableView]
  D[Delegate] --> C
  E[Worker filter/sort/import] --> A
  F[SQLite/JSON/File store] --> A

Responsibilities:

PartResponsibility
modelexpose row/column data cheaply
viewscrolling, selection, focus
delegatepaint/edit visible cells
workerexpensive filter/sort/import
storageSQLite/JSON/files as needed

The critical rule:

data(index, role) must be cheap

Bad:

data():
  parse date
  format currency with slow locale lookup
  query database
  fetch URL
  read file

Good:

data():
  return precomputed display value
  return placeholder if expensive value is not ready

If a value is expensive, compute it before the paint path.

5. Sorting And Filtering

Sorting 100,000 rows on every keystroke is bad.

Better flow:

flowchart LR
  A[User types filter] --> B[Debounce]
  B --> C[Worker computes matching row ids]
  C --> D[Model swaps visible mapping]
  D --> E[View repaints]

For small tables, a proxy model may be enough. For larger datasets, use an explicit visible-row mapping:

all_rows = source records
visible_rows = row ids matching current filter

The model maps:

source_row = visible_rows[view_row]

This avoids copying full records around.

Filter state should be visible:

showing 1,284 of 92,000 records
filter: status=open, source=remote, text="python"

Long-running filters should be cancellable:

new filter typed -> cancel old filter job

Otherwise, old filter results can overwrite newer user intent.

6. Import Auditability

Expense managers, chat importers, SMS importers, and CSV tools need audit trails.

Import should not be:

parse file -> insert rows silently

It should be:

parse file
show preview
show parse errors
show duplicate candidates
create import batch
allow rollback/delete batch

Useful import fields:

FieldWhy
import batch idrollback and audit
source filenametrace origin
source row numberexplain parse errors
raw row textdebug normalization
normalized fieldsdisplay final meaning
parse statusseparate valid/invalid rows
duplicate keyprevent repeated imports
created record idlink source to output

Import flow:

flowchart TD
  A[Source file] --> B[Parse preview]
  B --> C[Normalize fields]
  C --> D[Detect duplicates]
  D --> E[Show errors and warnings]
  E --> F{User confirms?}
  F -- no --> G[No durable change]
  F -- yes --> H[Create import batch]
  H --> I[Insert records]
  I --> J[Batch can be reviewed or rolled back]

Why this matters:

if import goes wrong, user can inspect and undo

Money and message-history tools must be explainable. A silent import is not acceptable.

7. Parser Health

Parsers drift.

Examples:

job site changes HTML
bank changes SMS format
chat export format changes
price page moves value into script tag
CVE feed changes schema

Parser health should be product state.

Show:

last successful parse
last failed parse
parse version
records found
records rejected
error sample
next retry
source freshness

Bad UX:

empty result list

Better UX:

Parser failed: salary field selector matched 0 items.
Last good run: yesterday.
Source page changed or parser needs update.

Parser record shape:

source_id
parser_version
last_success_at
last_failure_at
last_error_kind
last_error_sample
records_seen
records_accepted
records_rejected

The user should never have to guess whether there are no results or the parser broke.

8. Scraping: Direct HTTP vs Browser Session

Direct HTTP is simpler:

request page/API
parse HTML/JSON
store result

Use it when:

site is static
API exists
authentication is simple
JavaScript rendering is not required

Browser-session scraping is heavier:

launch browser/profile
load page
wait for dynamic content
extract DOM
close cleanly

Use it when:

session state matters
JavaScript renders content
login/cookies are required
direct HTTP is blocked

Browser scraping needs lifecycle controls:

show browser profile/cache size
show session/login status
stop browser on exit
kill child process if needed
clear session data from UI
surface extraction errors

Never leave invisible browser processes behind.

Scraping systems should be designed for drift:

parser version
source version
last sample captured
health status
user-visible error
manual retry

The question is not whether a parser will break. It is whether the product can show that it broke.

9. Expense Manager Example

Money apps need deterministic imports before AI suggestions.

Correct pipeline:

CSV/bank export
-> parse preview
-> normalize date/amount/currency
-> detect duplicate
-> user confirms
-> create import batch
-> optional category suggestions

AI can suggest categories later:

merchant: "Metro Cafe"
suggested category: Food
confidence: medium

But AI should not replace import correctness:

amount
date
currency
account
duplicate status

These are structured facts. They should be parsed and validated with deterministic code.

Failure examples:

FailureCorrect behavior
invalid daterow stays in preview error list
duplicate transactionshow duplicate candidate
unknown currencyask user or reject row
changed CSV columnsmapping screen appears
partial import failurebatch status shows failed rows

The design goal:

the user can explain every transaction's source

10. Job Scraper Example

A job scraper is not just a fetch loop. It is:

source registry
query configuration
fetch policy
parser
dedupe
status tracking
review UI

Useful listing fields:

source
query
listing id or URL
title
company
location
date seen
status
parse version
last parse error
dedupe key

Health dashboard:

FieldMeaning
last successful runfreshness
last failed runreliability
listings foundoutput volume
parse errorssource drift
duplicates skippeddedupe behavior
next scheduled runscheduling visibility

Silent failure is bad UX. The user should not mistake a broken parser for an empty job market.

11. Price Tracker Example

A price tracker is a scheduler plus parser plus alert system.

flowchart LR
  A[Tracked item] --> B[Schedule check]
  B --> C[Fetch page or API]
  C --> D[Parse price]
  D --> E[Store history]
  E --> F{Threshold crossed?}
  F -- yes --> G[Notify user]
  F -- no --> H[Update last checked]

UI fields:

last checked
next check
last error
current price
target price
history chart
notification status
parser/source health

No hidden infinite loops.

No stray browser/helper processes after exit.

No fake precision:

unknown price
parser failed
source blocked
network unavailable

These are different states.

12. CVE Checker Example

A CVE checker needs source freshness and match explainability.

Fields to expose:

FieldWhy
advisory sourcetrust and provenance
last database updatestaleness
package normalizationmatch quality
version parsercorrectness
match reasonexplain result
unknown stateavoid false safety

Security tools should say “unknown” when evidence is insufficient.

Bad:

No CVEs found. Safe.

Better:

No matching advisories in the local database.
Database age: 9 days.
2 package names could not be normalized.

Absence of a match is not proof of safety.

13. Kanban Example

A local Kanban app can use simple JSON or SQLite, but it needs state rules.

Questions to define:

QuestionProduct decision
where is board stored?show location in settings
when does autosave happen?visible autosave state
what if file changes externally?reload/conflict prompt
is there undo?bounded undo stack
are backups created?visible backup cleanup

Simple storage is fine if recovery behavior is explicit.

Bad:

autosave failed silently

Better:

Autosave failed: permission denied.
Board has unsaved changes.
Save As...

For small productivity tools, data loss is the worst bug.

14. Dashboards

Dashboards are polling systems.

Bad:

poll everything every second

Better:

LaneExamples
fastcheap local process checks
mediumnormal APIs
slowexpensive or rate-limited APIs
manualdestructive or costly actions

Every widget needs state:

loading
fresh
stale
error
empty
permission needed
rate limited

A blank dashboard is not useful. A stale/error dashboard is actionable.

Polling is a budget:

network budget
CPU budget
API quota budget
attention budget

Show freshness:

updated 12s ago
stale for 18m
last error: timeout

15. Tray Tools

Tray tools look small but run for long sessions.

They need:

single-instance behavior
clear running/stopped state
visible logs
settings window
quit action
clean shutdown

If a tray app starts a server:

menu shows server status
menu has stop server action
quit stops server
startup does not silently expose port

Small does not mean careless.

Tray app hazards:

orphan process
hidden port
stale lock file
config path unknown
logs hidden
autostart hard to disable

The tray menu should answer:

what is running?
where is config?
where are logs?
how do I stop it?

16. Browser-Backed Desktop Tools

Some desktop apps embed a browser view or automate a browser profile.

This adds local state:

cookies
cache
local storage
browser profile
download directory
session state

The UI should expose this because clearing browser data can log the user out or remove session state.

Good storage panel:

database size
cache size
browser profile size
logs size
clear cache
clear browser profile
clear local messages
export settings

Browser-backed tools need clearer boundaries:

native local database
embedded web session
hydrated native UI state
user-visible exports

If web content is used only as a data source, do not pretend the native UI owns the remote service state.

17. PyQt vs egui

PyQt model/view is useful when the app has:

native widgets
large tables
dialogs
menus
OS integration
signals/slots

egui-style immediate mode is useful when:

state is compact
rendering is rebuilt from state every frame
you want explicit state structs
layout is tightly controlled

Different style, same rule:

do not do heavy work during rendering

In PyQt:

do not block data()
do not block paintEvent()
do not block selection handlers

In egui:

do not block update()
do not recompute expensive tables every frame
do not run network/file work in the frame loop

Immediate mode does not remove runtime concerns. It just makes state ownership more explicit.

18. Local State And Paths

Desktop apps create:

config files
SQLite databases
JSON settings
logs
cache files
browser profiles
downloaded indexes
exports
screenshots
model files
temporary files

Paths should not be hardcoded.

Use platform conventions:

config directory
data directory
cache directory
state/log directory

The UI should expose:

where data is stored
how large it is
what it is for
what happens if deleted

This is not cosmetic. It is operational support.

If users cannot find the data, they cannot back it up, delete it, or debug it.

19. Generated Data Cleanup

Generated data should be separated by risk.

Examples:

ActionRisk
clear thumbnail cachelow, regenerate later
clear logsremoves diagnostics
clear browser profilelogs user out
clear downloaded indexfeature stale/unavailable until refresh
delete import batchremoves user records
delete exportsdeletes user-created files

Do not call all of these “clean up.”

They are different actions.

Good cleanup UI:

category
size
path
purpose
delete consequence
action button
last modified

Generated files should be deletable from the GUI when the app created them.

Primary user data needs stronger confirmation than caches.

20. Settings

Settings should be editable from the app when they affect app behavior.

Good settings surfaces:

runtime paths
worker limits
poll intervals
parser source settings
notification preferences
theme/display options
privacy/storage options
import/export controls
startup tutorial reset

Bad pattern:

edit hidden config file by hand
restart and hope it worked

Settings should validate input before saving:

invalid interval rejected
missing executable shown as warning
unwritable path rejected
conflicting options explained

21. Logging

Desktop apps need logs because failures happen outside request/response boundaries.

Log boundaries:

startup
settings load/save
import start/finish/failure
parser failure
scheduler run
worker cancellation
browser session start/stop
database migration
shutdown

Logs should not contain secrets by default.

Examples to redact:

API tokens
cookies
auth headers
full bank transaction exports
private chat content
local-only sensitive paths when exported

The app should expose logs:

open log file
copy diagnostic summary
delete logs
export support bundle with redaction

22. Shutdown

Shutdown should be explicit.

stop timers
cancel workers
wait briefly
close databases
flush logs
stop browser/session processes
stop local servers
save settings
remove lock files

If shutdown cannot stop immediately, show why:

Stopping background import...
Closing browser session...
Flushing database...

The user should not close the app and later find stray processes.

Shutdown must handle:

close during import
close during scrape
close during scheduled poll
close during database write
close while browser is open
close while local server is running

The minimum rule:

no hidden worker or child process survives normal exit

23. Implementation Blueprint

A simple desktop runtime shape:

UI:
  views, actions, dialogs, status

model/store:
  local records, settings, generated data metadata

workers:
  imports, scans, parsing, network, cleanup

scheduler:
  next run, last run, retry state

parser:
  versioned extraction logic, health report

storage governance:
  size, location, delete/rebuild actions

Keep this direct. Do not create wrapper layers that only forward calls.

Better small design:

main window
table model
storage functions
worker jobs
parser functions
scheduler state

Worse design:

Controller -> Service -> Manager -> Provider -> Client

If a layer adds no behavior, remove it.

24. What To Measure

Desktop app quality is measurable.

Measure:

startup time
time to first usable UI
import time
filter latency
scroll responsiveness
database size
cache/log size
worker cancellation time
shutdown time
parser success rate
last scheduler lag

For table UI:

rows visible
rows total
filter time
paint jank
memory usage

For scrapers:

fetch duration
parse duration
records accepted
records rejected
duplicate count
last error

For tray/server tools:

server uptime
active port
last request
shutdown success
child process count

The goal is not enterprise observability. The goal is enough visibility to debug local failures.

25. What Was Done And Why

Worker jobs are used because desktop UIs freeze when imports, parsing, scraping, database work, and browser automation run on the main thread.

Virtualized model/view tables are used because large datasets should not become thousands of widgets. The UI should render visible rows and keep expensive work out of data() and paint paths.

Import previews and batch IDs are used because local records need provenance. If a CSV or chat import goes wrong, the user should be able to inspect, rollback, or delete the exact batch.

Parser health is used because scrapers and importers drift. An empty result is not enough information; the product must distinguish “nothing found” from “parser failed.”

Scheduler visibility is used because polling without state becomes hidden behavior. The user should know last run, next run, last error, and whether data is stale.

Generated-storage cleanup is used because local apps create databases, caches, logs, browser profiles, exports, and temp files. If the app creates it, the app should expose it.

Clean shutdown is used because local desktop tools often start workers, timers, browser sessions, or servers. Closing the window should not leave the runtime alive.

26. What Was Used, Removed, Or Deferred

Used:

worker threads/processes
virtualized tables
cheap model data contract
debounced filtering
import previews
batch ids and rollback
parser health
scheduler visibility
local SQLite/JSON state
browser-session lifecycle controls
generated-data cleanup
tray lifecycle controls
visible settings and logs
clean shutdown

Removed or avoided:

widget-per-row tables
main-thread imports
main-thread scraping
silent parser failures
hidden infinite polling loops
hardcoded paths
undeletable caches
orphan browser/server processes
wrapper layers that only forward calls

Deferred:

distributed sync
enterprise policy systems
generic app frameworks
shared abstractions across unrelated tools
cloud observability stack

The core lesson is that desktop apps fail at the runtime edges. Tables must stay virtualized, imports must be auditable, parsers must show health, schedulers must be honest, generated files must be visible, and shutdown must leave nothing behind.

Comments