Skip to content

[WIP] Add Option for Desktop App Deployment#5

Draft
marbindrakon wants to merge 59 commits into
mainfrom
feat/desktop-installer-poc
Draft

[WIP] Add Option for Desktop App Deployment#5
marbindrakon wants to merge 59 commits into
mainfrom
feat/desktop-installer-poc

Conversation

@marbindrakon

Copy link
Copy Markdown
Owner

No description provided.

marbindrakon and others added 29 commits April 27, 2026 15:00
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Create the empty desktop package and test scaffolding for the
Windows PyInstaller bundle. Includes shared fixture for mocking
platformdirs.user_data_dir in desktop-related tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Single point of platform divergence for path resolution. All other desktop
modules call into this module rather than reading platform-specific
environment variables directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…persistent SECRET_KEY

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ntinel

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement DesktopAutoLoginMiddleware to auto-attach the desktop user from
desktop_user.json to every request in no-auth mode. Caches the user lookup
once per process to avoid repeated DB queries. Gracefully falls through to
AnonymousUser if the pointer file is missing or references a deleted user.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r no-auth default

Implements ensure_initial_user(auth_mode) with two modes:
- "disabled": creates no-auth 'desktop' user (regular, not superuser) and writes
  desktop_user.json for DesktopAutoLoginMiddleware
- "required": consumes bootstrap.json (username + password from installer), creates
  superuser with those credentials, deletes bootstrap.json

Idempotent: re-running is safe. If target user already exists, leaves it alone and
only consumes the bootstrap file if present. set_unusable_password() prevents direct
login as 'desktop' user; auto-login middleware is the only entry point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement desktop/instance.py for single-instance guard using lock file
containing process PID and timestamp. Acquire returns opaque handle on
success or None if another live instance holds the lock. PID-liveness
check is portable (Windows OpenProcess, POSIX kill signal 0) and stale
locks from crashed processes are automatically cleaned up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement backup_database(), rotate_backups(), and backup_and_rotate()
using sqlite3.Connection.backup() to handle SQLite WAL correctly.
Supports backup rotation with keep parameter for automated migration
recovery before upgrades.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the launcher quits (via tray or Task Manager), daemon threads running
logbook/oil-analysis imports are killed. ImportJob rows left in 'running'
state become orphaned and are necessarily failed by the time the next
launcher starts. Mark them as failed with an explanatory event so the user
sees a sensible status in the UI rather than a permanent spinner.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement desktop/launcher.py with a two-layer split: startup_sequence()
handles all orchestration from logging setup through server-ready (testable
on Linux with stubbed waitress/pystray), while main() wires the real
callables and runs the pystray tray loop. Add a sqlite3 touch before
migrate to ensure paths.db_path() exists in test environments where Django
is pre-configured against a different database.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add PowerShell build script to orchestrate venv setup, dependency installation, static file collection, PyInstaller bundling, and Inno Setup packaging. Update settings_desktop.py to honor STATIC_ROOT_OVERRIDE env var for build-time asset collection into writable directory, with fallback to frozen runtime path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds comprehensive README covering build prerequisites, step-by-step
build script execution, smoke-test checklist (build & startup, installer
behavior, reinstall scenarios, robustness, SmartScreen), and manual
recovery procedures (password reset, database corruption, factory reset).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Gate static() media route on SAM_DESKTOP to prevent latent auth bypass
  if DEBUG=True is ever toggled in desktop mode
- Add TODO note explaining the test-only db.sqlite3 pre-touch
- Defensively validate username/password length in bootstrap.py
- Replace placeholder AppId with a valid GUID in installer.iss

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Require explicit desktop auth configuration before enabling no-auth auto-login, preventing missing config.ini from failing open.

Also avoid pre-settings Django model imports, make instance lock acquisition atomic, and escape bootstrap credentials in installer JSON.

Co-Authored-By: OpenAI GPT-5.5 <noreply@openai.com>
health is a pure API app with no templates directory. The spec was
unconditionally adding it to datas, which broke builds on a clean clone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pip writes informational notices to stderr; PowerShell's
$ErrorActionPreference = "Stop" treats any native-command stderr as a
terminating error. Switch to python -m pip (avoids Windows file-lock on
the pip exe during self-upgrade) and redirect stderr to stdout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hecks

$ErrorActionPreference = "Stop" misinterprets native-command stderr
(pip notices, PyInstaller progress) as fatal NativeCommandErrors. Keep
Stop for PowerShell cmdlet errors but add Assert-LastExit() checks after
each native command, and merge stderr into stdout with 2>&1 so output
remains visible without triggering false failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes validated against Windows Server 2022 via SSH:
- Invoke-Native helper sets ErrorActionPreference=Continue locally so
  pip/PyInstaller/iscc stderr progress doesn't trigger NativeCommandError
