source code added

This commit is contained in:
2026-08-28 19:24:04 +05:30
parent c446fa7f01
commit 5b34c127de
138 changed files with 12915 additions and 0 deletions

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