Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-14 11:43:20 -06:00
parent ae787ca060
commit 4c1c6f31d5
5 changed files with 169 additions and 39 deletions

View File

@ -10,12 +10,13 @@ Deliver one installable macOS app that:
- launches an embedded backend executable from app resources - launches an embedded backend executable from app resources
- opens the web UI only inside `WKWebView` - opens the web UI only inside `WKWebView`
- persists settings locally and supports native-to-web sync - persists settings locally and supports native-to-web sync
- provides native app-level help/onboarding (not only in the web UI)
- can be packaged as a DMG - can be packaged as a DMG
## Layout Standard ## Layout Standard
Use this layout for new projects: Use this layout for new projects:
- `web/` for web backend source (`app.py`, modules, requirements) - `web/src/` for web backend source (`app.py`, modules, requirements)
- `mac/<AppName>Mac/` for Xcode project and SwiftUI shell - `mac/src/` for Xcode project and SwiftUI shell sources
- `scripts/` for build and packaging automation - `scripts/` for build and packaging automation
- `docs/` for architecture and onboarding docs - `docs/` for architecture and onboarding docs
@ -34,35 +35,52 @@ Detailed naming guidance: `references/layout-and-naming.md`
3. Build backend into a single executable. 3. Build backend into a single executable.
- Add `scripts/build_embedded_backend.sh` that compiles backend into a one-file executable and copies it into the mac target resource folder. - Add `scripts/build_embedded_backend.sh` that compiles backend into a one-file executable and copies it into the mac target resource folder.
- Use generic discovery:
- discover `*.xcodeproj` under `mac/src`
- discover scheme via `xcodebuild -list -json` (fallback to project name)
- discover/create `EmbeddedBackend` folder under selected project sources
- Include all required web files/modules in build inputs. - Include all required web files/modules in build inputs.
- Default backend name should be stable (for example `WebBackend`) and configurable via env.
4. Implement SwiftUI host shell. 4. Implement SwiftUI host shell.
- Use `@Observable` host state (no `ObservableObject`/Combine). - Use `@Observable` host state (no `ObservableObject`/Combine).
- Start/stop backend process, detect available local port, and handle retries. - Start/stop backend process, detect available local port, and handle retries.
- Render URL in `WKWebView` only. - Render URL in `WKWebView` only.
- Keep app responsive when backend fails and surface actionable status. - Keep app responsive when backend fails and surface actionable status.
- Resolve backend executable by checking both current and legacy names during migration.
- Add a native toolbar `Help` button that opens bundled help content in-app.
5. Define settings sync contract. 5. Define settings sync contract.
- Use shared settings file (for example `~/.<app>/settings.json`) as source of truth. - Use shared settings file (for example `~/.<app>/settings.json`) as source of truth.
- Normalize settings both in web app and native app. - Normalize settings both in web app and native app.
- Pass effective settings via URL query params on launch/reload/restart. - Pass effective settings via URL query params on launch/reload/restart.
- Keep onboarding limited to starter fields; preserve advanced fields. - Keep onboarding limited to starter fields; preserve advanced fields.
- Add web-only fallback access to help/onboarding (for users not using the mac wrapper).
6. Automate packaging. 6. Automate packaging.
- Add `scripts/build_selfcontained_mac_app.sh` to build embedded backend then Xcode app. - Add `scripts/build_selfcontained_mac_app.sh` to build embedded backend then Xcode app.
- Add `scripts/create_installer_dmg.sh` for distributable DMG. - Add `scripts/create_installer_dmg.sh` for distributable DMG.
- Use generic app bundle discovery for DMG defaults where possible.
- Standardize installer artifacts under `build/dmg/` (not repo root).
- Ensure `.gitignore` excludes generated artifacts (`build/`, `*.dmg`, temp `rw.*.dmg`).
7. Validate. 7. Validate.
- Python syntax: `python -m py_compile` on web entrypoint. - Python syntax: `python -m py_compile` on web entrypoint.
- Tests: `PYTHONPATH=web/src pytest -q web/src/tests`.
- Xcode build: `xcodebuild ... build`. - Xcode build: `xcodebuild ... build`.
- Runtime check: no external browser opens, webview loads locally, settings persist across relaunch. - Runtime check: no external browser opens, webview loads locally, settings persist across relaunch.
- Verify help works in both modes:
- native toolbar help popup in mac app
- sidebar help fallback in web-only mode
## Required Deliverables ## Required Deliverables
- Embedded backend build script - Embedded backend build script
- macOS app host that launches backend + WKWebView - macOS app host that launches backend + WKWebView
- Shared settings sync path - Shared settings sync path
- Native help/onboarding popup + web fallback help entry
- README section for local build + DMG workflow - README section for local build + DMG workflow
- Verified build commands and final artifact locations - Verified build commands and final artifact locations
- `.gitignore` updated for build/installer outputs
## References ## References
- Layout and migration rules: `references/layout-and-naming.md` - Layout and migration rules: `references/layout-and-naming.md`

