@@ -79,7 +173,7 @@ Click the **AI** tab on the right edge of the window to slide the panel in from
### Scenario modeling
-Add transfers or expenses to model their impact on the forecast without modifying your real data. Modeling transations can be one-time or recurring, and may be removed at any time.
+Add transfers or expenses to model their impact on the forecast without modifying your real data. Modeling transactions can be one-time or recurring, and may be removed at any time.
@@ -87,7 +181,7 @@ Add transfers or expenses to model their impact on the forecast without modifyin
### Dark mode
-Switch between light and dark mode using the icon in the header. Avoid safety squints, crow's feet, and retinal burn-in.
+Switch between light and dark mode using the icon in the header.
@@ -95,7 +189,7 @@ Switch between light and dark mode using the icon in the header. Avoid safety sq
### Settings — first run
-On first launch the user just needs to double-click one file in the folder and the app opens in the default browser, first with a startup splash page and then to the Settings page with the Monarch Connection section highlighted. (If no Python installation is detected the startup screen will provide instructions on how to install before continuing.
+On first launch the app opens a startup splash page and then takes you directly to the Settings page with the Monarch Connection section highlighted. If no Python installation is detected the startup screen provides instructions for installing it before continuing.
@@ -119,85 +213,6 @@ Configure your AI provider, model, and API key here. The **Last generated** line
---
-## What you'll need before starting
-
-- A **Mac** (macOS 13+) or **Linux** computer — Windows not supported in this version
-- A **[Monarch Money](https://www.monarchmoney.com/)** account
-- **Python 3.11 or later** — **Linux only** (Mac users get Python bundled inside the app)
- - Linux: `sudo apt install python3 python3-venv` (Debian/Ubuntu) or `sudo dnf install python3` (Fedora)
-
-**Optional — for AI insights:**
-An API key from one of these providers (pick one). See [Setting up AI Insights](#setting-up-ai-insights) below for step-by-step instructions.
-- [Anthropic (Claude)](https://console.anthropic.com/) — recommended
-- [OpenAI (GPT)](https://platform.openai.com/)
-- [Google (Gemini)](https://aistudio.google.com/)
-
----
-
-## Getting started
-
-### Step 1 — Download the app
-
-**On Mac:** Go to the [Releases page](../../releases/latest) and download **`Butterfly.Effect-mac.dmg`**. Open the `.dmg`, drag the app to your Applications folder, and double-click to launch.
-
-> The first time you open it, macOS may warn you it's from an unidentified developer. Right-click the app → **Open** → **Open** to proceed. You only need to do this once.
-
-**On Linux:** Go to the [Releases page](../../releases/latest) and download **`butterfly-effect-linux-x86_64.AppImage`**. Make it executable and run it:
-```bash
-chmod +x butterfly-effect-*-linux-x86_64.AppImage
-./butterfly-effect-*-linux-x86_64.AppImage
-```
-Or clone the repo and run `./run.sh` directly (requires Python 3.11+).
-
-### Step 2 — Launch the App
-
-**Mac:** Python is bundled inside the app — no installation needed. Just double-click **Butterfly Effect** in your Applications folder. A startup page opens in your browser and redirects to the app once it's ready.
-
-**Linux:** On first run, `run.sh` automatically sets up a Python virtual environment and installs all required packages. This takes a couple of minutes. The startup page will warn you if Python is missing or too old.
-
-### Step 3 — Connect to Monarch
-
-Once setup is complete the app will take you directly to the **Settings** page. The **Monarch Connection** section (highlighted with a blue border) is the only required step:
-
-1. Click **Connect to Monarch** — a Chrome browser window will open automatically. Log in to Monarch when prompted; the window closes on its own after a successful login (~30–60 seconds).
-2. Wait a few moments for your Monarch accounts to populate, then select your **primary bill pay account** from the dropdown. Click **Save Monarch Settings** to complete the setup.
-
-Once these two steps are complete, the **Go to Dashboard →** button at the top of the page will be enabled. Click it to pull transactions and open your forecast — this may take 30 seconds or so on first run.
-
-> Down further on the Settings page are **AI Insights** and **Forecast Settings**. These are both optional on first run and can always be customized later.
-
-### Step 4 — View & customize your first forecast
-
-The first time you open the dashboard the app fetches your transaction history from Monarch. Your forecast chart will appear at the top of the left pane when it's done. After the first run, your data stays cached so the dashboard loads instantly on future visits.
-
----
-
-## Day-to-day use
-
-**Starting the app:** Double-click **`Start Balance Forecast.command`** (Mac) or run `./run.sh` (Linux). A startup page opens in your browser and redirects automatically once the server is ready. You can also bookmark `http://localhost:5002`.
-
-**Refresh Forecast** — click the button in the upper right whenever you want updated transaction data from Monarch. Takes 1–2 minutes.
-
-**Run AI Analysis** — generates fresh AI insights. Run this once a day or whenever you want an updated analysis. Requires an AI API key in Settings → AI Insights.
-
-**AI Insights drawer** — click the **AI** tab on the right edge of the window to slide the panel in over the transaction schedule. Click the **×** inside, or click the tab again, to close it.
-
-**Resize columns** — drag the vertical handle between the two panes to adjust how much width each side gets.
-
-**Settings** — click the butterfly icon (⚙) in the top-right to open Settings. Key options:
-
-| Setting | Description |
-|---|---|
-| Primary Account | Select the account used for bill payments — refresh the list if you don't see it |
-| AI Insights | Enable AI Insights (off by default); set your preferred provider (Anthropic, OpenAI, or Google), choose your preferred model and add your API key; set the default number of transaction months to retrieve for analysis; customize how long before the analysis is labeled as stale. You can also kick off an AI Analysis right from Settings and see it run in a status window |
-| Forecast Horizon | How many days to project forward (default: 45) |
-| Buffer Threshold | Get a warning when your balance drops below this dollar amount |
-| User Context & AI Corrections | Hand-edit your AI corrections in free text (also stored in `user_context.md`) |
-| Calendar Integration | Overlay custom calendar transaction events onto the forecast chart with Google Calendar, iCloud, or a custom ICS feed |
-| App Settings | Change the default port, turn on debug mode, or reset to factory defaults |
-
----
-
## Setting up AI Insights
AI Insights is optional but adds meaningful value — seasonal spending patterns, predicted upcoming expenses, and transfer recommendations tailored to your actual history. You need an API key from one provider. The feature is off by default; you enable it in **Settings → AI Insights**.
@@ -256,7 +271,7 @@ Google AI Studio keys include a generous free tier. Usage beyond the free tier i
- **Billing Day Overrides** — correct the day-of-month for any recurring payment whose billing date Monarch has learned incorrectly
- **Scenario Modeling** — temporarily model one-time transfers or expenses to see how they affect your balance forecast
- **Corrections & Context** — feed the AI specific facts about your finances to improve its accuracy
-- **Startup page** — animated loading screen with helpful error messages if Python is missing or too old (Linux only — Mac bundles Python)
+- **Startup page** — animated loading screen with live progress updates and helpful error messages if Python is missing or too old
- **Dark mode** — toggle in Settings or with the moon icon in the header
---
@@ -265,7 +280,9 @@ Google AI Studio keys include a generous free tier. Usage beyond the free tier i
**"Setup needed" error on the dashboard** — click **→ Open Settings** in the error box and complete the Monarch Connection section (connect to Monarch and select your primary account).
-**Startup page shows a Python error (Linux)** — follow the on-screen instructions to install Python 3.11 or later: `sudo apt install python3 python3-venv` (Debian/Ubuntu) or `sudo dnf install python3` (Fedora). Mac users don't see this — Python is bundled inside the app.
+**Startup page shows a Python error** — follow the on-screen instructions to install Python 3.11 or later:
+- **Linux:** `sudo apt install python3 python3-venv` (Debian/Ubuntu) or `sudo dnf install python3` (Fedora)
+- **Windows:** Download from [python.org/downloads](https://python.org/downloads) and check "Add Python to PATH" during install, then open a **new** Command Prompt window and run `"Start Butterfly Effect - Windows.cmd"` again.
**Forecast is slow** — this is normal on first run. The app opens a browser, logs into Monarch, and fetches months of transaction history. Subsequent loads use a cached session and are much faster.
@@ -273,9 +290,9 @@ Google AI Studio keys include a generous free tier. Usage beyond the free tier i
**Account list looks incomplete after connecting** — click **Refresh Accounts** in Settings → Monarch Connection.
-**"API key is not configured"** — go to Settings → AI Insights, select your AI provider, and paste in your key. See [Setting up AI Insights](#setting-up-ai-insights) for step-by-step instructions on getting a key from each provider.
+**"API key is not configured"** — go to Settings → AI Insights, select your AI provider, and paste in your key. See [Setting up AI Insights](#setting-up-ai-insights) for step-by-step instructions.
-**Browser doesn't open automatically (Linux)** — navigate to `http://localhost:5002` in your browser manually.
+**Browser doesn't open automatically** — navigate to `http://localhost:5002` in your browser manually.
**The app was working and suddenly stopped** — Monarch occasionally updates their web app, which can break the data-fetching layer. Check the [project page on GitHub](https://github.com/vendaface/butterfly-effect) for updates.
@@ -299,6 +316,7 @@ All runtime data is stored in your user data directory — never in a cloud serv
- **Mac:** `~/Library/Application Support/Butterfly Effect/`
- **Linux:** `~/.local/share/butterfly-effect/`
+- **Windows:** `%APPDATA%\Butterfly Effect\` (typically `C:\Users\[yourname]\AppData\Roaming\Butterfly Effect\`)
| File | What it contains | Leaves your device? |
|---|---|---|
@@ -313,7 +331,7 @@ All runtime data is stored in your user data directory — never in a cloud serv
| `monarch_raw_cache.json` | Cached Monarch transaction data | **Never** |
| `user_context.md` | Corrections you feed to the AI | Only if you configure an AI provider |
-All sensitive files are written with owner-only permissions (`chmod 600`) so other users on the same machine cannot read them.
+All sensitive files are written with owner-only permissions so other users on the same machine cannot read them.
### Security
@@ -335,10 +353,11 @@ Your AI API key is stored only in `.env` on your device. It is sent only to your
## Power user reference
-### Running from Terminal
+### Running from source
+**Mac / Linux:**
```bash
-# Start (foreground — closes when you close the Terminal window)
+# Start (foreground — stops when you close the terminal)
./run.sh
# Start as a background daemon
@@ -351,11 +370,25 @@ Your AI API key is stored only in `.env` on your device. It is sent only to your
./server.sh logs # tail the server log
```
+**Windows:**
+```batch
+rem Start (foreground — from Command Prompt or double-click)
+"Start Butterfly Effect - Windows.cmd"
+
+rem Background daemon (from PowerShell)
+.\server.ps1 start
+.\server.ps1 stop
+.\server.ps1 restart
+.\server.ps1 status
+.\server.ps1 logs
+```
+
### File overview
All runtime files live in your user data directory:
- **Mac:** `~/Library/Application Support/Butterfly Effect/`
- **Linux:** `~/.local/share/butterfly-effect/`
+- **Windows:** `%APPDATA%\Butterfly Effect\`
| File | Purpose |
|---|---|
@@ -373,10 +406,16 @@ All runtime files live in your user data directory:
### Resetting to a clean state
+**Mac / Linux:**
```bash
./reset-for-testing.sh
```
+**Windows:**
+```batch
+reset.cmd
+```
+
Kills the running server, deletes all cached and generated files, and removes the virtual environment. You will be asked to confirm before anything is deleted.
---
@@ -385,7 +424,6 @@ Kills the running server, deletes all cached and generated files, and removes th
- No official Monarch API exists — this tool intercepts the same network calls the Monarch web app makes. Changes to Monarch's web app may break it without warning.
- Designed for desktop use; mobile layout is not optimized.
-- Windows is not supported in this version.
---
diff --git a/Start Balance Forecast.command b/Start Butterfly Effect - Mac.command
similarity index 100%
rename from Start Balance Forecast.command
rename to Start Butterfly Effect - Mac.command
diff --git a/Start Butterfly Effect - Windows.cmd b/Start Butterfly Effect - Windows.cmd
new file mode 100644
index 0000000..480be76
--- /dev/null
+++ b/Start Butterfly Effect - Windows.cmd
@@ -0,0 +1,9 @@
+@echo off
+:: Butterfly Effect -- Windows launcher
+:: Double-click this file or run it from cmd.exe to start the app.
+::
+:: chcp 65001 sets the console to UTF-8 (code page 65001) so that Unicode
+:: characters written by PowerShell and subprocesses (block chars, ellipsis,
+:: box-drawing lines, etc.) render correctly instead of showing as Gua garbage.
+chcp 65001 > nul
+powershell.exe -ExecutionPolicy Bypass -NoProfile -File "%~dp0run.ps1" %*
diff --git a/VERSION b/VERSION
index 8e258dd..5763dd2 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.9.4-beta
+0.9.5-beta
diff --git a/ai_advisor.py b/ai_advisor.py
index ed743ee..e8a09ea 100644
--- a/ai_advisor.py
+++ b/ai_advisor.py
@@ -404,7 +404,7 @@ def _build_user_prompt(
# Load user corrections — injected early so they take priority
user_context_section = []
if _USER_CONTEXT_FILE.exists():
- ctx = _USER_CONTEXT_FILE.read_text().strip()
+ ctx = _USER_CONTEXT_FILE.read_text(encoding='utf-8').strip()
if ctx:
user_context_section = [
"",
@@ -510,7 +510,7 @@ def get_ai_insights(
print("Run: python ai_daily.py")
sys.exit(1)
- insights = json.loads(INSIGHTS_FILE.read_text())
+ insights = json.loads(INSIGHTS_FILE.read_text(encoding='utf-8'))
generated = insights.get("generated_at", "unknown")
print(f"\n{'='*60}")
print(f"AI Insights (generated: {generated})")
@@ -537,13 +537,13 @@ def get_ai_insights(
print(f"\nSurplus alerts:")
for s in surpluses:
print(f" {s.get('period', '?')}: ${s.get('estimated_surplus', 0):,.2f}")
- print(f" → {s.get('suggested_allocation', '')}")
+ print(f" -> {s.get('suggested_allocation', '')}")
flags = insights.get("risk_flags", [])
if flags:
print(f"\nRisk flags:")
for f in flags:
- print(f" ⚠ {f}")
+ print(f" [!] {f}")
notes = insights.get("seasonal_notes", "")
if notes:
diff --git a/ai_daily.py b/ai_daily.py
index ea044cf..489bf77 100644
--- a/ai_daily.py
+++ b/ai_daily.py
@@ -38,7 +38,7 @@
def _load_config() -> dict:
if not _CONFIG_PATH.exists():
raise RuntimeError("config.yaml not found.")
- return yaml.safe_load(_CONFIG_PATH.read_text())
+ return yaml.safe_load(_CONFIG_PATH.read_text(encoding='utf-8'))
def run(dry_run: bool = False) -> None:
@@ -127,8 +127,8 @@ def run(dry_run: bool = False) -> None:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
- _INSIGHTS_FILE.write_text(json.dumps(insights, indent=2, default=str))
- print(f"[{datetime.now().strftime('%H:%M:%S')}] ✓ Insights written to {_INSIGHTS_FILE.name}")
+ _INSIGHTS_FILE.write_text(json.dumps(insights, indent=2, default=str), encoding='utf-8')
+ print(f"[{datetime.now().strftime('%H:%M:%S')}] [OK] Insights written to {_INSIGHTS_FILE.name}")
# Print summary
print(f"\nNarrative: {insights.get('narrative', '')[:200]}")
@@ -142,7 +142,7 @@ def run(dry_run: bool = False) -> None:
if flags:
print(f"Risk flags: {len(flags)}")
for f in flags:
- print(f" ⚠ {f}")
+ print(f" [!] {f}")
if __name__ == "__main__":
diff --git a/butterfly-effect.spec b/butterfly-effect.spec
index 61a7c5a..5642e2f 100644
--- a/butterfly-effect.spec
+++ b/butterfly-effect.spec
@@ -43,7 +43,10 @@ a = Analysis(
hiddenimports=[
*playwright_hidden,
*webview_hidden,
- 'webview.platforms.cocoa', # macOS platform — not auto-detected
+ # Platform-specific pywebview backend — not auto-detected by PyInstaller
+ 'webview.platforms.cocoa' if sys.platform == 'darwin' else
+ 'webview.platforms.edgechromium' if sys.platform == 'win32' else
+ 'webview.platforms.gtk',
# AI providers
'anthropic',
'openai',
@@ -86,7 +89,8 @@ exe = EXE(
target_arch=None,
codesign_identity=None,
entitlements_file=None,
- icon=str(SRC / 'static' / 'icon.icns') if sys.platform == 'darwin' else None,
+ icon=(str(SRC / 'static' / 'icon.icns') if sys.platform == 'darwin' else
+ str(SRC / 'static' / 'icon.ico') if sys.platform == 'win32' else None),
)
coll = COLLECT(
diff --git a/calendar_client.py b/calendar_client.py
index 3133941..42cff41 100644
--- a/calendar_client.py
+++ b/calendar_client.py
@@ -31,7 +31,7 @@ def _load_ics_url() -> str:
config_path = Path(__file__).parent / "config.yaml"
if not config_path.exists():
raise RuntimeError("config.yaml not found.")
- config = yaml.safe_load(config_path.read_text())
+ config = yaml.safe_load(config_path.read_text(encoding='utf-8'))
url = (config.get("calendar", {}).get("ics_url", "") or "").strip()
if not url or "PASTE_YOUR" in url:
raise RuntimeError(
@@ -177,7 +177,7 @@ def get_events(horizon_days: int = 45) -> list[dict]:
if __name__ == "__main__":
config_path = Path(__file__).parent / "config.yaml"
- config = yaml.safe_load(config_path.read_text()) if config_path.exists() else {}
+ config = yaml.safe_load(config_path.read_text(encoding='utf-8')) if config_path.exists() else {}
horizon = config.get("forecast", {}).get("horizon_days", 45)
try:
diff --git a/config.py b/config.py
index e89a818..89113be 100644
--- a/config.py
+++ b/config.py
@@ -57,7 +57,7 @@ def _load_config() -> dict:
raise RuntimeError(
"config.yaml not found. Copy config.yaml.example and fill in values."
)
- return yaml.safe_load(_CONFIG_PATH.read_text())
+ return yaml.safe_load(_CONFIG_PATH.read_text(encoding='utf-8'))
def _save_config(config: dict) -> None:
@@ -100,7 +100,7 @@ def _env_key_status(key: str) -> str:
return "configured"
# Fallback: read .env file directly
if _ENV_PATH.exists():
- for line in _ENV_PATH.read_text().splitlines():
+ for line in _ENV_PATH.read_text(encoding='utf-8').splitlines():
if line.startswith(f"{key}="):
val = line[len(f"{key}="):].strip().strip('"').strip("'")
if val:
@@ -111,7 +111,7 @@ def _env_key_status(key: str) -> str:
def _update_env_key(key: str, value: str) -> None:
"""Update or add key=value in .env; also update os.environ in-memory."""
- lines = _ENV_PATH.read_text().splitlines() if _ENV_PATH.exists() else []
+ lines = _ENV_PATH.read_text(encoding='utf-8').splitlines() if _ENV_PATH.exists() else []
found = False
for i, line in enumerate(lines):
if line.startswith(f"{key}="):
@@ -130,7 +130,7 @@ def _delete_env_key(key: str) -> None:
Used for privacy opt-out — leaves no empty key line behind.
"""
if _ENV_PATH.exists():
- lines = [l for l in _ENV_PATH.read_text().splitlines()
+ lines = [l for l in _ENV_PATH.read_text(encoding='utf-8').splitlines()
if not l.startswith(f"{key}=")]
_atomic_write(_ENV_PATH, "\n".join(lines) + ("\n" if lines else ""))
os.environ.pop(key, None)
@@ -142,7 +142,7 @@ def _read_env_value(key: str) -> str:
if val:
return val
if _ENV_PATH.exists():
- for line in _ENV_PATH.read_text().splitlines():
+ for line in _ENV_PATH.read_text(encoding='utf-8').splitlines():
if line.startswith(f"{key}="):
return line[len(f"{key}="):].strip().strip('"').strip("'")
return ""
diff --git a/forecast_builder.py b/forecast_builder.py
index 83bd16a..714512a 100644
--- a/forecast_builder.py
+++ b/forecast_builder.py
@@ -182,7 +182,7 @@ def _get_forecast_data(config: dict) -> dict:
if config.get("demo_mode"):
_demo = _BASE_DIR / "demo" / "forecast_data.json"
if _demo.exists():
- data = json.loads(_demo.read_text())
+ data = json.loads(_demo.read_text(encoding='utf-8'))
_cache["data"] = data
_cache["ts"] = time.time()
return data
@@ -352,7 +352,10 @@ def _matches_override(desc: str) -> bool:
_fetched_dt = datetime.now()
else:
_fetched_dt = datetime.now()
- result["refreshed_at"] = _fetched_dt.strftime("%A %b %-d, %Y at %-I:%M %p")
+ # %-d and %-I are POSIX-only (Linux/macOS); use datetime attributes for cross-platform
+ # zero-stripping so this works on Windows too.
+ _hour = _fetched_dt.strftime("%I:%M %p").lstrip("0") # "01:30 PM" → "1:30 PM"
+ result["refreshed_at"] = f"{_fetched_dt.strftime('%A %b')} {_fetched_dt.day}, {_fetched_dt.year} at {_hour}"
result["horizon_days"] = horizon
result["has_ai_predictions"] = bool(predicted_events)
diff --git a/main.py b/main.py
index ed30e5a..9fffbe0 100644
--- a/main.py
+++ b/main.py
@@ -234,10 +234,17 @@ def main():
import socket as _socket
with _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM) as _s:
if _s.connect_ex(('127.0.0.1', port)) == 0:
+ if sys.platform == 'win32':
+ _kill_hint = (
+ f'netstat -ano | findstr :{port} '
+ f'# note the PID, then: taskkill /PID brew install python@3.12'
+ 'Option 2 \u2014 Direct download
' + 'Visit python.org/downloads and install Python 3.12 or newer.
'; + if (os === 'windows') return '' + + 'Butterfly Effect needs Python 3.11+.
' + + 'Option 1 \u2014 Direct download
' + + 'Visit python.org/downloads and install Python 3.12 or newer.
' + + '\u2139\ufe0f During installation, check \u201cAdd Python to PATH\u201d.
' + + 'Option 2 \u2014 winget (Windows 11 / updated Windows 10)
' + + 'winget install Python.Python.3.12';
return ''
+ 'Butterfly Effect needs Python 3.11+.
' + 'Ubuntu / Debian:
' @@ -307,6 +314,9 @@Python is installed but the virtual environment could not be created.
' + + 'Ensure Python 3.12+ was installed using the standard installer from python.org/downloads, then close this window and run "Start Butterfly Effect - Windows.cmd" again.
Python is installed but the venv module is missing. On Ubuntu, Debian, and Linux Mint it is a separate package:
sudo apt install python3-venv'
@@ -316,6 +326,11 @@ pip could not install the required packages. This usually means a corrupted Python environment.
' + + 'Try resetting the virtual environment:
' + + 'rmdir /s /q .venv\n"Start Butterfly Effect - Windows.cmd"'
+ + 'If the problem persists, check your internet connection and try again.
'; return '' + 'pip could not install the required packages. This usually means a corrupted Python environment.
' + 'Try resetting the virtual environment:
' diff --git a/storage.py b/storage.py index fcc96f2..1d9c19a 100644 --- a/storage.py +++ b/storage.py @@ -79,23 +79,23 @@ def _check_list_schema( Logs a warning (to stdout) for each dropped record — never raises. """ if not isinstance(data, list): - print(f"[schema] {path.name}: expected a list, got {type(data).__name__} — ignoring file") + print(f"[schema] {path.name}: expected a list, got {type(data).__name__} - ignoring file") return [] good = [] for i, item in enumerate(data): if not isinstance(item, dict): - print(f"[schema] {path.name}[{i}]: expected dict, got {type(item).__name__} — skipping") + print(f"[schema] {path.name}[{i}]: expected dict, got {type(item).__name__} - skipping") continue bad = False for k in required_str_keys: if not isinstance(item.get(k), str) or not item[k].strip(): - print(f"[schema] {path.name}[{i}]: missing/invalid string field '{k}' — skipping") + print(f"[schema] {path.name}[{i}]: missing/invalid string field '{k}' - skipping") bad = True break if not bad: for k in required_num_keys: if not isinstance(item.get(k), (int, float)): - print(f"[schema] {path.name}[{i}]: missing/invalid numeric field '{k}' — skipping") + print(f"[schema] {path.name}[{i}]: missing/invalid numeric field '{k}' - skipping") bad = True break if not bad: @@ -114,7 +114,7 @@ def _parse_corrections() -> list[dict]: """ if not _USER_CONTEXT_FILE.exists(): return [] - lines = _USER_CONTEXT_FILE.read_text().splitlines() + lines = _USER_CONTEXT_FILE.read_text(encoding='utf-8').splitlines() results: list[dict] = [] cur_type = "Correction" # tracks section for old-format migration for i, raw in enumerate(lines): @@ -153,7 +153,7 @@ def _load_scenarios() -> list[dict]: if not _SCENARIOS_FILE.exists(): return [] try: - data = json.loads(_SCENARIOS_FILE.read_text()) + data = json.loads(_SCENARIOS_FILE.read_text(encoding='utf-8')) return _check_list_schema( _SCENARIOS_FILE, data, required_str_keys=("date", "description"), @@ -168,17 +168,17 @@ def _load_payment_overrides() -> dict: if not _PAYMENT_OVERRIDES_FILE.exists(): return {} try: - data = json.loads(_PAYMENT_OVERRIDES_FILE.read_text()) + data = json.loads(_PAYMENT_OVERRIDES_FILE.read_text(encoding='utf-8')) if not isinstance(data, dict): - print(f"[schema] {_PAYMENT_OVERRIDES_FILE.name}: expected a dict — ignoring file") + print(f"[schema] {_PAYMENT_OVERRIDES_FILE.name}: expected a dict - ignoring file") return {} good = {} for k, v in data.items(): if not isinstance(v, dict): - print(f"[schema] {_PAYMENT_OVERRIDES_FILE.name}[{k!r}]: expected dict value — skipping") + print(f"[schema] {_PAYMENT_OVERRIDES_FILE.name}[{k!r}]: expected dict value - skipping") continue if not isinstance(v.get("name"), str) or not isinstance(v.get("amount"), (int, float)): - print(f"[schema] {_PAYMENT_OVERRIDES_FILE.name}[{k!r}]: missing name/amount — skipping") + print(f"[schema] {_PAYMENT_OVERRIDES_FILE.name}[{k!r}]: missing name/amount - skipping") continue good[k] = v return good @@ -191,7 +191,7 @@ def _load_payment_skips() -> list: if not _PAYMENT_SKIPS_FILE.exists(): return [] try: - data = json.loads(_PAYMENT_SKIPS_FILE.read_text()) + data = json.loads(_PAYMENT_SKIPS_FILE.read_text(encoding='utf-8')) return _check_list_schema( _PAYMENT_SKIPS_FILE, data, required_str_keys=("name", "month"), @@ -205,7 +205,7 @@ def _load_payment_monthly_amounts() -> list: if not _PAYMENT_MONTHLY_AMOUNTS_FILE.exists(): return [] try: - data = json.loads(_PAYMENT_MONTHLY_AMOUNTS_FILE.read_text()) + data = json.loads(_PAYMENT_MONTHLY_AMOUNTS_FILE.read_text(encoding='utf-8')) return _check_list_schema( _PAYMENT_MONTHLY_AMOUNTS_FILE, data, required_str_keys=("name",), @@ -220,7 +220,7 @@ def _load_insights() -> dict | None: if not _INSIGHTS_FILE.exists(): return None try: - return json.loads(_INSIGHTS_FILE.read_text()) + return json.loads(_INSIGHTS_FILE.read_text(encoding='utf-8')) except Exception: return None @@ -230,7 +230,7 @@ def _load_dismissed_suggestions() -> list: if not _DISMISSED_SUGGESTIONS_FILE.exists(): return [] try: - data = json.loads(_DISMISSED_SUGGESTIONS_FILE.read_text()) + data = json.loads(_DISMISSED_SUGGESTIONS_FILE.read_text(encoding='utf-8')) return data if isinstance(data, list) else [] except Exception: return [] @@ -246,7 +246,7 @@ def _load_payment_day_overrides() -> dict: if not _PAYMENT_DAY_OVERRIDES_FILE.exists(): return {} try: - data = json.loads(_PAYMENT_DAY_OVERRIDES_FILE.read_text()) + data = json.loads(_PAYMENT_DAY_OVERRIDES_FILE.read_text(encoding='utf-8')) if not isinstance(data, dict): return {} good = {} @@ -275,7 +275,7 @@ def _load_monarch_raw_cache() -> dict | None: if not _MONARCH_RAW_CACHE_FILE.exists(): return None try: - data = json.loads(_MONARCH_RAW_CACHE_FILE.read_text()) + data = json.loads(_MONARCH_RAW_CACHE_FILE.read_text(encoding='utf-8')) if not isinstance(data, dict): return None required = ("fetched_at", "balance", "transactions", "recurring") diff --git a/templates/settings.html b/templates/settings.html index 6019fe0..0f13224 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -1440,9 +1440,7 @@