- Output path corrected: installer lands in desktop\Output\, not .\Output\
- (Earlier commits: health\templates optional in spec, python -m pip for
  self-upgrade, spec conditional include)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Installs Python 3.12, Git for Windows (latest from GitHub API), and
Inno Setup 6; adds them to the system PATH; creates C:\build; adds
Defender exclusions to prevent file-locking during Inno Setup runs.
Safe to re-run — every step checks before acting.

Validated on Windows Server 2022 (idempotent re-run) and a fresh
Windows 11 VM (full install). Fix: renamed $Args parameter to
$Switches in Install-Silently to avoid shadowing PowerShell's
automatic $args variable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
base settings.py defaults PROMETHEUS_METRICS_ENABLED=True and mutates
INSTALLED_APPS before settings_desktop.py can override the flag, so
setting PROMETHEUS_METRICS_ENABLED=False there had no effect. Explicitly
filter django_prometheus out of INSTALLED_APPS and MIDDLEWARE after the
wildcard import. The package is not bundled by PyInstaller and Prometheus
scraping is meaningless on a loopback desktop server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CompressedManifestStaticFilesStorage requires a staticfiles.json manifest
to resolve hashed filenames at render time. Without it every {% static %}
tag silently fails and pages load unstyled. For a loopback desktop server
there is no CDN or aggressive HTTP caching, so content-hashing provides no
benefit. Switch to plain StaticFilesStorage so WhiteNoise can serve files
directly by path with no manifest required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@marbindrakon marbindrakon marked this pull request as draft April 28, 2026 02:59
marbindrakon and others added 14 commits April 27, 2026 22:29
…ffline use

Pinned versions: PatternFly 5.3.1, Chart.js 4.4.9, Alpine.js 3.14.9.
Picked up automatically by collectstatic via core/static/vendor/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ENDOR_ASSETS is set

CDN links remain the default; local files served when SAM_USE_VENDOR_ASSETS=True
(desktop mode or air-gapped web deployment).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…M_USE_VENDOR_ASSETS is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Download all 18 font files (RedHatDisplay, RedHatText, RedHatMono,
  Font Awesome solid, pf-v5-pficon) into vendor/assets/ so PatternFly's
  relative font paths resolve correctly when served locally
