Files
automiz_app_full/development/planxo/PLANXO_SYSTEM_DOC.md
2026-08-28 19:24:04 +05:30

237 lines
12 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` |