[WIP] Add Option for Desktop App Deployment#5
Draft
marbindrakon wants to merge 59 commits into
Draft
Conversation
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>
…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).
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.