View File

@ -1,4 +1,4 @@
interface: interface:
display_name: "macOS Self-Contained Web App" display_name: "macOS Self-Contained Web App"
short_description: "Embed web backend in local Mac app." short_description: "Embed a local web backend in a self-contained macOS WKWebView app."
default_prompt: "Create or refactor this project into a self-contained macOS app wrapper for a local web backend running inside WKWebView." default_prompt: "Create or refactor this project into a self-contained macOS app wrapper with web/src + mac/src layout, embedded backend binary, native Help popup, DMG output in build/dmg, and proper .gitignore rules."

View File

@ -7,30 +7,41 @@
set -euo pipefail set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WEB_SRC_DIR="$ROOT_DIR/web/src"
MAC_SRC_DIR="${MAC_SRC_DIR:-$ROOT_DIR/mac/src}"
PYTHON_BIN="$ROOT_DIR/.venv/bin/python" PYTHON_BIN="$ROOT_DIR/.venv/bin/python"
APP_NAME="TraderBackend" BACKEND_BIN_NAME="${BACKEND_BIN_NAME:-WebBackend}"
BUILD_ROOT="$ROOT_DIR/dist-backend-build" BUILD_ROOT="$ROOT_DIR/dist-backend-build"
DIST_PATH="$BUILD_ROOT/dist" DIST_PATH="$BUILD_ROOT/dist"
WORK_PATH="$BUILD_ROOT/build" WORK_PATH="$BUILD_ROOT/build"
SPEC_PATH="$BUILD_ROOT/spec" SPEC_PATH="$BUILD_ROOT/spec"
TARGET_DIR="$ROOT_DIR/mac/TraderMac/TraderMac/EmbeddedBackend" PROJECT_PATH="${MAC_PROJECT_PATH:-}"
SCHEME="${MAC_SCHEME:-}"
TARGET_DIR="${EMBEDDED_BACKEND_DIR:-}"
if [[ -z "$PROJECT_PATH" ]]; then
PROJECT_PATH="$(find "$MAC_SRC_DIR" -maxdepth 4 -name "*.xcodeproj" | sort | head -n 1)"
fi
PROJECT_DIR="$(dirname "$PROJECT_PATH")"
if [[ -z "$TARGET_DIR" ]]; then
TARGET_DIR="$(find "$PROJECT_DIR" -maxdepth 4 -type d -name EmbeddedBackend | head -n 1)"
fi
mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" "$TARGET_DIR" mkdir -p "$DIST_PATH" "$WORK_PATH" "$SPEC_PATH" "$TARGET_DIR"
"$PYTHON_BIN" -m pip install -q pyinstaller "$PYTHON_BIN" -m pip install -q pyinstaller
"$PYTHON_BIN" -m PyInstaller \ "$PYTHON_BIN" -m PyInstaller \
--noconfirm --clean --onefile \ --noconfirm --clean --onefile \
--name "$APP_NAME" \ --name "$BACKEND_BIN_NAME" \
--distpath "$DIST_PATH" \ --distpath "$DIST_PATH" \
--workpath "$WORK_PATH" \ --workpath "$WORK_PATH" \
--specpath "$SPEC_PATH" \ --specpath "$SPEC_PATH" \
--add-data "$ROOT_DIR/web/app.py:." \ --add-data "$WEB_SRC_DIR/app.py:." \
--add-data "$ROOT_DIR/web/<module_dir>:<module_dir>" \ --add-data "$WEB_SRC_DIR/<module_dir>:<module_dir>" \
"$ROOT_DIR/web/backend_embedded_launcher.py" "$WEB_SRC_DIR/backend_embedded_launcher.py"
cp "$DIST_PATH/$APP_NAME" "$TARGET_DIR/$APP_NAME" cp "$DIST_PATH/$BACKEND_BIN_NAME" "$TARGET_DIR/$BACKEND_BIN_NAME"
chmod +x "$TARGET_DIR/$APP_NAME" chmod +x "$TARGET_DIR/$BACKEND_BIN_NAME"
``` ```
## SwiftUI Host Requirements ## SwiftUI Host Requirements
@ -40,6 +51,7 @@ chmod +x "$TARGET_DIR/$APP_NAME"
- Render backend URL in `WKWebView`. - Render backend URL in `WKWebView`.
- Retry provisional load failure after short delay. - Retry provisional load failure after short delay.
- Keep debug controls behind `#if DEBUG`. - Keep debug controls behind `#if DEBUG`.
- Add toolbar Help action that opens a bundled local `help.html` in a sheet/webview.
## Settings Sync Contract ## Settings Sync Contract
- Shared path: `~/.<app>/settings.json` - Shared path: `~/.<app>/settings.json`
@ -47,6 +59,7 @@ chmod +x "$TARGET_DIR/$APP_NAME"
- Load shared file before app launch. - Load shared file before app launch.
- Push normalized fields as query params when launching/reloading webview. - Push normalized fields as query params when launching/reloading webview.
- Persist setup-sheet changes back to shared file. - Persist setup-sheet changes back to shared file.
- Keep a legacy fallback path for migration when needed.
## Packaging Commands ## Packaging Commands
- Build app: - Build app:
@ -61,9 +74,26 @@ chmod +x "$TARGET_DIR/$APP_NAME"
APP_BUNDLE_PATH="dist-mac/<timestamp>/<AppName>Mac.app" ./scripts/create_installer_dmg.sh APP_BUNDLE_PATH="dist-mac/<timestamp>/<AppName>Mac.app" ./scripts/create_installer_dmg.sh
``` ```
Expected DMG output:
```text
build/dmg/<AppName>-<timestamp>.dmg
```
## Git Ignore Baseline
```gitignore
build/
*.dmg
rw.*.dmg
```
## Verification Checklist ## Verification Checklist
- No external browser window opens. - No external browser window opens.
- App starts with embedded backend from app resources. - App starts with embedded backend from app resources.
- `WKWebView` loads local URL. - `WKWebView` loads local URL.
- Settings persist across relaunch and remain in sync. - Settings persist across relaunch and remain in sync.
- Native Help popup renders bundled content.
- Web-only run has sidebar help fallback.
- DMG installs and runs on a second machine. - DMG installs and runs on a second machine.
- DMG is produced under `build/dmg/` and repo root stays clean.

