From 1c04f2e2283e086c59e44e4814a709e409d4ee73 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 06:23:30 +0000 Subject: [PATCH] Improve repo hygiene, safety parity, and docs after Sub-GHz Restore the missing CI test workflow for web/desktop/C suites, fix stale /stor0800 paths in the flight manual, sync README/ROADMAP with shipped Favorites/NFC/Sub-GHz features, wire the USB auto-check setting to the status poll, and make on-device safety matching honor flexible whitespace like safety-rules.json. Also centralize NFC/Sub-GHz paths and stop tracking host-test binaries. Co-authored-by: Muhammad Rafiq --- .github/workflows/test.yml | 48 +++++++++ README.md | 24 +++-- desktop_helper/src/lib/paths.ts | 4 + docs/ROADMAP.md | 5 +- docs/flight_manual.md | 8 +- docs/installer-flow.md | 16 ++- src/flipdeck_app.c | 21 +++- src/paths.h | 2 + src/profile_manager.c | 136 +++++++++++++++++++------- src/tests/host/.gitignore | 5 + src/tests/host/test_profile_manager | Bin 34512 -> 0 bytes src/tests/host/test_profile_manager.c | 8 ++ src/tests/host/test_usb_hid | Bin 25616 -> 0 bytes web/CLAUDE.md | 2 +- web/src/lib/flipper-serial.ts | 16 +-- web/src/lib/paths.ts | 4 + 16 files changed, 237 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 src/tests/host/.gitignore delete mode 100755 src/tests/host/test_profile_manager delete mode 100755 src/tests/host/test_usb_hid diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9436737 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,48 @@ +name: Tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + web: + name: Web installer (vitest) + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: web/package-lock.json + - run: npm ci + - run: npm test + + desktop-helper: + name: Desktop helper (jest) + runs-on: ubuntu-latest + defaults: + run: + working-directory: desktop_helper + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: desktop_helper/package-lock.json + - run: npm ci + - run: npm test + + flipper-host: + name: Flipper C app (host tests) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run host tests + run: make -C src/tests/host run diff --git a/README.md b/README.md index 98e832c..f6d4b53 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,12 @@ FlipDeck transforms your Flipper Zero into a programmable command deck. Store fr - ✅ **Profile Storage** — Save command profiles on the SD card - ✅ **USB HID Keyboard** — Send text, key presses, and shortcuts -- ✅ **Profile Types** — Commands, snippets, shortcuts, and scripts -- ✅ **No WiFi Required** — Works on stock Flipper Zero -- ✅ **Safe Mode** — Confirmation before sending commands -- ✅ **Settings** — Configurable delays and preferences +- ✅ **Favorites + quick-send** — Pin actions; long-press OK to skip confirm +- ✅ **NFC tag triggers** — Bind a tag to an action's confirm screen +- ✅ **Sub-GHz RF triggers** — RX-only 433MHz remote bind/fire (never transmits) +- ✅ **WiFi Dev Board UART** — Optional `wifi_uart` command target +- ✅ **Safe Mode** — Confirmation before sending; critical patterns blocked +- ✅ **Settings** — Configurable delays, confirm, and USB auto-check ## Installation for Flipper Zero users @@ -228,6 +230,8 @@ On the Flipper, the SD card is mounted under `/ext`, so the app reads from `/ext ├── snippets/ # Text snippet templates │ ├── typescript.txt # TS code templates │ └── go.txt # Go code templates +├── nfc_tags.json # NFC tag → action bindings (created on first bind) +├── subghz_remotes.json # Sub-GHz remote → action bindings (created on first bind) ├── logs/ # Session logs └── settings.json # User preferences ``` @@ -302,22 +306,25 @@ flipdeck/ ├── src/ # Flipper app source │ ├── flipdeck_app.c # Main application logic │ ├── flipdeck_app.h -│ ├── flipdeck_ui.c # User interface (browser, favorites, NFC scan, confirm) +│ ├── flipdeck_ui.c # UI (browser, favorites, NFC/Sub-GHz scan, confirm) │ ├── flipdeck_ui.h -│ ├── profile_manager.c # SD card profile system, favorites, NFC tag mappings +│ ├── profile_manager.c # Profiles, favorites, NFC + Sub-GHz mappings │ ├── profile_manager.h │ ├── usb_hid.c # USB HID communication │ ├── usb_hid.h │ ├── uart_bridge.c # UART bridge to the WiFi Dev Board │ ├── uart_bridge.h -│ ├── nfc_bridge.c # NFC tag scan bridge (tag-triggered sends) +│ ├── nfc_bridge.c # NFC tag scan bridge │ ├── nfc_bridge.h +│ ├── subghz_bridge.c # Sub-GHz RX-only remote scan bridge +│ ├── subghz_bridge.h │ └── settings.c # Settings management ├── web/ # Next.js web installer ├── docs/ │ ├── flight_manual.md # Safety and usage guide │ ├── installer-flow.md # Web Serial / ZIP installer walkthrough │ ├── nfc_trigger_spec.md # NFC tag trigger design +│ ├── subghz_trigger_spec.md # Sub-GHz remote trigger design │ ├── security-model.md # Safety rules and confirmation model │ └── ROADMAP.md # Development roadmap └── README.md @@ -346,9 +353,8 @@ See [docs/ROADMAP.md](docs/ROADMAP.md) for planned features. - [ ] Custom profile creation from Flipper UI - [ ] Profile import/export via SD card -- [ ] Key combination support - [ ] Presentation remote mode -- [ ] Desktop companion app for profile sync +- [ ] Desktop companion app for live profile sync - [ ] Momentum firmware integration, optional ## Contributing diff --git a/desktop_helper/src/lib/paths.ts b/desktop_helper/src/lib/paths.ts index f625c28..f115172 100644 --- a/desktop_helper/src/lib/paths.ts +++ b/desktop_helper/src/lib/paths.ts @@ -19,6 +19,10 @@ export const PATHS = { SNIPPETS_DIR: "/ext/apps_data/flipdeck/snippets", // Settings file SETTINGS_FILE: "/ext/apps_data/flipdeck/settings.json", + // NFC tag → action mappings + NFC_TAGS_FILE: "/ext/apps_data/flipdeck/nfc_tags.json", + // Sub-GHz remote → action mappings + SUBGHZ_REMOTES_FILE: "/ext/apps_data/flipdeck/subghz_remotes.json", // Logs directory LOGS_DIR: "/ext/apps_data/flipdeck/logs", // The installed flipdeck.fap app itself diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d76bc31..faf6ff1 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -22,15 +22,16 @@ ## Phase 3: USB Enhancements - [x] Key combination support (Ctrl, Alt, Shift, Win) -- [x] Delay configuration per profile (`delay_ms` per command) +- [x] Delay configuration per command (`delay_ms` on each command; profile-level default still open — see #42) - [ ] Key press/release timing control -- [ ] USB connection auto-detection +- [x] USB connection auto-detection (settings toggle gates the status poll) - [ ] Multiple keyboard layouts ## Phase 4: Advanced Features - [x] Favorites + quick-send (long-press OK skips confirm; Right toggles favorite) - [x] NFC tag triggers (scan a tag to jump straight to a mapped action's confirm screen) +- [x] Sub-GHz RF remote triggers (RX-only 433MHz bind/fire; always confirms) - [ ] Presentation remote mode (left/right/up/down keys) - [ ] Mouse movement and clicks - [ ] Desktop companion app for profile sync diff --git a/docs/flight_manual.md b/docs/flight_manual.md index 3034308..bbf9996 100644 --- a/docs/flight_manual.md +++ b/docs/flight_manual.md @@ -36,14 +36,14 @@ Default profiles use safe commands (comments, harmless text). You must create or ### Review Default Profiles Default profiles include Git and dev commands. Review them in: ``` -/stor0800/flipdeck/profiles/ +/ext/apps_data/flipdeck/profiles/ ``` ### Backup Your SD Card Before creating many profiles, backup your SD card. Profile data is stored in: ``` -/stor0800/flipdeck/profiles/ -/stor0800/flipdeck/settings.json +/ext/apps_data/flipdeck/profiles/ +/ext/apps_data/flipdeck/settings.json ``` --- @@ -119,7 +119,7 @@ FlipDeck's default flow is Category → Action → Confirm → Send. These short Hold MENU + DOWN during startup to disable USB. ### Factory Reset -Delete `/stor0800/flipdeck/` folder to reset all profiles and settings. +Delete `/ext/apps_data/flipdeck/` folder to reset all profiles and settings. ### Report Issues Open an issue at: https://github.com/mohabbis/flipdeck/issues diff --git a/docs/installer-flow.md b/docs/installer-flow.md index b272f72..fe3b4f2 100644 --- a/docs/installer-flow.md +++ b/docs/installer-flow.md @@ -4,9 +4,23 @@ The web installer exists to make FlipDeck feel like a simple device install inst ## User Flow +There are two install paths. Both write the same on-device layout under +`/ext/apps_data/flipdeck/`. + +### Path A: Web Serial (Chrome / Edge) + +1. Open the deployed FlipDeck installer in Chrome or Edge. +2. Choose the default profiles or a custom subset. +3. Review the browser safety check (critical matches block install). +4. Click **Connect Flipper & Stage Pack**, pick the Flipper in the OS picker. +5. Click **Stage N profiles to Flipper** — files are written over the CLI serial port. +6. Launch FlipDeck from the Flipper apps menu (**Apps → Tools → FlipDeck**). + +### Path B: ZIP + qFlipper (any browser) + 1. Open the deployed FlipDeck installer. 2. Choose the default profiles or a custom subset. -3. Download the install ZIP. +3. Review the browser safety check, then download the install ZIP. 4. Extract the ZIP locally. 5. Drag `apps_data` onto the Flipper SD card root in qFlipper. 6. Launch FlipDeck from the Flipper apps menu. diff --git a/src/flipdeck_app.c b/src/flipdeck_app.c index 03db781..abee751 100644 --- a/src/flipdeck_app.c +++ b/src/flipdeck_app.c @@ -45,8 +45,15 @@ bool flipdeck_app_init(void* furi_void) { // Load categories from SD card flipdeck_load_categories(); - // Check USB connection - g_app_ctx->usb_connected = usb_hid_is_connected(); + // Check USB connection when auto-detect is enabled + if(g_app_ctx->settings.auto_detect_usb) { + g_app_ctx->usb_connected = usb_hid_is_connected(); + } else { + // Off: don't probe — avoid a sticky "NO USB" banner when the user + // has opted out of status polling. Send still checks the live HID + // link at the moment of transmission. + g_app_ctx->usb_connected = true; + } // Initialize UART bridge to the WiFi dev board uart_bridge_init(); @@ -112,9 +119,13 @@ void flipdeck_app_free(void* furi_void) { void flipdeck_app_loop(void* furi_void) { UNUSED(furi_void); - // Poll USB connection status - g_app_ctx->usb_connected = usb_hid_is_connected(); - + // Poll USB connection status when auto-detect is enabled + if(g_app_ctx->settings.auto_detect_usb) { + g_app_ctx->usb_connected = usb_hid_is_connected(); + } else { + g_app_ctx->usb_connected = true; + } + switch(g_app_ctx->state) { case FlipDeckState_Idle: break; diff --git a/src/paths.h b/src/paths.h index 5dfe850..889ccd8 100644 --- a/src/paths.h +++ b/src/paths.h @@ -17,6 +17,8 @@ #define FLIPDECK_PROFILES_PATH "/ext/apps_data/flipdeck/profiles" #define FLIPDECK_SNIPPETS_PATH "/ext/apps_data/flipdeck/snippets" #define FLIPDECK_SETTINGS_PATH "/ext/apps_data/flipdeck/settings.json" +#define FLIPDECK_NFC_TAGS_PATH "/ext/apps_data/flipdeck/nfc_tags.json" +#define FLIPDECK_SUBGHZ_REMOTES_PATH "/ext/apps_data/flipdeck/subghz_remotes.json" #define FLIPDECK_LOGS_PATH "/ext/apps_data/flipdeck/logs" #define FLIPDECK_APP_PATH "/ext/apps/Tools/flipdeck.fap" diff --git a/src/profile_manager.c b/src/profile_manager.c index d2ac981..795700d 100644 --- a/src/profile_manager.c +++ b/src/profile_manager.c @@ -5,6 +5,7 @@ #include "profile_manager.h" #include "flipdeck_app.h" +#include "paths.h" #include #include #include @@ -13,11 +14,10 @@ #include #include -#define FLIPDECK_DATA_PATH "/ext/apps_data/flipdeck" -#define PROFILE_BASE_PATH FLIPDECK_DATA_PATH "/profiles" -#define SETTINGS_PATH FLIPDECK_DATA_PATH "/settings.json" -#define NFC_TAGS_PATH FLIPDECK_DATA_PATH "/nfc_tags.json" -#define SUBGHZ_REMOTES_PATH FLIPDECK_DATA_PATH "/subghz_remotes.json" +#define PROFILE_BASE_PATH FLIPDECK_PROFILES_PATH +#define SETTINGS_PATH FLIPDECK_SETTINGS_PATH +#define NFC_TAGS_PATH FLIPDECK_NFC_TAGS_PATH +#define SUBGHZ_REMOTES_PATH FLIPDECK_SUBGHZ_REMOTES_PATH static void storage_ensure_flipdeck_dirs(Storage* storage) { storage_common_mkdir(storage, "/ext/apps_data"); @@ -794,6 +794,67 @@ bool profile_manager_save_subghz_remotes(const FlipDeckSubghzRemote* remotes, ui * @param value Command string to check * @return true if value is safe */ +/** + * @brief Case-folded substring match where each space in `pattern` matches + * one or more whitespace characters in `haystack`. + * + * Mirrors the `\s+` / `\s*` usage in safety-rules.json so on-device blocking + * catches the same spacing variants the web/desktop auditors reject + * (e.g. `rm -rf`, `dd\tif=`, `> /dev/sda`). + */ +static bool contains_flexible_ws(const char* haystack, const char* pattern) { + if(!haystack || !pattern || !*pattern) return false; + + for(const char* start = haystack; *start; start++) { + const char* h = start; + const char* p = pattern; + bool matched = true; + + while(*p) { + if(*p == ' ') { + if(!isspace((unsigned char)*h)) { + matched = false; + break; + } + while(isspace((unsigned char)*h)) h++; + p++; + continue; + } + if(*h != *p) { + matched = false; + break; + } + h++; + p++; + } + + if(matched) return true; + } + + return false; +} + +/** + * @brief True when haystack contains `|` followed by optional whitespace and + * then a whole-token match of `token` (already uppercased), approximating + * `\\|\\s*(sh|bash)`. Requires a non-alnum boundary after the token so + * `|BASH` does not spuriously match the `SH` token. + */ +static bool contains_pipe_token(const char* haystack, const char* token) { + size_t token_len = strlen(token); + for(const char* p = haystack; *p; p++) { + if(*p != '|') continue; + p++; + while(isspace((unsigned char)*p)) p++; + if(strncmp(p, token, token_len) == 0) { + char next = p[token_len]; + if(next == '\0' || !isalnum((unsigned char)next)) return true; + } + if(!*p) break; + } + return false; +} + bool profile_manager_is_value_safe(const char* value) { if(!value) return false; @@ -808,28 +869,36 @@ bool profile_manager_is_value_safe(const char* value) { } upper_value[len] = '\0'; - // Critical patterns — block the command. + // Critical patterns — block the command. Spaces in these patterns match + // one-or-more whitespace (see contains_flexible_ws), matching the + // `rm\s+-rf` / `dd\s+if=` / `>\s*/dev/sd` rules in safety-rules.json. const char* critical_patterns[] = { - "RM -RF", // Recursive delete + "RM -RF", // Recursive delete (rm\s+-rf) "MKFS", // Format filesystem - "DD IF=", // Raw disk write - "> /DEV/SD", // Raw disk redirect - ":(){ :|:& };:", // Fork bomb (no letters; case-insensitive copy is identical) + "DD IF=", // Raw disk write (dd\s+if=) + "> /DEV/SD", // Raw disk redirect with whitespace (>\s+/dev/sd) + ">/DEV/SD", // Raw disk redirect with no whitespace (>\s*/dev/sd) NULL }; for(int i = 0; critical_patterns[i]; i++) { - if(strstr(upper_value, critical_patterns[i])) { + if(contains_flexible_ws(upper_value, critical_patterns[i])) { FURI_LOG_W("FlipDeck", "Blocked critical command: %s", critical_patterns[i]); return false; } } + // Fork bomb: allow flexible whitespace around the classic shape. + if(strstr(upper_value, ":(){") && strstr(upper_value, ":|:&")) { + FURI_LOG_W("FlipDeck", "Blocked critical command: fork bomb"); + return false; + } + // Remote shell pipe: (curl|wget) ... | (sh|bash). Approximates the regex // "(curl|wget).*\\|\\s*(sh|bash)" — catches real one-liners like // "curl -fsSL https://x | bash" that a literal "curl | sh" match misses. bool has_fetch = strstr(upper_value, "CURL") || strstr(upper_value, "WGET"); - bool has_pipe_shell = strstr(upper_value, "| SH") || strstr(upper_value, "|SH") || - strstr(upper_value, "| BASH") || strstr(upper_value, "|BASH"); + bool has_pipe_shell = + contains_pipe_token(upper_value, "SH") || contains_pipe_token(upper_value, "BASH"); if(has_fetch && has_pipe_shell) { FURI_LOG_W("FlipDeck", "Blocked remote shell pipe"); return false; @@ -837,28 +906,29 @@ bool profile_manager_is_value_safe(const char* value) { // Warning patterns — surfaced in the log but allowed (the user still has to // confirm OK on-device before the command is sent). - const char* warning_patterns[] = { - "SUDO", // Privilege escalation - "CHMOD 777", // Loose permissions - "CHOWN ROOT", // Ownership change to root - NULL - }; - for(int i = 0; warning_patterns[i]; i++) { - if(strstr(upper_value, warning_patterns[i])) { - FURI_LOG_W("FlipDeck", "Warning: command matched %s", warning_patterns[i]); - } + if(strstr(upper_value, "SUDO")) { + FURI_LOG_W("FlipDeck", "Warning: command matched SUDO"); + } + if(contains_flexible_ws(upper_value, "CHMOD 777")) { + FURI_LOG_W("FlipDeck", "Warning: command matched CHMOD 777"); + } + if(contains_flexible_ws(upper_value, "CHOWN ROOT")) { + FURI_LOG_W("FlipDeck", "Warning: command matched CHOWN ROOT"); } - // Credential assignments (NAME=...) — warn only. - if(strchr(upper_value, '=')) { - const char* credential_patterns[] = { - "PASSWORD", "TOKEN", "API_KEY", "SECRET", "PRIVATE_KEY", NULL - }; - for(int i = 0; credential_patterns[i]; i++) { - if(strstr(upper_value, credential_patterns[i])) { - FURI_LOG_W("FlipDeck", "Warning: possible credential assignment"); - break; - } + // Credential assignments (NAME=...) — warn only. Allow whitespace around '=' + // to mirror `(PASSWORD|...)\\s*=` in safety-rules.json. + const char* credential_patterns[] = { + "PASSWORD", "TOKEN", "API_KEY", "SECRET", "PRIVATE_KEY", NULL + }; + for(int i = 0; credential_patterns[i]; i++) { + const char* hit = strstr(upper_value, credential_patterns[i]); + if(!hit) continue; + const char* after = hit + strlen(credential_patterns[i]); + while(isspace((unsigned char)*after)) after++; + if(*after == '=') { + FURI_LOG_W("FlipDeck", "Warning: possible credential assignment"); + break; } } diff --git a/src/tests/host/.gitignore b/src/tests/host/.gitignore new file mode 100644 index 0000000..dc6dd76 --- /dev/null +++ b/src/tests/host/.gitignore @@ -0,0 +1,5 @@ +# Built host-test binaries (rebuilt via `make`) +/test_profile_manager +/test_usb_hid +/test_profile_manager_target +/test_settings diff --git a/src/tests/host/test_profile_manager b/src/tests/host/test_profile_manager deleted file mode 100755 index 9c3d23e9077e6d58592a4958f300552a9f57da40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34512 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 LI3^E}R}}vj9{HdR diff --git a/src/tests/host/test_profile_manager.c b/src/tests/host/test_profile_manager.c index 00cbb25..f700219 100644 --- a/src/tests/host/test_profile_manager.c +++ b/src/tests/host/test_profile_manager.c @@ -26,6 +26,8 @@ static int g_failures = 0; static void test_dangerous_rm_rf(void) { EXPECT_FALSE(profile_manager_is_value_safe("rm -rf /"), "blocks rm -rf /"); EXPECT_FALSE(profile_manager_is_value_safe("echo x && rm -rf ."), "blocks rm -rf in chain"); + EXPECT_FALSE(profile_manager_is_value_safe("rm -rf /"), "blocks rm -rf (extra spaces)"); + EXPECT_FALSE(profile_manager_is_value_safe("rm\t-rf /tmp"), "blocks rm\\t-rf (tab)"); } static void test_warning_sudo(void) { @@ -41,14 +43,20 @@ static void test_dangerous_remote_exec(void) { profile_manager_is_value_safe("curl -fsSL https://example.com/install.sh | bash"), "blocks real curl|bash one-liner"); EXPECT_FALSE(profile_manager_is_value_safe("CURL https://x | SH"), "blocks curl|sh case-insensitively"); + EXPECT_FALSE(profile_manager_is_value_safe("curl https://x |\tbash"), "blocks curl … |\\tbash"); + EXPECT_FALSE(profile_manager_is_value_safe("wget https://x | sh"), "blocks wget … | sh"); /* "curl.ssh" has no pipe into a shell - it isn't a remote-exec pattern */ EXPECT_TRUE(profile_manager_is_value_safe("curl.ssh user@host"), "allows curl.ssh (no pipe to shell)"); + EXPECT_TRUE(profile_manager_is_value_safe("curl https://x | cat"), "allows curl … | cat"); } static void test_dangerous_disk_ops(void) { EXPECT_FALSE(profile_manager_is_value_safe("mkfs.ext4 /dev/sda"), "blocks mkfs"); EXPECT_FALSE(profile_manager_is_value_safe("dd if=/dev/zero of=/dev/sda"), "blocks dd if="); + EXPECT_FALSE(profile_manager_is_value_safe("dd if=/dev/zero of=/dev/sda"), "blocks dd if= (extra spaces)"); EXPECT_FALSE(profile_manager_is_value_safe("echo data > /dev/sda"), "blocks > /dev/sd"); + EXPECT_FALSE(profile_manager_is_value_safe("echo data >/dev/sda"), "blocks >/dev/sd (no space)"); + EXPECT_FALSE(profile_manager_is_value_safe("echo data > /dev/sdb"), "blocks > /dev/sd (extra spaces)"); } static void test_dangerous_fork_bomb(void) { diff --git a/src/tests/host/test_usb_hid b/src/tests/host/test_usb_hid deleted file mode 100755 index 2b9cb09a906b77a79b301fe1de45282c50b75fb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25616 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 diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 780f433..7eb89dd 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -27,7 +27,7 @@ npm test - Main install-pack endpoint: `/api/install-bundle/download` - The generated ZIP must include `README-FIRST.txt`, `apps_data/flipdeck/settings.json`, `apps_data/flipdeck/profiles/*.json`, and `apps_data/flipdeck/snippets/*.txt`. -- `npm run start` serves the standalone Next.js build on `0.0.0.0` for Railway. +- `npm run start` serves the standalone Next.js build on `0.0.0.0` (used for local/production Node hosting). ## Guidelines diff --git a/web/src/lib/flipper-serial.ts b/web/src/lib/flipper-serial.ts index 5aee0c7..e29709f 100644 --- a/web/src/lib/flipper-serial.ts +++ b/web/src/lib/flipper-serial.ts @@ -1,3 +1,5 @@ +import { PATHS } from "@/lib/paths"; + // Minimal Web Serial type declarations for Flipper Zero CLI communication interface FlipperSerialPort { open(options: { baudRate: number; dataBits: number; stopBits: number; parity: string }): Promise; @@ -318,25 +320,25 @@ export class FlipperSerial { const tick = (msg: string) => onProgress(msg, step++ / total); tick("Creating directory…"); - await this.ensureDir("/ext/apps_data/flipdeck"); + await this.ensureDir(PATHS.DEVICE.DATA_ROOT); tick("Creating profiles directory…"); - await this.ensureDir("/ext/apps_data/flipdeck/profiles"); + await this.ensureDir(PATHS.DEVICE.PROFILES_DIR); tick("Writing settings…"); - await this.writeFile("/ext/apps_data/flipdeck/settings.json", settingsJson); + await this.writeFile(PATHS.DEVICE.SETTINGS_FILE, settingsJson); for (const { id, json } of profiles) { tick(`Writing ${id}.json…`); - await this.writeFile(`/ext/apps_data/flipdeck/profiles/${id}.json`, json); + await this.writeFile(`${PATHS.DEVICE.PROFILES_DIR}/${id}.json`, json); } if (snippets.length) { tick("Creating snippets directory…"); - await this.ensureDir("/ext/apps_data/flipdeck/snippets"); + await this.ensureDir(PATHS.DEVICE.SNIPPETS_DIR); for (const { name, content } of snippets) { tick(`Writing snippet ${name}…`); - await this.writeFile(`/ext/apps_data/flipdeck/snippets/${name}`, content); + await this.writeFile(`${PATHS.DEVICE.SNIPPETS_DIR}/${name}`, content); } } @@ -351,7 +353,7 @@ export class FlipperSerial { await this.ensureDirRpc("/ext/apps/Tools"); tick("Installing flipdeck.fap…"); - await this.writeFileRpc("/ext/apps/Tools/flipdeck.fap", appBinary); + await this.writeFileRpc(PATHS.DEVICE.APP_PATH, appBinary); } onProgress(appBinary ? "Done!" : "Profiles staged. Install the app binary separately.", 1); diff --git a/web/src/lib/paths.ts b/web/src/lib/paths.ts index f625c28..f115172 100644 --- a/web/src/lib/paths.ts +++ b/web/src/lib/paths.ts @@ -19,6 +19,10 @@ export const PATHS = { SNIPPETS_DIR: "/ext/apps_data/flipdeck/snippets", // Settings file SETTINGS_FILE: "/ext/apps_data/flipdeck/settings.json", + // NFC tag → action mappings + NFC_TAGS_FILE: "/ext/apps_data/flipdeck/nfc_tags.json", + // Sub-GHz remote → action mappings + SUBGHZ_REMOTES_FILE: "/ext/apps_data/flipdeck/subghz_remotes.json", // Logs directory LOGS_DIR: "/ext/apps_data/flipdeck/logs", // The installed flipdeck.fap app itself