source code added
This commit is contained in:
236
development/planxo/PLANXO_SYSTEM_DOC.md
Normal file
236
development/planxo/PLANXO_SYSTEM_DOC.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# PlanXO Desktop Sync – System Documentation & Issue Report
|
||||
|
||||
> Last reviewed: March 2026
|
||||
> Target platform: macOS Desktop (Flutter); Windows Desktop planned
|
||||
|
||||
---
|
||||
|
||||
## 1. System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PlanXO Desktop App (Flutter/Dart) │
|
||||
│ main.dart │
|
||||
└───────────────────────────┬─────────────────────────────────────┘
|
||||
WebSocket (wss) │ HTTP (https / dio)
|
||||
┌──────────────────┘──────────────────────┐
|
||||
▼ ▼
|
||||
┌─────────────────────┐ ┌──────────────────────────┐
|
||||
│ WebSocket Bridge │ axios (HTTP)│ Asset Manager (Python) │
|
||||
│ server.js (Node) │◄──────────────┤ asset_manager_app.py │
|
||||
│ Port: WS_PORT | │ │ Port: 5002 │
|
||||
│ Port: HTTP_PORT │ │ S3 + MongoDB │
|
||||
└─────────┬───────────┘ └──────────────────────────┘
|
||||
│ HTTP (axios)
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ CMS Backend │
|
||||
│ app.py (Flask) │
|
||||
│ + MongoDB + JWT │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Component Roles
|
||||
|
||||
| Component | Language | Responsibility |
|
||||
|---|---|---|
|
||||
| `main.dart` | Flutter/Dart | Desktop UI, file watch, upload/download sync |
|
||||
| `server.js` | Node.js (Express + WS) | WebSocket relay, auth key registration/approval, message routing |
|
||||
| `asset_manager_app.py` | Python (Flask) | S3 operations: upload presign, download folder as ZIP, asset CRUD |
|
||||
| `app.py` | Python (Flask) | CMS logic: users, projects, auth key CRUD, JWT, role enforcement |
|
||||
|
||||
### Frontend Admin Settings
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `DesktopSyncSettings.js` | Sets global sync interval (stored in CMS backend via `/settings/desktop-sync`) |
|
||||
| `FolderSyncSettings.js` | Sets admin folder age filter (how many days back to sync; via `/settings/folder-sync`) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Authentication Flow
|
||||
|
||||
```
|
||||
Desktop App WebSocket Server CMS Backend Browser
|
||||
│ │ │ │
|
||||
│─── WSS connect (apiKey, unique_id) ───────────────────► │ │
|
||||
│ │ │ │
|
||||
│─── register_auth_key ─────────► │ │
|
||||
│ │─── POST /planxo/app/auth/register-key ──────────► │
|
||||
│ │◄── {auth_key, browser_url} ───────────────────── │
|
||||
│◄── auth_key_registered ───────│ │ │
|
||||
│ │ │ │
|
||||
│─── open browser with /planxo-auth/{auth_key} ─────────────────────────────────► │
|
||||
│ │ │ │
|
||||
│ (polling validate_auth every 2s) ──────────────────────► │ │
|
||||
│ │ │ │
|
||||
│ │ Admin approves in browser ◄────────────────│
|
||||
│ │◄── POST /api/planxo/app/auth/{key}/approve ────── │
|
||||
│◄── auth_approved (user, sync_interval) ────────────────── │ │
|
||||
│ │ │ │
|
||||
│ store auth key, start sync │ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Sync Logic (Download Path)
|
||||
|
||||
1. After successful auth, `fetchFileList()` is called.
|
||||
2. App fetches filtered folders list: `GET /planxo/folders/filtered` (HTTP, with `X-Auth-Key` header).
|
||||
3. For each folder in the list, `_downloadAndExtractFolder(folder)` is called:
|
||||
- `GET /folders/download?folder=<encoded>` → returns a ZIP stream.
|
||||
- ZIP is decoded with `archive` package.
|
||||
- For each file in ZIP: if local file does NOT exist OR remote mtime is newer → write to disk.
|
||||
- After extraction, `_primeKnownKeys(prefix)` fetches the S3 asset list into `_knownS3Keys`.
|
||||
- A `DirectoryWatcher` is started on the local folder.
|
||||
4. Periodic sync (per-folder timer or global timer) calls `_downloadAndExtractFolder` again to check for remote changes.
|
||||
|
||||
## 4. Sync Logic (Upload Path)
|
||||
|
||||
1. `DirectoryWatcher` fires `ADD` or `MODIFY` events.
|
||||
2. `_onLocalFileChanged(path, folderPrefix)` is called:
|
||||
- Debounces: skip if same file was processed < 1 second ago.
|
||||
- Computes SHA-256 hash. Compares to `_syncMeta[path]['hash']`. Skips if unchanged.
|
||||
- If file key is in `_knownS3Keys`: calls `_replaceExisting()` (presign-replace → PUT to S3 → complete-replace).
|
||||
- Else: calls `_uploadNew()` (presign-upload → PUT to S3 → register-upload).
|
||||
- Updates `_syncMeta` and persists to `~/.planxo_sync_meta.json`.
|
||||
3. Periodic `_syncLocalChanges()` also scans all files and uploads modified ones (same logic).
|
||||
|
||||
## 5. Persistence Files (macOS `$HOME`)
|
||||
|
||||
| File | Content |
|
||||
|---|---|
|
||||
| `~/.planxo_client` | Last used client name (subdomain) |
|
||||
| `~/.planxo_auth_key` | Saved auth key (plaintext) |
|
||||
| `~/.planxo_storage` | Storage base path |
|
||||
| `~/.planxo_sync_settings.json` | Per-folder sync enabled/interval |
|
||||
| `~/.planxo_sync_meta.json` | Per-file `{lastModified, hash}` for change detection |
|
||||
| `~/Library/.../sync_data.db` | SQLite event log via DatabaseHelper |
|
||||
|
||||
## 6. Role-Based Sync Intent (Design Goal)
|
||||
|
||||
| Role | Intended Behaviour |
|
||||
|---|---|
|
||||
| **Normal user** | Sync only folders/projects assigned to them |
|
||||
| **Admin** | Sync all projects created within `folder_age_days` (configured in Frontend `FolderSyncSettings.js`) |
|
||||
|
||||
The filtering is supposed to happen at `GET /planxo/folders/filtered` on the CMS backend. The desktop app currently passes `X-Auth-Key` so the backend knows who the user is.
|
||||
|
||||
---
|
||||
|
||||
## 7. Issues Found
|
||||
|
||||
### 🔴 Critical Bugs
|
||||
|
||||
|
||||
|
||||
### 🟠 Logic Bugs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### 7.16 `_buildLoginScreen` Shows WS Status But Never Initiates Connection
|
||||
|
||||
The login screen shows a red/green dot for WS status, but `_connectWs()` is never called on app launch. The dot will always be red until the user clicks "Login".
|
||||
|
||||
See also issue 7.14 above.
|
||||
|
||||
---
|
||||
|
||||
#### 7.17 `MyApp` (Root Widget) Holds All State
|
||||
|
||||
The entire app state including network connections, timers, and file watchers lives in `_MyAppState` which wraps `MaterialApp`. This is an anti-pattern in Flutter. A rebuild of `MyApp` disposes and re-creates all state including open WebSockets.
|
||||
|
||||
**Recommendation:** Break out a `HomePage` stateful widget and keep `MyApp` as a thin shell containing only `MaterialApp` configuration.
|
||||
|
||||
---
|
||||
|
||||
#### 7.18 Auth Key Stored as Plaintext File
|
||||
|
||||
`~/.planxo_auth_key` is a plaintext file readable by any process running as the same user. On a shared machine this is a security risk.
|
||||
|
||||
**Recommendation (macOS):** Use the macOS Keychain via the `flutter_secure_storage` package.
|
||||
|
||||
---
|
||||
|
||||
#### 7.19 `_storageController` and `_clientController` Are Leaked
|
||||
|
||||
These `TextEditingController` instances are initialized in `initState` but the `_storageController.text` is only set later in async callbacks (`_loadStoragePath`, `_loadClientName`). If the widget is disposed before these complete, there could be a "setState called after dispose" error. The async loaders call `setState()` without a `mounted` guard.
|
||||
|
||||
**Fix:** Wrap all `setState()` calls inside async methods with `if (!mounted) return;`.
|
||||
|
||||
---
|
||||
|
||||
#### 7.20 `_syncInterval` from Backend Not Passed to Per-Folder Timers
|
||||
|
||||
`_syncInterval` (received from `auth_approved` or `auth_success`) is used only for `_periodicSyncTimer`. The per-folder timers (`_syncTimers`) use a locally configured interval. There is no reconciliation between the admin-set global interval from the frontend (`DesktopSyncSettings.js`) and the per-folder interval.
|
||||
|
||||
**Fix:** When `_syncInterval` is received from the server, apply it as the default for any new folder timers and update existing ones.
|
||||
|
||||
---
|
||||
|
||||
## 8. Windows Desktop Readiness
|
||||
|
||||
| Item | Status |
|
||||
|---|---|
|
||||
| `dart:io` WebSocket | ✅ Works on Windows |
|
||||
| `file_selector` | ✅ Has Windows support |
|
||||
| `watcher` | ✅ Uses native FS events |
|
||||
| `sqflite` + FFI | ⚠️ Needs `sqflite_common_ffi` + Windows SQLite DLL |
|
||||
| `Process.run('open', [...])` | ❌ macOS only – all `openInFinder`, `_openUrl`, `_ensureStorageBaseExists` use `open` command |
|
||||
| `Platform.environment['HOME']` | ⚠️ On Windows use `Platform.environment['USERPROFILE']` |
|
||||
| Sandbox / path permissions | ✅ Windows has fewer restrictions than macOS |
|
||||
|
||||
**Recommended abstraction:**
|
||||
|
||||
```dart
|
||||
String getHomeDir() {
|
||||
if (Platform.isMacOS || Platform.isLinux) {
|
||||
return Platform.environment['HOME'] ?? Directory.current.path;
|
||||
} else if (Platform.isWindows) {
|
||||
return Platform.environment['USERPROFILE'] ?? Platform.environment['HOMEPATH'] ?? 'C:\\Users\\Default';
|
||||
}
|
||||
return Directory.current.path;
|
||||
}
|
||||
|
||||
Future<void> openInExplorer(String path) async {
|
||||
if (Platform.isMacOS) {
|
||||
await Process.run('open', ['-R', path]);
|
||||
} else if (Platform.isWindows) {
|
||||
await Process.run('explorer', ['/select,', path]);
|
||||
} else {
|
||||
await Process.run('xdg-open', [p.dirname(path)]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Summary Table
|
||||
|
||||
| # | Severity | Category | Issue |
|
||||
|---|---|---|---|
|
||||
| 7.1 | 🔴 Critical | Architecture | `unique_id` hardcoded – breaks multi-machine |
|
||||
| 7.2 | 🔴 Critical | Platform | `sqflite` won't work on macOS/Windows desktop without FFI |
|
||||
| 7.3 | 🔴 Critical | Logic | `_uploadFile` ignores `relativePath` param |
|
||||
| 7.4 | 🔴 Critical | Logic | ZIP mtime is DOS format, not Unix – skip-overwrite logic wrong |
|
||||
| 7.5 | 🔴 Critical | Stability | `_ws!.add` without null/open guard → potential crash |
|
||||
| 7.6 | 🟠 Bug | Stability | Double reconnect scheduling, exponential reconnect storms |
|
||||
| 7.7 | 🟠 Bug | Logic | `_handleAuthRevoked` leaks per-folder timers |
|
||||
| 7.8 | 🟠 Bug | Performance | Auth polling sends WS validate during pending phase (noise) |
|
||||
| 7.9 | 🟠 Bug | Logic | `_primeKnownKeys` expects List, gets Map → always empty |
|
||||
| 7.10 | 🟠 Bug | Logic | Role-based folder filtering skipped when WS is connected |
|
||||
| 7.11 | 🟠 Bug | Logic | Two overlapping periodic sync timers cause race conditions |
|
||||
| 7.12 | 🟡 Quality | State | `_knownS3Keys` lost on restart → all files re-uploaded as new |
|
||||
| 7.13 | 🟡 Quality | Deps | `http` and `web_socket_channel` are unused dependencies |
|
||||
| 7.14 | 🟡 Quality | UX | WS not auto-connected on startup with saved auth |
|
||||
| 7.15 | 🟡 Quality | Feature | `guessContentType` missing most media file types |
|
||||
| 7.16 | 🟡 Quality | UX | Login screen WS indicator always red at launch |
|
||||
| 7.17 | 🟡 Quality | Architecture | All state in root `MyApp` widget (anti-pattern) |
|
||||
| 7.18 | 🟡 Quality | Security | Auth key stored as plaintext file |
|
||||
| 7.19 | 🟡 Quality | Stability | `setState` after dispose possible in async loaders |
|
||||
| 7.20 | 🟡 Quality | Feature | Backend sync interval not applied to per-folder timers |
|
||||
| W.1 | 🟠 Platform | Windows | All `Process.run('open', ...)` calls are macOS-only |
|
||||
| W.2 | 🟠 Platform | Windows | `HOME` env var not set on Windows – use `USERPROFILE` |
|
||||
Reference in New Issue
Block a user