View File

@ -5,44 +5,68 @@
```text ```text
<repo-root>/ <repo-root>/
web/ web/
run.sh
src/
app.py app.py
<web modules> <web modules>
requirements.txt requirements.txt
ONBOARDING.md
mac/ mac/
<AppName>Mac/ src/
<AppName>Mac.xcodeproj/ <Project>.xcodeproj/
<AppName>Mac/ App/
ContentView.swift ContentView.swift
<AppName>MacApp.swift <AppMain>.swift
EmbeddedBackend/ EmbeddedBackend/
<AppName>Backend WebBackend
Help/
help.html
AppTests/
AppUITests/
scripts/ scripts/
build_embedded_backend.sh build_embedded_backend.sh
build_selfcontained_mac_app.sh build_selfcontained_mac_app.sh
create_installer_dmg.sh create_installer_dmg.sh
build/
dmg/
<AppName>-<timestamp>.dmg
docs/ docs/
architecture.md architecture.md
onboarding.md
``` ```
## Naming Rules ## Naming Rules
- Repo: lowercase hyphenated (`trader-desktop-shell`). - Repo: lowercase hyphenated (`trader-desktop-shell`).
- macOS app target/project: PascalCase with `Mac` suffix (`TraderMac`). - macOS target/scheme: PascalCase (`TraderMac` or project-defined), but source folders should stay generic (`App`, `AppTests`, `AppUITests`).
- Embedded backend binary: PascalCase + `Backend` (`TraderBackend`). - Embedded backend binary: stable generic default (`WebBackend`) with env override support.
- Settings directory: lowercase snake or kebab (`~/.trader_app`). - Settings directory: lowercase snake or kebab (`~/.trader_app`).
- Environment port var: uppercase snake (`TRADER_PORT`). - Environment port var: uppercase snake (keep legacy fallbacks during migration).
## Script Discovery Rules
- Build scripts should discover the first `*.xcodeproj` under `mac/src` unless `MAC_PROJECT_PATH` is provided.
- Build scripts should discover scheme via `xcodebuild -list -json` unless `MAC_SCHEME` is provided.
- Embedded backend target dir should be derived from selected project sources or `EMBEDDED_BACKEND_DIR`.
- Python tests should run with `PYTHONPATH=web/src`.
- DMG scripts should write to `build/dmg/` and clean temporary staging folders.
## Ignore Rules
Add these to root `.gitignore`:
- `build/`
- `*.dmg`
- `rw.*.dmg`
## Migration Rules For Existing Projects ## Migration Rules For Existing Projects
1. Do not rename everything in one commit. 1. Do not rename everything in one commit.
2. First, add new folders and compatibility references. 2. First, add new folders and compatibility references.
3. Move scripts and docs next. 3. Move scripts and docs next.
4. Move web source only after build scripts are updated. 4. Move web source only after build scripts are updated.
5. Rename Xcode project/target last, with a dedicated verification commit. 5. Move mac source into `mac/src/App*` before optional target/project renames.
6. Add compatibility lookup for old backend binary names and old settings paths during transition.
## Commit Strategy ## Commit Strategy
1. `chore(layout): add canonical folders` 1. `chore(layout): add canonical folders`
2. `build(backend): add embedded binary build` 2. `build(backend): add embedded binary build`
3. `feat(mac-shell): host local backend in webview` 3. `feat(mac-shell): host local backend in webview`
4. `feat(sync): add shared settings contract` 4. `feat(sync): add shared settings contract`
5. `build(packaging): add self-contained app + dmg scripts` 5. `feat(help): add native help popup + web fallback`
6. `chore(rename): finalize naming migration` 6. `build(packaging): add self-contained app + dmg scripts`
7. `chore(rename): finalize naming migration`

View File

@ -1,28 +1,32 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
if [[ $# -lt 2 ]]; then if [[ $# -lt 1 ]]; then
echo "Usage: $0 <repo-root> <app-name-pascal>" echo "Usage: $0 <repo-root> [app-name]"
echo "Example: $0 ~/Code/trader-app Trader" echo "Example: $0 ~/Code/trader-app Trader"
exit 1 exit 1
fi fi
REPO_ROOT="$1" REPO_ROOT="$1"
APP_NAME="$2" APP_NAME="${2:-WebShellApp}"
MAC_APP_NAME="${APP_NAME}Mac"
BACKEND_NAME="${APP_NAME}Backend"
mkdir -p "$REPO_ROOT/web" mkdir -p "$REPO_ROOT/web/src"
mkdir -p "$REPO_ROOT/mac/$MAC_APP_NAME/$MAC_APP_NAME/EmbeddedBackend" mkdir -p "$REPO_ROOT/web/src/web_core"
mkdir -p "$REPO_ROOT/web/src/tests"
mkdir -p "$REPO_ROOT/mac/src/App/EmbeddedBackend"
mkdir -p "$REPO_ROOT/mac/src/App/Help"
mkdir -p "$REPO_ROOT/mac/src/AppTests"
mkdir -p "$REPO_ROOT/mac/src/AppUITests"
mkdir -p "$REPO_ROOT/scripts" mkdir -p "$REPO_ROOT/scripts"
mkdir -p "$REPO_ROOT/docs" mkdir -p "$REPO_ROOT/docs"
cat > "$REPO_ROOT/docs/architecture.md" <<DOC cat > "$REPO_ROOT/docs/architecture.md" <<DOC
# ${APP_NAME} Architecture # ${APP_NAME} Architecture
- web backend source: ./web - web backend source: ./web/src
- mac shell source: ./mac/${MAC_APP_NAME} - mac shell source: ./mac/src
- embedded backend binary: ./mac/${MAC_APP_NAME}/${MAC_APP_NAME}/EmbeddedBackend/${BACKEND_NAME} - embedded backend binary: ./mac/src/App/EmbeddedBackend/WebBackend
- native help page: ./mac/src/App/Help/help.html
DOC DOC
cat > "$REPO_ROOT/scripts/README.build.md" <<DOC cat > "$REPO_ROOT/scripts/README.build.md" <<DOC
@ -32,10 +36,64 @@ Add:
- build_embedded_backend.sh - build_embedded_backend.sh
- build_selfcontained_mac_app.sh - build_selfcontained_mac_app.sh
- create_installer_dmg.sh - create_installer_dmg.sh
Suggested script behaviors:
- Discover \`*.xcodeproj\` under \`mac/src\` unless \`MAC_PROJECT_PATH\` is provided.
- Discover scheme via \`xcodebuild -list -json\` unless \`MAC_SCHEME\` is provided.
- Default backend binary name: \`WebBackend\` (override with \`BACKEND_BIN_NAME\`).
- Write DMG artifacts to \`build/dmg/\`.
DOC DOC
cat > "$REPO_ROOT/web/src/ONBOARDING.md" <<DOC
# ${APP_NAME} Onboarding
Add your web help/onboarding content here.
DOC
cat > "$REPO_ROOT/mac/src/App/Help/help.html" <<DOC
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Help</title>
</head>
<body>
<h1>${APP_NAME} Help</h1>
<p>Replace this with your quick start and onboarding content.</p>
</body>
</html>
DOC
cat > "$REPO_ROOT/web/run.sh" <<'DOC'
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENV_DIR="$ROOT_DIR/.venv"
WEB_SRC_DIR="$ROOT_DIR/web/src"
if [[ ! -d "$VENV_DIR" ]]; then
python3 -m venv "$VENV_DIR"
fi
# shellcheck disable=SC1091
source "$VENV_DIR/bin/activate"
pip install -r "$WEB_SRC_DIR/requirements.txt"
exec streamlit run "$WEB_SRC_DIR/app.py"
DOC
chmod +x "$REPO_ROOT/web/run.sh"
touch "$REPO_ROOT/.gitignore"
for line in "build/" "*.dmg" "rw.*.dmg"; do
if ! grep -Fxq "$line" "$REPO_ROOT/.gitignore"; then
printf "%s\n" "$line" >> "$REPO_ROOT/.gitignore"
fi
done
echo "Created layout for ${APP_NAME}:" echo "Created layout for ${APP_NAME}:"
echo "- $REPO_ROOT/web" echo "- $REPO_ROOT/web/src"
echo "- $REPO_ROOT/mac/$MAC_APP_NAME" echo "- $REPO_ROOT/mac/src"
echo "- $REPO_ROOT/scripts" echo "- $REPO_ROOT/scripts"
echo "- $REPO_ROOT/docs" echo "- $REPO_ROOT/docs"
echo "- $REPO_ROOT/.gitignore (updated with build/DMG ignore rules)"