Coded by Egyan
GhostBackup is a secure automated backup system built with Electron, React, and Python FastAPI. It is designed for environments requiring encrypted backups, long-term retention, auditability, and automated monitoring, such as accounting firms and regulated businesses.
The system performs encrypted backups, supports multi-disk redundancy, verifies file integrity, and enforces compliance-level retention policies.
| Dashboard | Live Run |
|---|---|
![]() |
![]() |
| Restore |
|---|
![]() |
Electron + React Frontend Python FastAPI Backend SQLite Database
| Feature | Description |
|---|---|
| 🔐 Encryption at Rest | AES-256-GCM streaming encryption (constant memory) |
| 🔒 API Security | Auto-generated session API tokens |
| 📜 Compliance Retention | Built-in retention policy enforcement |
| 💾 3-2-1 Backup Strategy | Primary and secondary storage support |
| ⏰ Scheduled Backups | Daily automated backups using configured time + timezone |
| 👁️ Real-Time File Watching | File system monitoring with debounce |
| 🔌 Circuit Breaker | Stops backup if more than 5% of files fail |
| ✅ Integrity Verification | /verify endpoint re-hashes backups |
| 📚 Audit Trail | All configuration changes logged |
Windows — one command setup:
- Install Python 3.10+ and Node.js 18+ if not already installed
- Clone the repository and double-click
install.bat - Follow the prompts — paths, SSD drive, and encryption key are all configured automatically
- Double-click
start.batto launch
Full step-by-step instructions are available in SETUP.md.
┌─────────────────────────────────────────────────────────────┐
│ ELECTRON │
│ • Generates API token (crypto.randomBytes) │
│ • Spawns Python backend process │
└──────────────────────────┬──────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ FASTAPI BACKEND (default port 8765; configurable via │
│ GHOSTBACKUP_API_PORT and used end-to-end by Electron + UI) │
│ │
│ 🔒 Authentication Middleware │
│ Requires X-API-Key header for all endpoints except /health │
│ │
│ ⏰ Scheduler │
│ 👁️ File Watcher │
│ │
│ Backup Engine │
│ ├─ 🔐 Encrypt files (AES-256-GCM streaming) │
│ ├─ 💾 Copy to primary and secondary drives │
│ ├─ ✅ Verify integrity using xxhash │
│ └─ 📚 Log results to SQLite │
└─────────────────────────────────────────────────────────────┘
▲
┌──────────────────────────┴──────────────────────────────────┐
│ REACT FRONTEND │
│ • Dashboard │
│ • Backup history │
│ • Restore interface │
│ • Compliance monitoring │
│ • Exponential backoff polling │
└─────────────────────────────────────────────────────────────┘
All endpoints require the X-API-Key header except /health.
| Method | Endpoint | Description |
|---|---|---|
| GET | /health | Health check (no auth required) |
| GET | /dashboard | Dashboard summary stats |
| GET | /run/status | Active run state |
| POST | /run/start | Start backup |
| POST | /run/stop | Cancel running backup |
| POST | /verify | Verify backup integrity |
| GET | /runs | Backup history |
| GET | /runs/:id | Single run detail |
| GET | /runs/:id/logs | Run log entries |
| POST | /restore | Restore files |
| GET | /config | Current configuration |
| PATCH | /config | Update configuration |
| GET | /config/audit | Configuration audit trail |
| POST | /config/sites | Add backup source folder |
| PATCH | /config/sites/:name | Update source folder settings |
| DELETE | /config/sites/:name | Remove backup source folder |
| GET | /ssd/status | SSD health and disk usage |
| GET | /alerts | In-app alert list |
| POST | /alerts/:id/dismiss | Dismiss alert |
| POST | /alerts/dismiss-all | Dismiss all alerts |
| PATCH | /settings/smtp | Update SMTP settings |
| POST | /settings/smtp/test | Send test email |
| PATCH | /settings/retention | Update retention policy |
| POST | /settings/prune | Run prune job |
| POST | /settings/encryption/generate-key | Generate new encryption key |
| GET | /watcher/status | File watcher status |
| POST | /watcher/start | Start file watcher |
| POST | /watcher/stop | Stop file watcher |
Configuration file location:
backend/config/config.yaml
Copy template from:
backend/config/config.yaml.example
Example configuration:
ssd_path: "D:\\GhostBackup"
secondary_ssd_path: "E:\\GhostBackup2" # optional, leave empty to disable
encryption:
enabled: true # requires GHOSTBACKUP_ENCRYPTION_KEY in .env.local
sources:
- label: "Client Records"
path: "C:\\Users\\admin\\SharePoint\\Red Parrot\\Clients"
enabled: true
retention:
daily_days: 365
weekly_days: 2555
compliance_years: 7
guard_days: 7
schedule:
time: "08:00"
timezone: "Europe/London"
circuit_breaker_threshold: 0.05export GHOSTBACKUP_ENCRYPTION_KEY="your-fernet-key"
export GHOSTBACKUP_SMTP_PASSWORD="your-password"
export GHOSTBACKUP_API_PORT="8765"GHOSTBACKUP_API_TOKEN is automatically generated by Electron.
GHOSTBACKUP_API_PORT is optional and defaults to 8765. Electron passes the active port to the backend and exposes the matching runtime API URL to the renderer, so desktop UI requests follow the configured port end-to-end.
GhostBackup/
│
├── install.bat ← run this first on a new machine
├── start.bat ← created by installer, launches the app
│
├── backend/
│ ├── config/
│ │ ├── config.yaml.example
│ │ └── config.yaml
│ ├── api.py ← FastAPI server (default port 8765)
│ ├── config.py ← ConfigManager
│ ├── manifest.py ← SQLite run/file/audit database
│ ├── reporter.py ← AlertManager + SMTP email
│ ├── scheduler.py ← APScheduler daily job + watchdog
│ ├── setup_helper.py ← called by install.bat
│ ├── syncer.py ← file scan, encrypt, copy, verify, prune
│ ├── utils.py ← shared fmt_bytes / fmt_duration helpers
│ ├── watcher.py ← watchdog real-time file watcher
│ └── tests/
│ ├── conftest.py
│ ├── test_api.py
│ ├── test_config.py
│ ├── test_crypto.py
│ ├── test_manifest.py
│ ├── test_reporter.py
│ ├── test_scheduler_utils.py
│ ├── test_setup_helper.py
│ ├── test_syncer_copy.py
│ ├── test_syncer_restore.py
│ ├── test_syncer_scan.py
│ ├── test_syncer_utils.py
│ ├── test_syncer_verify.py
│ ├── test_watcher.py
│ └── test_utils.py
│
├── electron/
│ ├── main.js ← main process, spawns backend, tray
│ └── preload.js ← contextBridge API surface
│
├── src/
│ ├── GhostBackup.jsx ← app shell + navigation
│ ├── main.jsx ← React entry point + backend poller
│ ├── api-client.js ← authenticated fetch wrapper
│ ├── styles.css ← all app styles
│ ├── splash.css ← splash screen styles
│ ├── components/ ← reusable UI components
│ │ ├── AlertBell.jsx
│ │ ├── Countdown.jsx
│ │ ├── ErrBanner.jsx
│ │ ├── Heatmap.jsx
│ │ ├── LoadingState.jsx
│ │ ├── SsdGauge.jsx
│ │ └── StatusPill.jsx
│ ├── pages/ ← full-page views
│ │ ├── BackupConfig.jsx
│ │ ├── Dashboard.jsx
│ │ ├── LiveRun.jsx
│ │ ├── LogsViewer.jsx
│ │ ├── RestoreUI.jsx
│ │ └── Settings.jsx
│ └── tests/
│ ├── api-client.test.js
│ ├── backup-config.test.jsx
│ ├── components.test.jsx
│ └── setup.js
│
└── SETUP.md
| Layer | Implementation |
|---|---|
| Encryption | AES-256-GCM streaming with version header (key rotation ready) |
| API Authentication | Timing-safe session API tokens (hmac.compare_digest) |
| Path Safety | Path traversal validation on restore endpoint |
| Electron Sandbox | Chromium sandbox enabled, CSP in dev + production |
| Credential Safety | Input sanitization on credential writes |
| Database Safety | SQLite with PRAGMA synchronous=FULL, batched commits |
| Process Safety | Process name verification before termination |
| Data Integrity | xxhash verification |
| Failure Control | Circuit breaker threshold |
GhostBackup supports long-term data retention requirements.
| Policy | Value |
|---|---|
| Daily retention | 365 days |
| Weekly retention | 2555 days |
| Audit trail | Configuration changes logged |
| Integrity check | /verify endpoint |
GhostBackup is suitable for:
• Accounting firms • Legal offices • Financial services • Medical record systems • Businesses requiring secure automated backups
Egyan07
Built for RedParrot Accounting.
MIT License
- Sidebar layout fix (
splash.css,main.jsx): splash screenbodystyles were bleeding into the main app layout, causing the sidebar to expand and collapse unpredictably across pages. Splash styles are now scoped to abody.splash-activeclass that is removed once the app is ready. - Port cleanup on quit (
main.js): closing the app via File → Exit or tray → Quit now kills any process still holding port 8765, preventing "port already in use" errors on next launch. wmicreplaced withtasklist(main.js):wmicis deprecated and unavailable on some Windows 11 configurations. Port conflict detection now usestasklist /FI "PID eq <pid>"which works on all supported Windows versions.
cryptographypinned to 44.0.2:msal(Microsoft SharePoint auth) requirescryptography<45. CVE-2026-26007 and CVE-2026-34073 are acknowledged and suppressed in CI viapip-audit --ignore-vulnuntilmsalrelaxes its constraint.- ESLint v9 flat config migration (
eslint.config.js): migrated from legacy.eslintrctoeslint.config.jsflat config format required by ESLint v9. Added browser and Node globals to fix false-positiveno-undeferrors in CI.
start.batremoved from repo: this file is generated byinstall.batwith machine-specific paths and should never be committed. Added to.gitignore.
- Timing-safe API token comparison (
api.py): replaced plain!=string comparison withhmac.compare_digest()to prevent timing-based side-channel attacks. - Path traversal validation (
api.py,syncer.py): restore endpoint now validates destination path and blocks..path segments. Both API layer and syncer enforce this as defence-in-depth. - Encryption key versioning (
syncer.py): encrypted files now include a version byte (_ENCRYPTION_VERSION = 0x01) in the header, enabling future key rotation without breaking old backups. Critical for 7-year compliance retention. - Electron
shell:open-pathvalidation (main.js): IPC handler now validates the path exists and is a directory before opening, preventing arbitrary file/executable execution from a compromised renderer. - Credential injection prevention (
main.js): values written to.env.localare sanitized — newline, carriage return, double quote, and backslash characters are rejected. - Notification server body limit (
main.js): HTTP notification server on port 8766 now caps request body at 10KB to prevent memory exhaustion from local processes. - Dependencies updated:
cryptography42.0.5 → 46.0.5,fastapi0.111.0 → 0.135.2,pydantic2.7.0 → 2.12.5,uvicorn0.29.0 → 0.34.0,python-multipart0.0.9 → 0.0.22,PyYAML6.0.1 → 6.0.2,xxhash3.4.1 → 3.5.0,electron31.0.0 → 33.3.1. All carry known CVE patches (CVE-2024-12797, CVE-2026-26007, CVE-2024-53981, CVE-2026-24486, CVE-2025-54121, CVE-2025-62727). package-lock.jsonsynced: regenerated to matchpackage.json— fixesnpm cifailures in CI.- CI security auditing (
ci.yml): addedpip-auditandnpm auditjob to catch vulnerable dependencies automatically.
- Race condition eliminated (
api.py):/run/startand/run/stopnow acquire_run_mutexbefore checking or mutating_active_run, preventing duplicate concurrent runs. - SQLite batch commits (
manifest.py): removed per-recordcommit()calls fromrecord_file()andlog(). Addedflush()method called after each library scan — major performance improvement for large backups (thousands of files). - Chromium sandbox enabled (
main.js):sandbox: truein BrowserWindow reduces attack surface. - Production CSP headers (
main.js): Content-Security-Policy now set for both dev and production builds. - Config encapsulation (
config.py,syncer.py): addedencryption_config_enabledproperty — syncer no longer accessesconfig._datadirectly. - SMTP key whitelisting (
config.py):update_smtp()uses an explicit key allowlist instead of only filtering password. - Query parameter bounds (
api.py):limitcapped at 1000,offsetminimum 0 on/runsand/config/auditendpoints. - Mount point matching (
syncer.py): replaced stringstartswith()withPath.is_relative_to()to prevent false matches. - Async desktop notify (
api.py): wrapped blockinghttp.clientcall inasyncio.to_thread()so it no longer blocks the event loop. - LiveRun polling fix (
LiveRun.jsx): replacedsetIntervalwithuseRef-basedsetTimeoutthat adjusts delay without recreating the interval on every status change. - ESLint restored in CI (
ci.yml): lint job now runsnpm run lint. - CI syntax check (
ci.yml): replaced hardcoded file list withpython -m compileall backend/ -q.
- Timezone consistency (
watcher.py):datetime.now()→datetime.now(timezone.utc). - UTC string handling (
LiveRun.jsx): normalizes Python's+00:00suffix toZbefore parsing. - Memory leak fix (
LogsViewer.jsx):URL.revokeObjectURL()called after CSV export. - Dynamic version chip (
GhostBackup.jsx): fetches version from IPC instead of hardcodedv2.0.0. - Duplicate exclusion check (
BackupConfig.jsx): prevents adding duplicate patterns. - Platform-aware restore path (
RestoreUI.jsx): defaults to Linux path on non-Windows. - Error logging (
AlertBell.jsx): emptycatch {}blocks now log viaconsole.warn. - Keyboard accessibility (
GhostBackup.jsx): nav items haverole="button",tabIndex,onKeyDown. - Install safety (
install.bat): won't overwrite existingstart.bat. - Coverage threshold (
ci.yml): raised from 60% to 70%. - Node 22 LTS (
ci.yml): added to frontend test matrix. - SETUP.md clarification: noted Fernet key format vs AES-256-GCM encryption.
- Test dependencies pinned (
requirements.txt):pytest==8.3.4,httpx==0.27.2(were unpinned with>=).
| File | Tests | Coverage |
|---|---|---|
test_syncer_copy.py |
6 | copy_file(): basic, atomic write, checksum, dirs, encryption, secondary SSD |
test_syncer_restore.py |
5 | restore_files(): basic, decryption round-trip, path traversal blocked, dirs, missing file |
test_syncer_verify.py |
3 | verify_backups(): intact, corrupted, missing |
test_watcher.py |
5 | _SourceHandler: debounce, cooldown, UTC timezone, ghosttmp ignored, exclusions |
test_manifest.py |
+1 | flush() batch commit verification |
- Streaming AES-256-GCM encryption (
syncer.py): replaced Fernet's whole-file-in-memory encryption with chunked AES-256-GCM streaming — memory usage is now constant regardless of file size. Legacy Fernet-encrypted backups are auto-detected and decrypted transparently. - Thread-safe
_active_runmutations (api.py): addedthreading.Lockaround all compound operations (+=,.append(), multi-field updates) on the shared run state dict, eliminating data races between the progress callback (executor thread) and the event loop thread.
datetime.utcnow()replaced (manifest.py,api.py,syncer.py): all deprecateddatetime.utcnow()calls replaced withdatetime.now(timezone.utc)for Python 3.12+ compatibility and correct timezone handling.- CI lint job fixed (
ci.yml): removed duplicate flake8 step, restored ESLint for JavaScript linting. - Secondary SSD infinite-recursion guard (
syncer.py): added_skip_secondaryparameter tocopy_file()to prevent recursive secondary copy from triggering another secondary copy. - Notification server authenticated (
main.js,api.py): the HTTP notification server on port 8766 now validates theX-API-Keyheader — only the backend can trigger desktop notifications. ThreadPoolExecutorshutdown (api.py): changed fromshutdown(wait=False)toshutdown(wait=True, cancel_futures=True)so in-flight copies are cancelled promptly on abort.- Version strings aligned (
api.py,package.json): all version references now consistently report2.0.0. - CORS
"null"origin removed (api.py): removed unnecessary"null"from allowed CORS origins.
schema_versiontable populated (manifest.py): the migration now inserts an initial schema version for future upgrade detection.- Retention UI max corrected (
Settings.jsx):weekly_daysinput max raised from 1825 to 3650 to accommodate the 7-year (2555-day) compliance default. - Electron-builder config paths fixed (
package.json):config/**/*updated tobackend/config/**/*since the rootconfig/directory was removed in v2.0.0. - Duplicate LIKE escaping consolidated (
manifest.py):clear_file_hashesnow uses the shared_escape_like()helper instead of reimplementing the same logic. - "Start with Windows" hidden on non-Windows (
main.js): the tray menu item is now only shown on Windows. - Adaptive LiveRun polling (
LiveRun.jsx): polls every 1s during active runs, every 5s when idle (saves battery on laptops). - Encryption description updated (
Settings.jsx): UI now correctly states "AES-256-GCM streaming" instead of "AES-128 (Fernet)".
- CSP hardened: removed
unsafe-inlinefromscript-srcinindex.html— all inline style injection replaced with static CSS imports asyncio.Lockon backup guard:_active_runcheck-and-set inapi.pyis now atomic, eliminating the race condition where two concurrent triggers could start duplicate backup runs- Encryption key rotation UI: new
POST /settings/encryption/generate-keyendpoint and Settings panel card with a confirmation modal — key is generated server-side, displayed once for the user to save, and never persisted by the backend
src/GhostBackup.jsxrewritten from 2134 lines to ~110 lines — all UI extracted into purpose-built files:src/pages/:Dashboard,LiveRun,LogsViewer,BackupConfig,RestoreUI,Settingssrc/components/:StatusPill,SsdGauge,Heatmap,Countdown,ErrBanner,LoadingState,AlertBell
src/main.jsx: removed inline<style>injection; importssplash.cssandstyles.cssas static filessrc/styles.cssandsrc/splash.css: all 600+ lines of CSS extracted from JSX template literalssrc/api-client.js: addedgenerateEncryptionKey()method
version_countremoved from API surface (api.py,config.py,BackupConfig.jsx): the setting was accepted and stored but silently had no effect on the pruner — exposing it was misleading. Removed until the pruner enforces it.setup_helper.py: YAML config patching replaced from fragile string-replace to properyaml.safe_load→ mutate →yaml.dump. Windows backslash paths, quoted values, and any future format changes are handled correctly. Added duplicate-source guard.utils.py(new): sharedfmt_bytes/fmt_durationhelpers extracted frommanifest.pyandreporter.pywhere they were duplicated verbatim.
package.json: all^version prefixes removed — exact pins across all 10 packages for reproducible installs- Added
@testing-library/react 15.0.7and@testing-library/user-event 14.5.2as pinned devDependencies vite.config.js: fixed testincludepath (was doubled underroot: "src"), addedsetupFilesfor@testing-library/reactcleanup
Backend (214 pytest tests across 10 files):
| File | Tests |
|---|---|
test_utils.py |
20 — fmt_bytes / fmt_duration boundaries |
test_reporter.py |
25 — Alert, AlertManager, Reporter.send_run_report |
test_crypto.py |
12 — encrypt/decrypt round-trip, wrong-key rejection, no-op mode |
test_syncer_scan.py |
18 — scan_source: exclusions, incremental cache, force_full, size guard |
test_setup_helper.py |
8 — YAML parse→mutate→dump, duplicate guard, Windows paths |
test_api.py |
+5 — TestEncryptionKey class added to existing suite |
test_manifest.py |
Fixed broken import after _fmt_bytes/_fmt_duration moved to utils.py |
Frontend (60 vitest tests across 3 files):
| File | Tests |
|---|---|
api-client.test.js |
+1 — generateEncryptionKey method |
components.test.jsx |
29 — ErrBanner, StatusPill (all statuses), LoadingState, Countdown |
src/api-client.js (fixed)
- Added
export default api—GhostBackup.jsximports the default export; the missing default export would have caused every API call to throw at runtime - Added 20 named convenience methods (
health,dashboard,startRun,stopRun,runStatus,getRuns,getRun,getRunLogs,restore,getConfig,updateConfig,addSite,removeSite,updateSmtp,testSmtp,updateRetention,runPrune,ssdStatus,getAlerts,dismissAlert,dismissAllAlerts,watcherStatus,watcherStart,watcherStop) — all calls fromGhostBackup.jsxnow resolve to correct endpoints
src/GhostBackup.jsx (fixed)
- Replaced 6 raw
fetch("http://127.0.0.1:8765/...")calls with the authenticatedapi.*()wrapper — these calls bypassed theX-API-Keyheader and would have returned 401 for any token-protected deployment:SsdDriveStatuscomponent:fetch(/ssd/status)→api.ssdStatus()- Settings panel
useEffect+refreshSsd(): twofetch(/ssd/status)→api.ssdStatus() AlertBell.fetchAlerts():fetch(/alerts)→api.getAlerts()AlertBell.dismiss():fetch(/alerts/:id/dismiss)→api.dismissAlert(id)AlertBell.dismissAll():fetch(/alerts/dismiss-all)→api.dismissAllAlerts()
src/tests/api-client.test.js (updated)
- Added 16 new tests for the named API methods and for the
export defaultidentity check - Total frontend test count: 38 tests
install.bat (new)
- One-step Windows installer: checks Python 3.10+ and Node 18+, creates
.venv, installs all dependencies, then callssetup_helper.py - Creates
start.batas a one-click launcher after setup completes
backend/setup_helper.py (new)
- Interactive setup: asks for source folder, primary SSD path, and optional secondary SSD path
- Generates a Fernet encryption key, saves it to
.env.local, and displays it prominently on screen with instructions to store it on a separate device - Patches
config.yamlin place with the entered paths so the app is ready to run immediately
backend/tests/test_api.py (new)
- 30 FastAPI endpoint tests covering
/health, auth middleware,/run/start,/run/stop,/run/status,/runs,/config,/restore,/alerts,/settings/retention,/verify,/watcher - All backend services mocked — tests run without a real SSD, scheduler, or file watcher
src/tests/api-client.test.js (new)
- 22 Vitest unit tests for
api-client.jscoveringApiError,request(), token caching, query string handling, error propagation, and theapiconvenience wrapper - Runs in jsdom — no browser or Electron required
package.json / vite.config.js
- Added
vitestandjsdomdev dependencies - Added
npm testandnpm run test:watchscripts - Added
testblock tovite.config.jspointing atsrc/tests/
backend/api.py
- Moved inline imports (
shutil,http.client,json,pathlib.Path) to module level - Replaced deprecated
asyncio.get_event_loop()withasyncio.get_running_loop()(Python 3.10+) - Extracted
_desktop_notify(),_new_run_state(),_retry_locked_files(), and_backup_manifest_to_ssd()out ofrun_backup_jobto reduce function length - Fixed private method access: replaced
syncer._hash_file_direct()withsyncer.hash_file() - Wrapped
executor.shutdown()intry/finallyto guarantee cleanup on error
backend/manifest.py
- Fixed thread-safety bug: added missing
with self._lock:guard inget_backup_files_for_prune() - Added
get_latest_backed_up_files_for_source()public method — callers no longer access._conndirectly - Added
_escape_like()helper to correctly escape%and_in SQLite LIKE queries - Moved
import socketfrom insidelog_config_change()to module level
backend/syncer.py
- Renamed
_hash_file_direct()→hash_file()to correctly reflect its public usage verify_backups()now uses the new manifest public method instead of accessing._conndirectly- Added
_LARGE_FILE_WARN_BYTESconstant (200 MB) with a log warning when Fernet loads a large file into memory
backend/watcher.py
- Split
_last_triggeredinto_last_triggered_mono(monotonic, for cooldown arithmetic) and_last_triggered_at(datetime, for display) — eliminated fragile mixed-clock calculation status()now returns a correctly formatted timestamp string
backend/config.py
- Removed duplicate
SourceConfig.circuit_breaker_thresholdfield — threshold is defined at global config level only - Moved
_deep_mergeto a module-level function (was a static method that inconsistently both mutated and returnedbase) remove_site()simplified usingnext()with a defaultadd_site()error messages now state exactly which field is missing- Fixed side-effect bug in
update_smtp()— no longer mutates the caller's dict via.pop()
backend/reporter.py
- Fixed thread-safety bug:
Alert._id_counternow incremented under_alert_id_lockvia_next_alert_id() - Fixed
REPORT_DIRfromPath("reports")(CWD-relative) toPath(__file__).parent / "reports"(anchored to module location) - Added proper type annotation for
_notify_callback:Optional[Callable[[str, str], Coroutine]] - Extracted
_LEVEL_COLOURSand_LEVEL_ICONSas module-level constants
backend/scheduler.py
- Replaced deprecated
datetime.utcnow()withdatetime.now(timezone.utc)(Python 3.12+) - Made
_job_start_timetimezone-aware - Moved
from datetime import timezoneout of an inner function into module-level imports - Extracted
_parse_time()to module level for testability - Renamed
_watchdog_alerted→_stall_alertedfor clarity
electron/main.js
- Removed
Atomics.wait(...)call that was synchronously blocking the Electron main thread for 300 ms on every startup - Replaced with an async
sleep()helper usingsetTimeout
src/api-client.js
- Set
ApiError.name = "ApiError"to fixinstanceofchecks across module boundaries - Added
clearTokenCache()export for use in tests - Added JSDoc parameter and return types to
request()
backend/tests/ (new)
- Added pytest test suite:
test_manifest.py(25 tests),test_config.py(34 tests),test_syncer_utils.py(18 tests),test_scheduler_utils.py - Added
conftest.pyto configuresys.pathfor all test modules
Silent. Secure. Compliant.