- Remove LicenseFile from installer.iss (public domain, no acceptance needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes build/sam-windows (PyInstaller cache), dist/SimpleAircraftManager,
and desktop/Output before each build so installer size reflects only the
current build rather than accumulated artifacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure rename in startup_sequence(). Second-instance branch no longer
opens a browser at the running port; run() (next task) surfaces the
'already running' message via show_message instead.
run() is the testable seam wrapping startup_sequence + start_ui +
shutdown. This commit covers the 'already running' second-instance
message. start_ui invocation and error handling come in follow-up tasks.
run() now owns the UI lifecycle: startup_sequence brings the server up,
run() drives start_ui and routes UI failures through show_message
pointing at the Microsoft WebView2 download URL. Shutdown runs in
finally so the lock and server are always released.
main() is now thin wiring around run(); _run_pywebview_window opens a
single Edge WebView2 window pointed at the local server. Closing the
window causes webview.start() to return, run() shuts down, process
exits. webbrowser and pystray imports removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r spec

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Inno Setup probes the WebView2 registry key (HKLM 32/64-bit and HKCU)
and aborts with a download-page prompt if absent. We do not bundle or
redistribute the runtime — Microsoft's distribution license requires
end-user EULA flow-down, which the project intentionally lacks.
Attribution for pywebview (BSD-3), pythonnet (MIT), clr-loader (MIT),
plus a note that the Microsoft Edge WebView2 Runtime is the user's to
install (we do not redistribute it).
Comment thread tests/desktop/test_launcher_run.py Dismissed
marbindrakon and others added 11 commits April 28, 2026 01:43
DesktopAutoLoginMiddleware now calls auth_login() (added in 331908f to
fix CSRF in no-auth mode), which writes to request.session. The
RequestFactory-built requests in these tests never had a session
attached, so the auth_login path raised AttributeError. Attach a real
SessionStore in a helper. The cache-behavior test now exercises
_get_user() directly so it doesn't drag in session writes and
last_login UPDATE query noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tests under tests/desktop/ import desktop.paths and desktop.config, which
require platformdirs and keyring. Those live only in requirements-desktop.txt,
so PR CI failed at collection with ModuleNotFoundError. pywebview/waitress
install cleanly on Linux and are only lazy-imported inside launcher functions
that the tests stub.
Adds a macOS .app + DMG build alongside the existing Windows installer
and replaces the Inno Setup wizard pages with a cross-platform in-app
setup form served at /desktop/setup/ on first launch (config.ini absent).
The desktop module is now a self-contained Django app registered only by
settings_desktop, so core/health and base/prod settings have zero
references to it.

- desktop/apps.py + desktop/urls.py: the desktop package is a SAM plugin
  whose url_prefix property reads SAM_DESKTOP at urlconf-load time, so
  the existing plugin-discovery loop mounts /desktop/* in desktop mode
  and skips it everywhere else.
- desktop/setup_view.py + templates + JS: single-page setup form (auth
  mode, username/password, optional Anthropic key) writes config.ini,
  creates the initial superuser or no-auth desktop user, stores the API
  key in the OS keyring, then 404s on subsequent loads.
- desktop/middleware.py: DesktopSetupRedirectMiddleware redirects every
  request to /desktop/setup/ while config.ini is missing.
- desktop/ui_messages.py: cross-platform fatal-error dialog (Windows
  MessageBoxW / macOS osascript / stderr fallback).
- desktop/launcher.py: setup-mode branch skips config.load_into_env and
  bootstrap.ensure_initial_user when config.ini is absent.
- desktop/bootstrap.py + desktop/config.py: drop the bootstrap.json
  consumption and api_key_seed.txt migration paths — the setup view
  writes these directly now.
- desktop/installer.iss: stripped to WebView2 preflight + file copy +
  shortcuts; the wizard pages are gone.
- desktop/sam-macos.spec + desktop/build-macos.sh: PyInstaller spec
  produces dist/Simple Aircraft Manager.app (cocoa platform, pyobjc
  hidden imports, ATS local-networking exception, bundle id
  app.simpleaircraftmanager.desktop). Build script collects static,
  runs PyInstaller, then create-dmg if available.
- requirements-desktop.txt: pyobjc-* deps gated by sys_platform=darwin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the in-app first-run setup form so the user can configure any
combination of three logbook-import AI providers — Anthropic API, local
Ollama, and an OpenAI-compatible endpoint (vLLM, OpenRouter, LiteLLM
proxy) — and pick which is the default in the import-page model
selector. Existing Anthropic-only setups keep working unchanged.

Plumbing:
- settings.py: lift LOGBOOK_IMPORT_EXTRA_MODELS / LOGBOOK_IMPORT_DEFAULT_MODEL
  env-var hooks out of settings_prod.py into the shared dev module so
  settings_desktop.py inherits them via `from .settings import *`.
- desktop/config.py: new KEYRING_USERNAME_LITELLM constant; load_into_env()
  reads an optional [ai] section (default_provider, ollama_*, litellm_*)
  and seeds LOGBOOK_IMPORT_EXTRA_MODELS, LOGBOOK_IMPORT_DEFAULT_MODEL,
  OLLAMA_BASE_URL, LITELLM_BASE_URL, LITELLM_API_KEY. Back-compat: a
  single configured provider with no default_provider key still routes
  correctly.
- desktop/setup_view.py: form reads litellm_model/base_url/api_key and
  default_provider; validation enforces that default_provider points at a
  configured provider when more than one is set, requires litellm_base_url
  whenever a litellm model is given, and rejects shell-metachar model
  names. LiteLLM API key goes to the OS keyring under a separate username,
  never into config.ini. Anthropic api_key, litellm_api_key, password, and
  confirm_password are scrubbed from the values dict re-rendered after a
  validation error.
- desktop_setup.html: AI fieldset reorganised into three labelled
  subsections plus a Default model radio group; cross-platform GPU framing
  in the explainer (Metal on Apple Silicon, CUDA/ROCm on supported PCs).
- desktop-setup.js: gates each provider's follow-up inputs on whether the
  parent provider is filled in, enables/disables the matching default
  radio reactively, falls back to the first remaining enabled radio when
  the current selection becomes invalid, auto-pins when only one provider
  is configured.

Docs (docs/user-guide/logbook-import.rst): new "Local & Custom AI
Providers (Desktop App)" section covers Ollama install, the OpenAI-
compatible endpoint flow (vLLM / OpenRouter / LiteLLM proxy), how to
flip the default later, per-platform config.ini paths, and where API
keys live in the OS credential store.

Tests: 21 new desktop tests cover LiteLLM validation, multi-provider
config.ini writing, keyring isolation (key never lands in config.ini),
default_provider routing across all three providers, and back-compat
for a single-provider [ai] section without default_provider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
health/logbook_import.py reads its prompt + JSON-schema files via
Path(__file__).parent / 'ai_prompts' at runtime. PyInstaller's static
analysis only follows imports, so the directory was never copied into
the bundle, and an actual import attempt inside the installed app failed
with FileNotFoundError pointing at
Contents/Frameworks/health/ai_prompts/logbook_extract_system_prompt.txt
(macOS) and the equivalent _internal/ path on Windows.

Add the directory to the datas list in both sam-macos.spec and
sam-windows.spec so it lands at health/ai_prompts/ inside the bundle,
exactly where the runtime lookup expects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds desktop/flatpak/ with manifest, launcher shim, .desktop entry,
AppStream metainfo, and build helper that produces a single .flatpak
bundle (matching the .dmg / .exe distribution model). No application
code changes — paths.py, settings_desktop.py, and launcher.py already
work on Linux via platformdirs, keyring SecretService, and pywebview's
WebKitGTK backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PyMuPDF is dual-licensed AGPL-3.0 / Artifex Commercial. Bundling it in the
desktop distribution would trigger AGPL's combined-work obligations across
the macOS, Windows, and Linux Flatpak builds. pypdfium2 (Apache-2.0 / BSD-3,
Google PDFium bindings) keeps the desktop bundle permissive end-to-end.

The two extractor functions (_extract_text, _get_words) are rewritten against
pypdfium2's char-level API. _get_words does a two-pass extraction: trust
whitespace as the only word delimiter (avoids splitting decimals like "95.3"
when the dot has a different ascender), then cluster records into rows by
baseline so words on the same visual line share a y value (the parser's
_make_by_y bucketing depends on that).

Verified against four real lab PDFs (Blackstone × 2, AVLab × 2): byte-
identical parser output vs PyMuPDF baseline. Performance within 6%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds four anonymized lab-report PDFs (Blackstone × 2, AVLab × 2) plus
matching golden JSON, exercising the multi-sample, single-sample, single-
section, and multi-section parser paths. test_golden_fixture parses each
PDF and asserts the output equals the checked-in JSON, guarding against
drift in pypdfium2 word extraction or lab-specific parser logic.

Anonymization is performed by scripts/anonymize_oil_pdfs.py, which loads
its replacement specs from test-pdfs/anonymize_config.py. The test-pdfs/
directory is gitignored so private originals and the PII-bearing config
never enter version control. Verified PII-clean via pypdfium2 text scan,
decompressed-stream byte scan, and raw-byte scan against an exhaustive
needle list (names, addresses, tail/lab/serial/client IDs, phones, email).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 1 (drop AGPL via PyMuPDF -> pypdfium2 swap) is done; Phases 2-6
(notices generation, UI surfacing, OFL packaging, AppStream metadata,
CI guardrail) are queued.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…factor

Phases completed:
- Phase 2: scripts/generate_third_party_notices.py + notices_extras.txt +
  requirements-build.txt; build scripts updated (macOS, Windows, Flatpak)
- Phase 3: /about/ page (AboutView, about.html, navbar link, URL, tests)
  with inline third-party notices table and link to generator script
- Phase 4: OFL-1.1 license files next to vendored fonts (RedHatDisplay,
  RedHatText, RedHatMono, webfonts); MIT license next to pficon
- Phase 5: Flatpak metainfo project_license updated to full SPDX expression;
  README license section updated with notices pointer
- Phase 6: CI license gate (pip-licenses --fail-on GPL/LGPL/AGPL/SSPL/…)

Refactor: compact THIRD-PARTY-NOTICES.txt + per-package licenses/ dir
- generate_third_party_notices.py rewritten with two modes:
  default → compact attribution table (name/version/license/author/URL)
  --save-licenses <dir> → one <pkg>-<ver>.txt + NOTICE-<pkg>-<ver>.txt per package
- THIRD-PARTY-NOTICES.txt and licenses/ git-ignored; generated at build time
- PyInstaller specs (macOS + Windows) bundle licenses/ dir
- Flatpak manifest generates licenses/ and copies it into app prefix
- THIRD-PARTY-NOTICES.txt removed from git tracking (git rm --cached)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the missing .map files alongside their minified counterparts so
browser devtools can resolve original sources when debugging offline.

- chart.umd.js.map  — Chart.js 4.4.9 (75 source files)
- patternfly.min.css.map — PatternFly 5.3.1 (1 source file)

Alpine.js 3.14.9 CDN bundle ships no source map; nothing to add there.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@marbindrakon marbindrakon changed the title [WIP] Add Option for Windows Desktop Deployment [WIP] Add Option for Desktop App Deployment May 1, 2026
marbindrakon and others added 3 commits May 2, 2026 13:04
Drop the legacy Overpass fonts.css include from both base templates and remove the stylesheet, since PatternFly 5 already provides its own font-face declarations and vendored assets include the matching local font files.

Add template coverage to keep vendor-assets mode on local app-shell assets and prevent the stale remote font stylesheet from returning.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Move PyInstaller and its packaging helpers out of requirements-desktop.txt into a pinned requirements-desktop-build.txt so runtime installs only carry desktop runtime dependencies.

Keep Flatpak build-only notice tooling outside /app, generate runtime notices before PyInstaller is installed, and add regression coverage for the dependency split.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants