diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5a6a54b..e769eda 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,12 +56,13 @@ jobs: cp -r dev-content/* _site/dev/ 2>/dev/null || true cp -r dev-content/content _site/dev/ 2>/dev/null || true - # Inject dev badge with date (visual display and JS constant with DEV-EPOCH format) - DEV_DATE=$(date -u +"%Y-%m-%d %H:%M UTC") + # Inject DEV version (visual display) and JS constant with DEV-EPOCH format DEV_EPOCH=$(date -u +%s) - sed -i 's|MeshCore Wardrive|MeshCore Wardrive DEV '"${DEV_DATE}"'|' _site/dev/index.html + # Replace the visible version in the dedicated span (this is what the UI shows) + sed -i 's|]*>[^<]*|DEV-'"${DEV_EPOCH}"'|' _site/dev/index.html + # Inject into JS constant sed -i 's|const APP_VERSION = "UNKNOWN";|const APP_VERSION = "DEV-'"${DEV_EPOCH}"'";|' _site/dev/content/wardrive.js - + find _site -name ". git" -exec rm -rf {} + 2>/dev/null || true find _site -name ".github" -exec rm -rf {} + 2>/dev/null || true diff --git a/.gitignore b/.gitignore index c78af96..c055d2b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ Thumbs.db # Temporary files tmp/ +test-log-ui.html diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000..8591679 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,187 @@ +# Two-Bar Status System - Implementation Summary + +## Overview +This implementation separates connection status from operational status into two independent status bars. + +## Visual Structure + +``` +┌────────────────────────────────────────────────────────┐ +│ Connection Status Bar (#connectionStatus) │ +│ ● Connected │ +└────────────────────────────────────────────────────────┘ +┌────────────────────────────────────────────────────────┐ +│ Dynamic App Status Bar (#status) │ +│ Ping sent │ +└────────────────────────────────────────────────────────┘ +``` + +## Connection Status Bar +**Purpose**: Shows ONLY connection state +**Location**: Top bar with status indicator dot +**Messages**: Exactly 4 fixed states + +### The Four States +1. **Connected** (green) - Ready for wardriving after GPS init completes +2. **Connecting** (blue) - During entire connection process (steps 1-9) +3. **Disconnected** (red) - No device connected +4. **Disconnecting** (blue) - During entire disconnection process + +### Key Behavior +- Updates immediately (no delay) +- Never shows operational messages +- Controlled by `setConnStatus(text, color)` + +## Dynamic App Status Bar +**Purpose**: Shows ALL operational messages +**Location**: Status message box below connection bar +**Messages**: ~30 different operational messages + +### Message Types +- GPS status ("Priming GPS", "Waiting for GPS fix") +- Channel setup ("Looking for #wardriving channel", "Created #wardriving") +- Capacity check ("Acquiring wardriving slot", "Acquired wardriving slot") +- Ping operations ("Sending manual ping", "Ping sent") +- Countdown timers ("Waiting for next auto ping (15s)") +- API operations ("Posting to API") +- Error messages ("WarDriving app has reached capacity") +- Empty placeholder (em dash: `—`) + +### Key Behavior +- 500ms minimum visibility for first display +- Immediate updates for countdown timers +- Shows `—` when no message present +- Blocks connection words (Connected/Connecting/Disconnecting/Disconnected) +- Controlled by `setDynamicStatus(text, color, immediate)` + +## Connection Flow Example + +### During Connection +``` +Time | Connection Bar | Dynamic Bar +------|------------------|--------------------------- +0s | Connecting | — +1s | Connecting | Acquiring wardriving slot +3s | Connecting | Acquired wardriving slot +4s | Connecting | Looking for #wardriving channel +5s | Connecting | Channel #wardriving found +6s | Connecting | Priming GPS +8s | Connected | — +``` + +### During Disconnection (Normal) +``` +Time | Connection Bar | Dynamic Bar +------|------------------|--------------------------- +0s | Disconnecting | — +1s | Disconnected | — +``` + +### During Disconnection (Error - Capacity Full) +``` +Time | Connection Bar | Dynamic Bar +------|------------------|--------------------------- +0s | Disconnecting | — +1s | Disconnected | WarDriving app has reached capacity +``` + +## Key Implementation Details + +### Function Signatures +```javascript +// Connection Status Bar +setConnStatus(text, color) +// Example: setConnStatus("Connected", STATUS_COLORS.success) + +// Dynamic App Status Bar +setDynamicStatus(text, color, immediate = false) +// Example: setDynamicStatus("Ping sent", STATUS_COLORS.success) +// Example: setDynamicStatus("—") // Empty state +``` + +### Protection Mechanisms +1. **Em Dash Normalization**: Empty/null/whitespace values become `—` +2. **Connection Word Blocking**: Prevents connection words in dynamic bar +3. **Minimum Visibility**: First dynamic message respects 500ms minimum +4. **Countdown Updates**: Immediate updates every second after first display + +### Error Message Changes +All error messages in dynamic bar NO LONGER have "Disconnected:" prefix: + +**Before**: +- `"Disconnected: WarDriving app has reached capacity"` +- `"Disconnected: WarDriving slot has been revoked"` + +**After**: +- Connection Bar: `"Disconnected"` +- Dynamic Bar: `"WarDriving app has reached capacity"` +- Dynamic Bar: `"WarDriving slot has been revoked"` + +## Files Modified + +### Code +- `content/wardrive.js` + - Added `setConnStatus()` function + - Added `setDynamicStatus()` function + - Updated ~30+ status calls throughout + - Updated countdown timer integration + - Updated error handling + +### Documentation +- `docs/STATUS_MESSAGES.md` + - Complete rewrite with two-bar system + - Connection Status Bar section (4 messages) + - Dynamic App Status Bar section (~30 messages) + - Implementation details and examples + +- `docs/CONNECTION_WORKFLOW.md` + - Updated all workflow steps with separate bars + - Connection sequence clearly shows both bars + - Disconnection sequence clearly shows both bars + - Error flows updated without prefix + +## Testing Checklist + +### Connection Workflow +- [ ] Connection bar shows "Connecting" from start to GPS init +- [ ] Connection bar shows "Connected" only after GPS init completes +- [ ] Dynamic bar shows intermediate messages (capacity check, channel setup, GPS) +- [ ] Dynamic bar clears to `—` when connection completes + +### Disconnection Workflow +- [ ] Connection bar shows "Disconnecting" during disconnect process +- [ ] Connection bar shows "Disconnected" after cleanup completes +- [ ] Dynamic bar shows `—` for normal disconnect +- [ ] Dynamic bar shows error message (without prefix) for error disconnect + +### Error Scenarios +- [ ] Capacity full: Connection bar "Disconnected", Dynamic bar "WarDriving app has reached capacity" +- [ ] App down: Connection bar "Disconnected", Dynamic bar "WarDriving app is down" +- [ ] Slot revoked: Connection bar "Disconnected", Dynamic bar "WarDriving slot has been revoked" +- [ ] Public key error: Connection bar "Disconnected", Dynamic bar "Unable to read device public key; try again" + +### Dynamic Messages +- [ ] Ping operations show in dynamic bar only +- [ ] GPS status shows in dynamic bar only +- [ ] Countdown timers show in dynamic bar with smooth updates +- [ ] API posting shows in dynamic bar only +- [ ] Connection words NEVER appear in dynamic bar +- [ ] Em dash (`—`) appears when no message to display + +### Visual Appearance +- [ ] Connection status indicator dot changes color with connection state +- [ ] Both bars visible and clearly separated +- [ ] Messages properly colored (green success, blue info, red error, etc.) +- [ ] No visual glitches during transitions + +## Summary + +This implementation successfully separates connection state management from operational status display, providing: + +1. **Clear Connection State**: Always visible in top bar +2. **Rich Operational Feedback**: All app operations in dynamic bar +3. **Better UX**: Users can see connection state AND what the app is doing +4. **Consistent Behavior**: Connection bar for state, dynamic bar for everything else +5. **Proper Error Handling**: Error reasons clearly shown without confusion + +The code is complete, documented, and ready for testing and deployment. diff --git a/README.md b/README.md index 99d5ef2..2061d87 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,27 @@ This app automatically posts ping data to the YOW MeshMapper API to help compare --- +## 🛠️ Development + +### Building Tailwind CSS + +This project uses Tailwind CSS v4 to generate the styles. If you make changes to the HTML or need to rebuild the CSS: + +```bash +# Install dependencies +npm install + +# Build CSS once +npm run build:css + +# Watch for changes and rebuild automatically +npm run watch:css +``` + +The CSS is generated from `content/tailwind-in.css` and outputs to `content/tailwind.css`. + +--- + ## 🙏 Credits This project is a fork and adaptation: diff --git a/content/style.css b/content/style.css index 0d0c3cc..2b7ae46 100644 --- a/content/style.css +++ b/content/style.css @@ -177,4 +177,125 @@ body, .mesh-control .top-rpt-row div:last-child { flex: 0 0 auto; white-space: nowrap; +} + +/* Session Log - Static Expandable Section */ +#logBottomSheet.open { + display: block !important; +} + +#logExpandArrow.expanded { + transform: rotate(180deg); +} + +/* Log Entry Styling */ +.logEntry { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + background: rgba(15, 23, 42, 0.5); + border: 1px solid rgba(51, 65, 85, 0.7); + border-radius: 0.5rem; +} + +.logRowTop { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + color: #cbd5e1; +} + +.logTime { + font-weight: 500; +} + +.logCoords { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + color: #94a3b8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 12rem; +} + +/* Heard Repeats Chips Container */ +.heardChips { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + align-items: center; +} + +/* Chip Base Styling */ +.chip { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.375rem 0.625rem; + border-radius: 999px; + font-size: 0.75rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-weight: 500; + background: rgba(51, 65, 85, 0.6); + border: 1px solid rgba(71, 85, 105, 0.8); + color: #e2e8f0; + white-space: nowrap; +} + +.chipId { + font-weight: 600; +} + +.chipSnr { + font-weight: 400; +} + +/* SNR Color Coding */ +.snr-red .chipSnr { + color: #f87171; + font-weight: 600; +} + +.snr-orange .chipSnr { + color: #fb923c; + font-weight: 600; +} + +.snr-green .chipSnr { + color: #4ade80; + font-weight: 600; +} + +/* Mobile-responsive chip styling */ +@media (max-width: 640px) { + /* Reduce chip size for mobile */ + .chip { + padding: 0.25rem 0.5rem; + font-size: 0.625rem; + gap: 0.2rem; + } + + /* 3-column grid layout for chips on mobile */ + .heardChips { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.25rem; + } + + /* Ensure chips don't overflow their grid cells */ + .chip { + min-width: 0; + justify-content: center; + } + + /* Adjust chip text to be more compact */ + .chipId { + font-size: 0.625rem; + } + + .chipSnr { + font-size: 0.625rem; + } } \ No newline at end of file diff --git a/content/tailwind.css b/content/tailwind.css index 34fb042..bdfe2d6 100644 --- a/content/tailwind.css +++ b/content/tailwind.css @@ -1,4 +1,4 @@ -/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */ +/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ @layer properties; @layer theme, base, components, utilities; @layer theme { @@ -10,12 +10,10 @@ --color-red-300: oklch(80.8% 0.114 19.571); --color-red-500: oklch(63.7% 0.237 25.331); --color-red-600: oklch(57.7% 0.245 27.325); - --color-orange-100: oklch(95.4% 0.038 75.164); - --color-orange-300: oklch(83.7% 0.128 66.29); --color-amber-300: oklch(87.9% 0.169 91.605); + --color-amber-400: oklch(82.8% 0.189 84.429); --color-amber-500: oklch(76.9% 0.188 70.08); --color-amber-600: oklch(66.6% 0.179 58.318); - --color-lime-400: oklch(84.1% 0.238 128.85); --color-emerald-300: oklch(84.5% 0.143 164.978); --color-emerald-500: oklch(69.6% 0.17 162.48); --color-emerald-600: oklch(59.6% 0.145 163.225); @@ -24,20 +22,17 @@ --color-sky-600: oklch(58.8% 0.158 241.966); --color-indigo-500: oklch(58.5% 0.233 277.117); --color-indigo-600: oklch(51.1% 0.262 276.966); - --color-rose-500: oklch(64.5% 0.246 16.439); - --color-rose-600: oklch(58.6% 0.253 17.585); --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); --color-slate-300: oklch(86.9% 0.022 252.894); --color-slate-400: oklch(70.4% 0.04 256.788); - --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-500: oklch(55.4% 0.046 257.417); --color-slate-700: oklch(37.2% 0.044 257.287); --color-slate-800: oklch(27.9% 0.041 260.031); --color-slate-900: oklch(20.8% 0.042 265.755); - --color-zinc-500: oklch(55.2% 0.016 285.938); - --color-zinc-600: oklch(44.2% 0.017 285.786); - --color-black: #000; + --color-white: #fff; --spacing: 0.25rem; - --container-3xl: 48rem; + --container-xl: 36rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; @@ -52,10 +47,12 @@ --text-2xl--line-height: calc(2 / 1.5); --font-weight-medium: 500; --font-weight-semibold: 600; - --radius-sm: 0.25rem; - --radius-md: 0.375rem; + --tracking-wide: 0.025em; --radius-lg: 0.5rem; --radius-xl: 0.75rem; + --blur-sm: 8px; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); } @@ -209,84 +206,173 @@ } } @layer utilities { + .visible { + visibility: visible; + } + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border-width: 0; + } + .absolute { + position: absolute; + } .fixed { position: fixed; } + .relative { + position: relative; + } .static { position: static; } - .mr-2 { - margin-right: calc(var(--spacing) * 2); + .top-2 { + top: calc(var(--spacing) * 2); } - .mb-2 { - margin-bottom: calc(var(--spacing) * 2); + .right-2 { + right: calc(var(--spacing) * 2); + } + .bottom-2 { + bottom: calc(var(--spacing) * 2); + } + .left-2 { + left: calc(var(--spacing) * 2); + } + .z-10 { + z-index: 10; + } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } } - .mb-4 { - margin-bottom: calc(var(--spacing) * 4); + .mt-0\.5 { + margin-top: calc(var(--spacing) * 0.5); } - .ml-4 { - margin-left: calc(var(--spacing) * 4); + .mt-2 { + margin-top: calc(var(--spacing) * 2); } - .ml-auto { - margin-left: auto; + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .ml-2 { + margin-left: calc(var(--spacing) * 2); } .block { display: block; } + .contents { + display: contents; + } .flex { display: flex; } + .grid { + display: grid; + } .hidden { display: none; } - .inline-block { - display: inline-block; + .inline { + display: inline; + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-6 { + height: calc(var(--spacing) * 6); } .h-8 { height: calc(var(--spacing) * 8); } - .h-40 { - height: calc(var(--spacing) * 40); + .max-h-48 { + max-height: calc(var(--spacing) * 48); } - .h-140 { - height: calc(var(--spacing) * 140); + .max-h-64 { + max-height: calc(var(--spacing) * 64); } .min-h-screen { min-height: 100vh; } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-6 { + width: calc(var(--spacing) * 6); + } .w-8 { width: calc(var(--spacing) * 8); } - .w-40 { - width: calc(var(--spacing) * 40); + .w-full { + width: 100%; } - .w-64 { - width: calc(var(--spacing) * 64); + .max-w-xl { + max-width: var(--container-xl); } - .w-80 { - width: calc(var(--spacing) * 80); + .min-w-0 { + min-width: calc(var(--spacing) * 0); } - .w-130 { - width: calc(var(--spacing) * 130); + .flex-1 { + flex: 1; } - .w-full { - width: 100%; + .flex-shrink-0 { + flex-shrink: 0; } - .max-w-3xl { - max-width: var(--container-3xl); + .grow { + flex-grow: 1; } - .transform { - transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + .cursor-pointer { + cursor: pointer; + } + .list-inside { + list-style-position: inside; } .list-disc { list-style-type: disc; } + .list-none { + list-style-type: none; + } + .grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .flex-col { + flex-direction: column; + } .flex-wrap { flex-wrap: wrap; } .items-center { align-items: center; } + .items-end { + align-items: flex-end; + } .items-start { align-items: flex-start; } @@ -296,9 +382,6 @@ .justify-center { justify-content: center; } - .gap-1 { - gap: calc(var(--spacing) * 1); - } .gap-2 { gap: calc(var(--spacing) * 2); } @@ -322,6 +405,13 @@ margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-3 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); + } + } .space-y-4 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -329,49 +419,72 @@ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); } } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .overflow-hidden { overflow: hidden; } + .overflow-y-auto { + overflow-y: auto; + } .rounded { border-radius: 0.25rem; } - .rounded-full { - border-radius: calc(infinity * 1px); - } .rounded-lg { border-radius: var(--radius-lg); } - .rounded-md { - border-radius: var(--radius-md); + .rounded-xl { + border-radius: var(--radius-xl); + } + .rounded-t-xl { + border-top-left-radius: var(--radius-xl); + border-top-right-radius: var(--radius-xl); + } + .rounded-b-none { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } - .rounded-sm { - border-radius: var(--radius-sm); + .rounded-b-xl { + border-bottom-right-radius: var(--radius-xl); + border-bottom-left-radius: var(--radius-xl); } .border { border-style: var(--tw-border-style); border-width: 1px; } + .border-x { + border-inline-style: var(--tw-border-style); + border-inline-width: 1px; + } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } .border-slate-700 { border-color: var(--color-slate-700); } + .border-slate-700\/70 { + border-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 70%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-slate-700) 70%, transparent); + } + } .bg-amber-600 { background-color: var(--color-amber-600); } - .bg-black { - background-color: var(--color-black); - } .bg-emerald-600 { background-color: var(--color-emerald-600); } .bg-indigo-600 { background-color: var(--color-indigo-600); } - .bg-orange-100 { - background-color: var(--color-orange-100); - } - .bg-red-500 { - background-color: var(--color-red-500); - } .bg-red-600 { background-color: var(--color-red-600); } @@ -387,29 +500,62 @@ .bg-slate-900 { background-color: var(--color-slate-900); } - .bg-zinc-600 { - background-color: var(--color-zinc-600); + .bg-slate-900\/50 { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 50%, transparent); + } + } + .bg-slate-900\/80 { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 80%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 80%, transparent); + } } .p-0 { padding: calc(var(--spacing) * 0); } + .p-1 { + padding: calc(var(--spacing) * 1); + } .p-2 { padding: calc(var(--spacing) * 2); } + .p-3 { + padding: calc(var(--spacing) * 3); + } .p-4 { padding: calc(var(--spacing) * 4); } - .px-2 { - padding-inline: calc(var(--spacing) * 2); + .px-1 { + padding-inline: calc(var(--spacing) * 1); } .px-3 { padding-inline: calc(var(--spacing) * 3); } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .py-0\.5 { + padding-block: calc(var(--spacing) * 0.5); + } .py-1\.5 { padding-block: calc(var(--spacing) * 1.5); } - .pl-4 { - padding-left: calc(var(--spacing) * 4); + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-4 { + padding-block: calc(var(--spacing) * 4); + } + .pt-2 { + padding-top: calc(var(--spacing) * 2); + } + .text-center { + text-align: center; } .text-right { text-align: right; @@ -441,6 +587,14 @@ font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); } + .leading-5 { + --tw-leading: calc(var(--spacing) * 5); + line-height: calc(var(--spacing) * 5); + } + .leading-none { + --tw-leading: 1; + line-height: 1; + } .font-medium { --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); @@ -449,9 +603,19 @@ --tw-font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold); } + .tracking-wide { + --tw-tracking: var(--tracking-wide); + letter-spacing: var(--tracking-wide); + } + .whitespace-nowrap { + white-space: nowrap; + } .text-amber-300 { color: var(--color-amber-300); } + .text-amber-400 { + color: var(--color-amber-400); + } .text-emerald-300 { color: var(--color-emerald-300); } @@ -470,22 +634,50 @@ .text-slate-400 { color: var(--color-slate-400); } + .text-slate-500 { + color: var(--color-slate-500); + } + .text-white { + color: var(--color-white); + } + .lowercase { + text-transform: lowercase; + } + .uppercase { + text-transform: uppercase; + } .italic { font-style: italic; } .underline { text-decoration-line: underline; } - .shadow-sm { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .backdrop-blur-sm { + --tw-backdrop-blur: blur(var(--blur-sm)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-colors { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); } - .ring-8 { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); } - .ring-lime-400 { - --tw-ring-color: var(--color-lime-400); + .duration-200 { + --tw-duration: 200ms; + transition-duration: 200ms; } .hover\:bg-amber-500 { &:hover { @@ -508,164 +700,227 @@ } } } - .hover\:bg-orange-300 { + .hover\:bg-red-500 { &:hover { @media (hover: hover) { - background-color: var(--color-orange-300); + background-color: var(--color-red-500); } } } - .hover\:bg-red-500 { + .hover\:bg-sky-500 { &:hover { @media (hover: hover) { - background-color: var(--color-red-500); + background-color: var(--color-sky-500); } } } - .hover\:bg-sky-500 { + .hover\:bg-slate-700 { &:hover { @media (hover: hover) { - background-color: var(--color-sky-500); + background-color: var(--color-slate-700); + } + } + } + .hover\:bg-slate-800\/60 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 60%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-800) 60%, transparent); + } } } } - .hover\:bg-zinc-500 { + .hover\:text-slate-200 { &:hover { @media (hover: hover) { - background-color: var(--color-zinc-500); + color: var(--color-slate-200); } } } + .disabled\:cursor-not-allowed { + &:disabled { + cursor: not-allowed; + } + } .disabled\:opacity-40 { &:disabled { opacity: 40%; } } + .has-\[\:checked\]\:border-emerald-600 { + &:has(*:is(:checked)) { + border-color: var(--color-emerald-600); + } + } + .has-\[\:checked\]\:bg-emerald-600 { + &:has(*:is(:checked)) { + background-color: var(--color-emerald-600); + } + } + .sm\:inline { + @media (width >= 40rem) { + display: inline; + } + } + .sm\:h-8 { + @media (width >= 40rem) { + height: calc(var(--spacing) * 8); + } + } + .sm\:text-xl { + @media (width >= 40rem) { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + } } -@property --tw-rotate-x { +@property --tw-space-y-reverse { syntax: "*"; inherits: false; + initial-value: 0; } -@property --tw-rotate-y { +@property --tw-border-style { syntax: "*"; inherits: false; + initial-value: solid; } -@property --tw-rotate-z { +@property --tw-leading { syntax: "*"; inherits: false; } -@property --tw-skew-x { +@property --tw-font-weight { syntax: "*"; inherits: false; } -@property --tw-skew-y { +@property --tw-tracking { syntax: "*"; inherits: false; } -@property --tw-space-y-reverse { +@property --tw-blur { syntax: "*"; inherits: false; - initial-value: 0; } -@property --tw-border-style { +@property --tw-brightness { syntax: "*"; inherits: false; - initial-value: solid; } -@property --tw-font-weight { +@property --tw-contrast { syntax: "*"; inherits: false; } -@property --tw-shadow { +@property --tw-grayscale { syntax: "*"; inherits: false; - initial-value: 0 0 #0000; } -@property --tw-shadow-color { +@property --tw-hue-rotate { syntax: "*"; inherits: false; } -@property --tw-shadow-alpha { - syntax: ""; +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; inherits: false; - initial-value: 100%; } -@property --tw-inset-shadow { +@property --tw-sepia { syntax: "*"; inherits: false; - initial-value: 0 0 #0000; } -@property --tw-inset-shadow-color { +@property --tw-drop-shadow { syntax: "*"; inherits: false; } -@property --tw-inset-shadow-alpha { +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { syntax: ""; inherits: false; initial-value: 100%; } -@property --tw-ring-color { +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { syntax: "*"; inherits: false; } -@property --tw-ring-shadow { +@property --tw-backdrop-contrast { syntax: "*"; inherits: false; - initial-value: 0 0 #0000; } -@property --tw-inset-ring-color { +@property --tw-backdrop-grayscale { syntax: "*"; inherits: false; } -@property --tw-inset-ring-shadow { +@property --tw-backdrop-hue-rotate { syntax: "*"; inherits: false; - initial-value: 0 0 #0000; } -@property --tw-ring-inset { +@property --tw-backdrop-invert { syntax: "*"; inherits: false; } -@property --tw-ring-offset-width { - syntax: ""; +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; inherits: false; - initial-value: 0px; } -@property --tw-ring-offset-color { +@property --tw-backdrop-sepia { syntax: "*"; inherits: false; - initial-value: #fff; } -@property --tw-ring-offset-shadow { +@property --tw-duration { syntax: "*"; inherits: false; - initial-value: 0 0 #0000; } @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { - --tw-rotate-x: initial; - --tw-rotate-y: initial; - --tw-rotate-z: initial; - --tw-skew-x: initial; - --tw-skew-y: initial; --tw-space-y-reverse: 0; --tw-border-style: solid; + --tw-leading: initial; --tw-font-weight: initial; - --tw-shadow: 0 0 #0000; - --tw-shadow-color: initial; - --tw-shadow-alpha: 100%; - --tw-inset-shadow: 0 0 #0000; - --tw-inset-shadow-color: initial; - --tw-inset-shadow-alpha: 100%; - --tw-ring-color: initial; - --tw-ring-shadow: 0 0 #0000; - --tw-inset-ring-color: initial; - --tw-inset-ring-shadow: 0 0 #0000; - --tw-ring-inset: initial; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-offset-shadow: 0 0 #0000; + --tw-tracking: initial; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + --tw-duration: initial; } } } diff --git a/content/wardrive.js b/content/wardrive.js index 9aee5e6..34a1983 100644 --- a/content/wardrive.js +++ b/content/wardrive.js @@ -88,6 +88,16 @@ const WARDIVE_IATA_CODE = "YOW"; // For DEV builds: Contains "DEV-" format (e.g., "DEV-1734652800") const APP_VERSION = "UNKNOWN"; // Placeholder - replaced during build +// ---- UI helpers ---- +// Status colors for different states +const STATUS_COLORS = { + idle: "text-slate-300", + success: "text-emerald-300", + warning: "text-amber-300", + error: "text-red-300", + info: "text-sky-300" +}; + // ---- DOM refs (from index.html; unchanged except the two new selectors) ---- const $ = (id) => document.getElementById(id); const statusEl = $("status"); @@ -103,11 +113,27 @@ const distanceInfoEl = document.getElementById("distanceInfo"); // Distance from const sessionPingsEl = document.getElementById("sessionPings"); // optional const coverageFrameEl = document.getElementById("coverageFrame"); setConnectButton(false); +setConnStatus("Disconnected", STATUS_COLORS.error); // NEW: selectors const intervalSelect = $("intervalSelect"); // 15 / 30 / 60 seconds const powerSelect = $("powerSelect"); // "", "0.3w", "0.6w", "1.0w" +// Session Log selectors +const logSummaryBar = $("logSummaryBar"); +const logBottomSheet = $("logBottomSheet"); +const logScrollContainer = $("logScrollContainer"); +const logCount = $("logCount"); +const logLastTime = $("logLastTime"); +const logLastSnr = $("logLastSnr"); + +// Session log state +const sessionLogState = { + entries: [], // Array of parsed log entries + isExpanded: false, + autoScroll: true +}; + // ---- State ---- const state = { connection: null, @@ -132,7 +158,8 @@ const state = { distanceUpdateTimer: null, // Timer for updating distance display capturedPingCoords: null, // { lat, lon, accuracy } captured at ping time, used for API post after 7s delay devicePublicKey: null, // Hex string of device's public key (used for capacity check) - disconnectReason: null, // Tracks the reason for disconnection (e.g., "app_down", "capacity_full", "public_key_error", "channel_setup_error", "ble_disconnect_error", "normal") + wardriveSessionId: null, // Session ID from capacity check API (used for all MeshMapper API posts) + disconnectReason: null, // Tracks the reason for disconnection (e.g., "app_down", "capacity_full", "public_key_error", "channel_setup_error", "ble_disconnect_error", "session_id_error", "normal") channelSetupErrorMessage: null, // Error message from channel setup failure bleDisconnectErrorMessage: null, // Error message from BLE disconnect failure repeaterTracking: { @@ -143,19 +170,10 @@ const state = { repeaters: new Map(), // Map listenTimeout: null, // Timeout handle for 7-second window rxLogHandler: null, // Handler function for rx_log events + currentLogEntry: null, // Current log entry being updated (for incremental UI updates) } }; -// ---- UI helpers ---- -// Status colors for different states -const STATUS_COLORS = { - idle: "text-slate-300", - success: "text-emerald-300", - warning: "text-amber-300", - error: "text-red-300", - info: "text-sky-300" -}; - // Status message management with minimum visibility duration const MIN_STATUS_VISIBILITY_MS = 500; // Minimum time a status message must remain visible const statusMessageState = { @@ -228,7 +246,7 @@ function setStatus(text, color = STATUS_COLORS.idle, immediate = false) { */ function applyStatusImmediately(text, color) { statusEl.textContent = text; - statusEl.className = `font-semibold ${color}`; + statusEl.className = `text-sm font-medium ${color}`; statusMessageState.lastSetTime = Date.now(); statusMessageState.currentText = text; statusMessageState.currentColor = color; @@ -244,9 +262,9 @@ function applyStatusImmediately(text, color) { function applyCountdownStatus(result, defaultColor, immediate = true) { if (!result) return; if (typeof result === 'string') { - setStatus(result, defaultColor, immediate); + setDynamicStatus(result, defaultColor, immediate); } else { - setStatus(result.message, result.color || defaultColor, immediate); + setDynamicStatus(result.message, result.color || defaultColor, immediate); } } @@ -391,6 +409,28 @@ function resumeAutoCountdown() { return false; } +/** + * Handle manual ping blocked during auto mode by resuming the paused countdown + * This ensures the UI returns to showing the auto countdown instead of staying stuck on the skip message + * + * When a manual ping is blocked during auto mode (GPS unavailable, outside geofence, or too close), this function: + * 1. Attempts to resume the paused auto countdown timer with remaining time + * 2. If no paused countdown exists, schedules a new auto ping + * 3. Does nothing if auto mode is not running + * + * @returns {void} + */ +function handleManualPingBlockedDuringAutoMode() { + if (state.running) { + debugLog("Manual ping blocked during auto mode - resuming auto countdown"); + const resumed = resumeAutoCountdown(); + if (!resumed) { + debugLog("No paused countdown to resume, scheduling new auto ping"); + scheduleNextAutoPing(); + } + } +} + function startRxListeningCountdown(delayMs) { debugLog(`Starting RX listening countdown: ${delayMs}ms`); state.rxListeningEndTime = Date.now() + delayMs; @@ -482,12 +522,19 @@ function cleanupAllTimers() { // Clear device public key state.devicePublicKey = null; + + // Clear wardrive session ID + state.wardriveSessionId = null; } function enableControls(connected) { connectBtn.disabled = false; channelInfoEl.textContent = CHANNEL_NAME; updateControlsForCooldown(); + + // Keep ping controls always visible but disable when not connected + // This is handled by updateControlsForCooldown() which sets disabled state + // No need to show/hide the controls anymore } function updateAutoButton() { if (state.running) { @@ -542,6 +589,64 @@ function setConnectButton(connected) { } } +/** + * Set connection status bar message + * Updates the #connectionStatus element with one of four fixed states: + * - "Connected" - Device ready for wardriving after full connection (green) + * - "Connecting" - Connection process in progress (blue) + * - "Disconnected" - No device connected (red) + * - "Disconnecting" - Disconnection process in progress (blue) + * + * @param {string} text - Connection status text (one of the four states above) + * @param {string} color - Status color class from STATUS_COLORS + */ +function setConnStatus(text, color) { + const connectionStatusEl = document.getElementById("connectionStatus"); + const statusIndicatorEl = document.getElementById("statusIndicator"); + + if (!connectionStatusEl) return; + + debugLog(`Connection status: "${text}"`); + connectionStatusEl.textContent = text; + connectionStatusEl.className = `font-medium ${color}`; + + // Update status indicator dot color to match + if (statusIndicatorEl) { + statusIndicatorEl.className = `text-lg ${color}`; + } +} + +/** + * Set dynamic status bar message + * Updates the #status element with non-connection status messages. + * Uses 500ms minimum visibility for first display, immediate for countdown updates. + * + * Connection status words (Connected/Connecting/Disconnecting/Disconnected) are blocked + * and replaced with em dash (—) placeholder. + * + * @param {string} text - Status message text (null/empty shows "—") + * @param {string} color - Status color class from STATUS_COLORS + * @param {boolean} immediate - If true, bypass minimum visibility (for countdown timers) + */ +function setDynamicStatus(text, color = STATUS_COLORS.idle, immediate = false) { + // Normalize empty/null/whitespace to em dash + if (!text || text.trim() === '') { + text = '—'; + color = STATUS_COLORS.idle; + } + + // Block connection words from dynamic bar + const connectionWords = ['Connected', 'Connecting', 'Disconnecting', 'Disconnected']; + if (connectionWords.includes(text)) { + debugWarn(`Attempted to show connection word "${text}" in dynamic status bar - blocked, showing em dash instead`); + text = '—'; + color = STATUS_COLORS.idle; + } + + // Reuse existing setStatus implementation with minimum visibility + setStatus(text, color, immediate); +} + // ---- Wake Lock helpers ---- @@ -690,7 +795,7 @@ function updateDistanceUi() { if (distance === null) { distanceInfoEl.textContent = "-"; } else { - distanceInfoEl.textContent = `${Math.round(distance)} m`; + distanceInfoEl.textContent = `∆${Math.round(distance)}m`; } } @@ -741,7 +846,8 @@ function updateGpsUi() { gpsInfoEl.textContent = "Acquiring GPS fix..."; gpsAccEl.textContent = "Please wait"; } else if (state.gpsState === "error") { - gpsInfoEl.textContent = "GPS error - check permissions"; + // GPS errors are now shown in Dynamic Status Bar, not in GPS block + gpsInfoEl.textContent = "-"; gpsAccEl.textContent = "-"; } else { gpsInfoEl.textContent = "-"; @@ -755,7 +861,7 @@ function updateGpsUi() { state.gpsState = "acquired"; gpsInfoEl.textContent = `${lat.toFixed(5)}, ${lon.toFixed(5)} (${ageSec}s ago)`; - gpsAccEl.textContent = accM ? `±${Math.round(accM)} m` : "-"; + gpsAccEl.textContent = accM ? `±${Math.round(accM)}m` : "-"; } // Start continuous GPS age display updates @@ -805,6 +911,8 @@ function startGeoWatch() { (err) => { debugError(`GPS watch error: ${err.code} - ${err.message}`); state.gpsState = "error"; + // Display GPS error in Dynamic Status Bar + setDynamicStatus("GPS error - check permissions", STATUS_COLORS.error); // Keep UI honest if it fails updateGpsUi(); }, @@ -862,6 +970,8 @@ async function primeGpsOnce() { } catch (e) { debugError(`primeGpsOnce failed: ${e.message}`); state.gpsState = "error"; + // Display GPS error in Dynamic Status Bar + setDynamicStatus("GPS error - check permissions", STATUS_COLORS.error); updateGpsUi(); } } @@ -970,16 +1080,16 @@ async function ensureChannel() { return state.channel; } - setStatus("Looking for #wardriving channel", STATUS_COLORS.info); + setDynamicStatus("Looking for #wardriving channel", STATUS_COLORS.info); debugLog(`Looking up channel: ${CHANNEL_NAME}`); let ch = await state.connection.findChannelByName(CHANNEL_NAME); if (!ch) { - setStatus("Channel #wardriving not found", STATUS_COLORS.info); + setDynamicStatus("Channel #wardriving not found", STATUS_COLORS.info); debugLog(`Channel ${CHANNEL_NAME} not found, attempting to create it`); try { ch = await createWardriveChannel(); - setStatus("Created #wardriving", STATUS_COLORS.success); + setDynamicStatus("Created #wardriving", STATUS_COLORS.success); debugLog(`Channel ${CHANNEL_NAME} created successfully`); } catch (e) { debugError(`Failed to create channel ${CHANNEL_NAME}: ${e.message}`); @@ -989,7 +1099,7 @@ async function ensureChannel() { ); } } else { - setStatus("Channel #wardriving found", STATUS_COLORS.success); + setDynamicStatus("Channel #wardriving found", STATUS_COLORS.success); debugLog(`Channel found: ${CHANNEL_NAME} (index: ${ch.channelIdx})`); } @@ -1053,7 +1163,7 @@ async function checkCapacity(reason) { // Set status for connect requests if (reason === "connect") { - setStatus("Acquiring wardriving slot", STATUS_COLORS.info); + setDynamicStatus("Acquiring wardriving slot", STATUS_COLORS.info); } try { @@ -1084,11 +1194,33 @@ async function checkCapacity(reason) { } const data = await response.json(); - debugLog(`Capacity check response: allowed=${data.allowed}`); + debugLog(`Capacity check response: allowed=${data.allowed}, session_id=${data.session_id || 'missing'}`); // Handle capacity full vs. allowed cases separately if (data.allowed === false && reason === "connect") { state.disconnectReason = "capacity_full"; // Track disconnect reason + return false; + } + + // For connect requests, validate session_id is present when allowed === true + if (reason === "connect" && data.allowed === true) { + if (!data.session_id) { + debugError("Capacity check returned allowed=true but session_id is missing"); + state.disconnectReason = "session_id_error"; // Track disconnect reason + return false; + } + + // Store the session_id for use in MeshMapper API posts + state.wardriveSessionId = data.session_id; + debugLog(`Wardrive session ID received and stored: ${state.wardriveSessionId}`); + } + + // For disconnect requests, clear the session_id + if (reason === "disconnect") { + if (state.wardriveSessionId) { + debugLog(`Clearing wardrive session ID on disconnect: ${state.wardriveSessionId}`); + state.wardriveSessionId = null; + } } return data.allowed === true; @@ -1115,6 +1247,18 @@ async function checkCapacity(reason) { */ async function postToMeshMapperAPI(lat, lon, heardRepeats) { try { + // Validate session_id exists before posting + if (!state.wardriveSessionId) { + debugError("Cannot post to MeshMapper API: no session_id available"); + setDynamicStatus("Error: No session ID for API post", STATUS_COLORS.error); + state.disconnectReason = "session_id_error"; // Track disconnect reason + // Disconnect after a brief delay to ensure user sees the error message + setTimeout(() => { + disconnect().catch(err => debugError(`Disconnect after missing session_id failed: ${err.message}`)); + }, 1500); + return; // Exit early + } + const payload = { key: MESHMAPPER_API_KEY, lat, @@ -1124,10 +1268,11 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { heard_repeats: heardRepeats, ver: APP_VERSION, test: 0, - iata: WARDIVE_IATA_CODE + iata: WARDIVE_IATA_CODE, + session_id: state.wardriveSessionId }; - debugLog(`Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}, heard_repeats=${heardRepeats}, ver=${payload.ver}, iata=${payload.iata}`); + debugLog(`Posting to MeshMapper API: lat=${lat.toFixed(5)}, lon=${lon.toFixed(5)}, who=${payload.who}, power=${payload.power}, heard_repeats=${heardRepeats}, ver=${payload.ver}, iata=${payload.iata}, session_id=${payload.session_id}`); const response = await fetch(MESHMAPPER_API_URL, { method: "POST", @@ -1146,7 +1291,7 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { // Check if slot has been revoked if (data.allowed === false) { debugWarn("MeshMapper API returned allowed=false, WarDriving slot has been revoked, disconnecting"); - setStatus("Error: Posting to API (Revoked)", STATUS_COLORS.error); + setDynamicStatus("Error: Posting to API (Revoked)", STATUS_COLORS.error); state.disconnectReason = "slot_revoked"; // Track disconnect reason // Disconnect after a brief delay to ensure user sees the error message setTimeout(() => { @@ -1185,7 +1330,7 @@ async function postToMeshMapperAPI(lat, lon, heardRepeats) { async function postApiAndRefreshMap(lat, lon, accuracy, heardRepeats) { debugLog(`postApiAndRefreshMap called with heard_repeats="${heardRepeats}"`); - setStatus("Posting to API", STATUS_COLORS.info); + setDynamicStatus("Posting to API", STATUS_COLORS.info); // Hidden 3-second delay before API POST (user sees "Posting to API" status during this time) await new Promise(resolve => setTimeout(resolve, 3000)); @@ -1223,8 +1368,8 @@ async function postApiAndRefreshMap(lat, lon, accuracy, heardRepeats) { debugLog("Resumed auto countdown after manual ping"); } } else { - debugLog("Setting status to idle"); - setStatus("Idle", STATUS_COLORS.idle); + debugLog("Setting dynamic status to em dash"); + setDynamicStatus("Idle"); } } }, MAP_REFRESH_DELAY_MS); @@ -1523,6 +1668,9 @@ async function handleRxLogEvent(data, originalPayload, channelIdx, expectedChann snr: data.lastSnr, seenCount: existing.seenCount + 1 }); + + // Trigger incremental UI update since SNR changed + updateCurrentLogEntryWithLiveRepeaters(); } else { debugLog(`Deduplication decision: keeping existing SNR for path ${pathHex} (existing ${existing.snr} >= new ${data.lastSnr})`); // Still increment seen count @@ -1535,6 +1683,9 @@ async function handleRxLogEvent(data, originalPayload, channelIdx, expectedChann snr: data.lastSnr, seenCount: 1 }); + + // Trigger incremental UI update for the new repeater + updateCurrentLogEntryWithLiveRepeaters(); } } catch (error) { debugError(`Error processing rx_log entry: ${error.message}`, error); @@ -1581,6 +1732,7 @@ function stopRepeaterTracking() { state.repeaterTracking.sentPayload = null; state.repeaterTracking.repeaters.clear(); state.repeaterTracking.rxLogHandler = null; + state.repeaterTracking.currentLogEntry = null; return repeaters; } @@ -1600,6 +1752,255 @@ function formatRepeaterTelemetry(repeaters) { return repeaters.map(r => `${r.repeaterId}(${r.snr})`).join(','); } +// ---- Mobile Session Log Bottom Sheet ---- + +/** + * Parse log entry string into structured data + * @param {string} logLine - Log line in format "timestamp | lat,lon | events" + * @returns {Object} Parsed log entry with timestamp, coords, and events + */ +function parseLogEntry(logLine) { + const parts = logLine.split(' | '); + if (parts.length !== 3) { + return null; + } + + const [timestamp, coords, eventsStr] = parts; + const [lat, lon] = coords.split(',').map(s => s.trim()); + + // Parse events: "4e(12),b7(0)" or "None" + const events = []; + if (eventsStr && eventsStr !== 'None' && eventsStr !== '...') { + const eventTokens = eventsStr.split(','); + for (const token of eventTokens) { + const match = token.match(/^([a-f0-9]+)\(([^)]+)\)$/i); + if (match) { + events.push({ + type: match[1], + value: parseFloat(match[2]) + }); + } + } + } + + return { + timestamp, + lat, + lon, + events + }; +} + +/** + * Get SNR severity class based on value + * Red: -12 to -1 + * Orange: 0 to 5 + * Green: 6 to 13+ + * @param {number} snr - SNR value + * @returns {string} CSS class name + */ +function getSnrSeverityClass(snr) { + if (snr <= -1) { + return 'snr-red'; + } else if (snr <= 5) { + return 'snr-orange'; + } else { + return 'snr-green'; + } +} + +/** + * Create chip element for a heard repeat + * @param {string} type - Event type (repeater ID) + * @param {number} value - SNR value + * @returns {HTMLElement} Chip element + */ +function createChipElement(type, value) { + const chip = document.createElement('span'); + chip.className = `chip ${getSnrSeverityClass(value)}`; + + const idSpan = document.createElement('span'); + idSpan.className = 'chipId'; + idSpan.textContent = type; + + const snrSpan = document.createElement('span'); + snrSpan.className = 'chipSnr'; + snrSpan.textContent = `${value.toFixed(2)} dB`; + + chip.appendChild(idSpan); + chip.appendChild(snrSpan); + + return chip; +} + +/** + * Create log entry element for mobile view + * @param {Object} entry - Parsed log entry + * @returns {HTMLElement} Log entry element + */ +function createLogEntryElement(entry) { + debugLog(`Creating log entry element for timestamp: ${entry.timestamp}`); + const logEntry = document.createElement('div'); + logEntry.className = 'logEntry'; + + // Top row: time + coords + const topRow = document.createElement('div'); + topRow.className = 'logRowTop'; + + const time = document.createElement('span'); + time.className = 'logTime'; + // Format timestamp to show only time (HH:MM:SS) + const date = new Date(entry.timestamp); + time.textContent = date.toLocaleTimeString(); + + const coords = document.createElement('span'); + coords.className = 'logCoords'; + coords.textContent = `${entry.lat},${entry.lon}`; + + topRow.appendChild(time); + topRow.appendChild(coords); + + // Chips row: heard repeats + const chipsRow = document.createElement('div'); + chipsRow.className = 'heardChips'; + + if (entry.events.length === 0) { + const noneSpan = document.createElement('span'); + noneSpan.className = 'text-xs text-slate-500 italic'; + noneSpan.textContent = 'No repeats heard'; + chipsRow.appendChild(noneSpan); + debugLog(`Log entry has no events (no repeats heard)`); + } else { + debugLog(`Log entry has ${entry.events.length} event(s)`); + entry.events.forEach(event => { + const chip = createChipElement(event.type, event.value); + chipsRow.appendChild(chip); + debugLog(`Added chip for repeater ${event.type} with SNR ${event.value} dB`); + }); + } + + logEntry.appendChild(topRow); + logEntry.appendChild(chipsRow); + + debugLog(`Log entry element created successfully with class: ${logEntry.className}`); + return logEntry; +} + +/** + * Update summary bar with latest log data + */ +function updateLogSummary() { + if (!logCount || !logLastTime || !logLastSnr) return; + + const count = sessionLogState.entries.length; + logCount.textContent = count === 1 ? '1 ping' : `${count} pings`; + + if (count === 0) { + logLastTime.textContent = 'No data'; + logLastSnr.textContent = '—'; + debugLog('Session log summary updated: no entries'); + return; + } + + const lastEntry = sessionLogState.entries[count - 1]; + const date = new Date(lastEntry.timestamp); + logLastTime.textContent = date.toLocaleTimeString(); + + // Count total heard repeats in the latest ping + const heardCount = lastEntry.events.length; + debugLog(`Session log summary updated: ${count} total pings, latest ping heard ${heardCount} repeats`); + + if (heardCount > 0) { + logLastSnr.textContent = heardCount === 1 ? '1 Repeat' : `${heardCount} Repeats`; + logLastSnr.className = 'text-xs font-mono text-slate-300'; + } else { + logLastSnr.textContent = '0 Repeats'; + logLastSnr.className = 'text-xs font-mono text-slate-500'; + } +} + +/** + * Render all log entries to the session log + */ +function renderLogEntries() { + if (!sessionPingsEl) return; + + debugLog(`Rendering ${sessionLogState.entries.length} log entries`); + sessionPingsEl.innerHTML = ''; + + if (sessionLogState.entries.length === 0) { + // Show placeholder when no entries + const placeholder = document.createElement('div'); + placeholder.className = 'text-xs text-slate-500 italic text-center py-4'; + placeholder.textContent = 'No pings logged yet'; + sessionPingsEl.appendChild(placeholder); + debugLog(`Rendered placeholder (no entries)`); + return; + } + + // Render newest first + const entries = [...sessionLogState.entries].reverse(); + + entries.forEach((entry, index) => { + const element = createLogEntryElement(entry); + sessionPingsEl.appendChild(element); + debugLog(`Appended log entry ${index + 1}/${entries.length} to sessionPingsEl`); + }); + + // Auto-scroll to top (newest) + if (sessionLogState.autoScroll && logScrollContainer) { + logScrollContainer.scrollTop = 0; + debugLog(`Auto-scrolled to top of log container`); + } + + debugLog(`Finished rendering all log entries`); +} + +/** + * Toggle session log expanded/collapsed + */ +function toggleBottomSheet() { + sessionLogState.isExpanded = !sessionLogState.isExpanded; + + if (logBottomSheet) { + if (sessionLogState.isExpanded) { + logBottomSheet.classList.add('open'); + logBottomSheet.classList.remove('hidden'); + } else { + logBottomSheet.classList.remove('open'); + logBottomSheet.classList.add('hidden'); + } + } + + // Toggle arrow rotation + const logExpandArrow = document.getElementById('logExpandArrow'); + if (logExpandArrow) { + if (sessionLogState.isExpanded) { + logExpandArrow.classList.add('expanded'); + } else { + logExpandArrow.classList.remove('expanded'); + } + } +} + +/** + * Add entry to session log + * @param {string} timestamp - ISO timestamp + * @param {string} lat - Latitude + * @param {string} lon - Longitude + * @param {string} eventsStr - Events string (e.g., "4e(12),b7(0)" or "None") + */ +function addLogEntry(timestamp, lat, lon, eventsStr) { + const logLine = `${timestamp} | ${lat},${lon} | ${eventsStr}`; + const entry = parseLogEntry(logLine); + + if (entry) { + sessionLogState.entries.push(entry); + renderLogEntries(); + updateLogSummary(); + } +} + // ---- Ping ---- /** * Acquire fresh GPS coordinates and update state @@ -1636,7 +2037,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { // Auto mode: validate GPS freshness before sending if (!state.lastFix) { debugWarn("Auto ping skipped: no GPS fix available yet"); - setStatus("Waiting for GPS fix", STATUS_COLORS.warning); + setDynamicStatus("Waiting for GPS fix", STATUS_COLORS.warning); return null; } @@ -1647,7 +2048,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { if (ageMs >= maxAge) { debugLog(`GPS data too old for auto ping (${ageMs}ms), attempting to refresh`); - setStatus("GPS data too old, requesting fresh position", STATUS_COLORS.warning); + setDynamicStatus("GPS data too old, requesting fresh position", STATUS_COLORS.warning); try { return await acquireFreshGpsPosition(); @@ -1700,7 +2101,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { // Data exists but is too old debugLog(`GPS data too old (${ageMs}ms), requesting fresh position`); - setStatus("GPS data too old, requesting fresh position", STATUS_COLORS.warning); + setDynamicStatus("GPS data too old, requesting fresh position", STATUS_COLORS.warning); } // Get fresh GPS coordinates for manual ping @@ -1720,7 +2121,7 @@ async function getGpsCoordinatesForPing(isAutoMode) { * @param {string} payload - The ping message * @param {number} lat - Latitude * @param {number} lon - Longitude - * @returns {HTMLElement|null} The list item element for later updates, or null + * @returns {Object|null} The log entry object for later updates, or null */ function logPingToUI(payload, lat, lon) { // Use ISO format for data storage but user-friendly format for display @@ -1731,43 +2132,77 @@ function logPingToUI(payload, lat, lon) { lastPingEl.textContent = `${now.toLocaleString()} — ${payload}`; } - if (sessionPingsEl) { - // Create log entry with placeholder for repeater data - // Format: timestamp | lat,lon | repeaters (using ISO for consistency with requirements) - const line = `${isoStr} | ${lat.toFixed(5)},${lon.toFixed(5)} | ...`; - const li = document.createElement('li'); - li.textContent = line; - li.setAttribute('data-timestamp', isoStr); - li.setAttribute('data-lat', lat.toFixed(5)); - li.setAttribute('data-lon', lon.toFixed(5)); - sessionPingsEl.appendChild(li); - // Auto-scroll to bottom - sessionPingsEl.scrollTop = sessionPingsEl.scrollHeight; - return li; - } + // Create log entry with placeholder for repeater data + const logData = { + timestamp: isoStr, + lat: lat.toFixed(5), + lon: lon.toFixed(5), + eventsStr: '...' + }; + + // Add to session log (this will handle both mobile and desktop) + addLogEntry(logData.timestamp, logData.lat, logData.lon, logData.eventsStr); - return null; + return logData; } /** * Update a ping log entry with repeater telemetry - * @param {HTMLElement|null} logEntry - The log entry element to update + * @param {Object|null} logData - The log data object to update * @param {Array<{repeaterId: string, snr: number}>} repeaters - Array of repeater telemetry */ -function updatePingLogWithRepeaters(logEntry, repeaters) { - if (!logEntry) return; +function updatePingLogWithRepeaters(logData, repeaters) { + if (!logData) return; - const timestamp = logEntry.getAttribute('data-timestamp'); - const lat = logEntry.getAttribute('data-lat'); - const lon = logEntry.getAttribute('data-lon'); const repeaterStr = formatRepeaterTelemetry(repeaters); - // Update the log entry with final repeater data - logEntry.textContent = `${timestamp} | ${lat},${lon} | ${repeaterStr}`; + // Find and update the entry in sessionLogState + const entryIndex = sessionLogState.entries.findIndex( + e => e.timestamp === logData.timestamp && e.lat === logData.lat && e.lon === logData.lon + ); + + if (entryIndex !== -1) { + // Update the entry + const logLine = `${logData.timestamp} | ${logData.lat},${logData.lon} | ${repeaterStr}`; + const updatedEntry = parseLogEntry(logLine); + + if (updatedEntry) { + sessionLogState.entries[entryIndex] = updatedEntry; + renderLogEntries(); + updateLogSummary(); + } + } debugLog(`Updated ping log entry with repeater telemetry: ${repeaterStr}`); } +/** + * Incrementally update the current ping log entry as repeaters are detected + * This provides real-time updates during the RX listening window + */ +function updateCurrentLogEntryWithLiveRepeaters() { + // Only update if we're actively listening and have a current log entry + if (!state.repeaterTracking.isListening || !state.repeaterTracking.currentLogEntry) { + return; + } + + const logData = state.repeaterTracking.currentLogEntry; + + // Convert current repeaters Map to array format + const repeaters = Array.from(state.repeaterTracking.repeaters.entries()).map(([id, data]) => ({ + repeaterId: id, + snr: data.snr + })); + + // Sort by repeater ID for deterministic output + repeaters.sort((a, b) => a.repeaterId.localeCompare(b.repeaterId)); + + // Reuse the existing updatePingLogWithRepeaters function + updatePingLogWithRepeaters(logData, repeaters); + + debugLog(`Incrementally updated ping log entry: ${repeaters.length} repeater(s) detected so far`); +} + /** * Send a wardrive ping with current GPS coordinates * @param {boolean} manual - Whether this is a manual ping (true) or auto ping (false) @@ -1779,7 +2214,7 @@ async function sendPing(manual = false) { if (manual && isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); debugLog(`Manual ping blocked by cooldown (${remainingSec}s remaining)`); - setStatus(`Wait ${remainingSec}s before sending another ping`, STATUS_COLORS.warning); + setDynamicStatus(`Wait ${remainingSec}s before sending another ping`, STATUS_COLORS.warning); return; } @@ -1788,14 +2223,14 @@ async function sendPing(manual = false) { // Manual ping during auto mode: pause the auto countdown debugLog("Manual ping during auto mode - pausing auto countdown"); pauseAutoCountdown(); - setStatus("Sending manual ping", STATUS_COLORS.info); + setDynamicStatus("Sending manual ping", STATUS_COLORS.info); } else if (!manual && state.running) { // Auto ping: stop the countdown timer to avoid status conflicts stopAutoCountdown(); - setStatus("Sending auto ping", STATUS_COLORS.info); + setDynamicStatus("Sending auto ping", STATUS_COLORS.info); } else if (manual) { // Manual ping when auto is not running - setStatus("Sending manual ping", STATUS_COLORS.info); + setDynamicStatus("Sending manual ping", STATUS_COLORS.info); } // Get GPS coordinates @@ -1806,6 +2241,10 @@ async function sendPing(manual = false) { if (!manual && state.running) { scheduleNextAutoPing(); } + // For manual ping during auto mode, resume the paused countdown + if (manual) { + handleManualPingBlockedDuringAutoMode(); + } return; } @@ -1821,7 +2260,9 @@ async function sendPing(manual = false) { if (manual) { // Manual ping: show skip message that persists - setStatus("Ping skipped, outside of geofenced region", STATUS_COLORS.warning); + setDynamicStatus("Ping skipped, outside of geofenced region", STATUS_COLORS.warning); + // If auto mode is running, resume the paused countdown + handleManualPingBlockedDuringAutoMode(); } else if (state.running) { // Auto ping: schedule next ping and show countdown with skip message scheduleNextAutoPing(); @@ -1841,7 +2282,9 @@ async function sendPing(manual = false) { if (manual) { // Manual ping: show skip message that persists - setStatus("Ping skipped, too close to last ping", STATUS_COLORS.warning); + setDynamicStatus("Ping skipped, too close to last ping", STATUS_COLORS.warning); + // If auto mode is running, resume the paused countdown + handleManualPingBlockedDuringAutoMode(); } else if (state.running) { // Auto ping: schedule next ping and show countdown with skip message scheduleNextAutoPing(); @@ -1887,11 +2330,14 @@ async function sendPing(manual = false) { startCooldown(); // Update status after ping is sent - setStatus("Ping sent", STATUS_COLORS.success); + setDynamicStatus("Ping sent", STATUS_COLORS.success); // Create UI log entry with placeholder for repeater data const logEntry = logPingToUI(payload, lat, lon); + // Store log entry in repeater tracking state for incremental updates + state.repeaterTracking.currentLogEntry = logEntry; + // Start RX listening countdown // The minimum 500ms visibility of "Ping sent" is enforced by setStatus() if (state.connection) { @@ -1901,6 +2347,8 @@ async function sendPing(manual = false) { // Schedule the sequence: listen for 7s, THEN finalize repeats and post to API // This timeout is stored in meshMapperTimer for cleanup purposes + // Capture coordinates locally to prevent race conditions with concurrent pings + const capturedCoords = state.capturedPingCoords; state.meshMapperTimer = setTimeout(async () => { debugLog(`RX listening window completed after ${RX_LOG_LISTEN_WINDOW_MS}ms`); @@ -1919,8 +2367,8 @@ async function sendPing(manual = false) { debugLog(`Formatted heard_repeats for API: "${heardRepeatsStr}"`); // Use captured coordinates for API post (not current GPS position) - if (state.capturedPingCoords) { - const { lat: apiLat, lon: apiLon, accuracy: apiAccuracy } = state.capturedPingCoords; + if (capturedCoords) { + const { lat: apiLat, lon: apiLon, accuracy: apiAccuracy } = capturedCoords; debugLog(`Using captured ping coordinates for API post: lat=${apiLat.toFixed(5)}, lon=${apiLon.toFixed(5)}, accuracy=${apiAccuracy}m`); // Post to API with heard repeats data @@ -1932,12 +2380,14 @@ async function sendPing(manual = false) { // Unlock ping controls since API post is being skipped unlockPingControls("after skipping API post due to missing coordinates"); + + // Fix 2: Schedule next auto ping if in auto mode to prevent getting stuck + if (state.running && !state.autoTimerId) { + debugLog("Scheduling next auto ping after skipped API post"); + scheduleNextAutoPing(); + } } - // Clear captured coordinates after API post completes (always, regardless of path) - state.capturedPingCoords = null; - debugLog(`Cleared captured ping coordinates after API post`); - // Clear timer reference state.meshMapperTimer = null; }, RX_LOG_LISTEN_WINDOW_MS); @@ -1946,7 +2396,7 @@ async function sendPing(manual = false) { updateDistanceUi(); } catch (e) { debugError(`Ping operation failed: ${e.message}`, e); - setStatus(e.message || "Ping failed", STATUS_COLORS.error); + setDynamicStatus(e.message || "Ping failed", STATUS_COLORS.error); // Unlock ping controls on error unlockPingControls("after error"); @@ -1960,7 +2410,7 @@ function stopAutoPing(stopGps = false) { if (!stopGps && isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); debugLog(`Auto ping stop blocked by cooldown (${remainingSec}s remaining)`); - setStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); + setDynamicStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } @@ -2020,7 +2470,7 @@ function startAutoPing() { if (isInCooldown()) { const remainingSec = getRemainingCooldownSeconds(); debugLog(`Auto ping start blocked by cooldown (${remainingSec}s remaining)`); - setStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); + setDynamicStatus(`Wait ${remainingSec}s before toggling auto mode`, STATUS_COLORS.warning); return; } @@ -2060,10 +2510,14 @@ async function connect() { return; } connectBtn.disabled = true; - setStatus("Connecting", STATUS_COLORS.info); + + // Set connection bar to "Connecting" - will remain until GPS init completes + setConnStatus("Connecting", STATUS_COLORS.info); + setDynamicStatus("Idle"); // Clear dynamic status try { debugLog("Opening BLE connection..."); + setDynamicStatus("BLE Connection Started", STATUS_COLORS.info); // Show BLE connection start const conn = await WebBleConnection.open(); state.connection = conn; debugLog("BLE connection object created"); @@ -2116,19 +2570,20 @@ async function connect() { } // Capacity check passed - setStatus("Acquired wardriving slot", STATUS_COLORS.success); + setDynamicStatus("Acquired wardriving slot", STATUS_COLORS.success); debugLog("Wardriving slot acquired successfully"); // Proceed with channel setup and GPS initialization await ensureChannel(); // GPS initialization - setStatus("Priming GPS", STATUS_COLORS.info); + setDynamicStatus("Priming GPS", STATUS_COLORS.info); debugLog("Starting GPS initialization"); await primeGpsOnce(); - // Connection complete, show Connected status - setStatus("Connected", STATUS_COLORS.success); + // Connection complete, show Connected status in connection bar + setConnStatus("Connected", STATUS_COLORS.success); + setDynamicStatus("Idle"); // Clear dynamic status to em dash debugLog("Full connection process completed successfully"); } catch (e) { debugError(`Channel setup failed: ${e.message}`, e); @@ -2141,43 +2596,50 @@ async function connect() { debugLog("BLE disconnected event fired"); debugLog(`Disconnect reason: ${state.disconnectReason}`); - // Set appropriate status message based on disconnect reason + // Always set connection bar to "Disconnected" + setConnStatus("Disconnected", STATUS_COLORS.error); + + // Set dynamic status based on disconnect reason (WITHOUT "Disconnected:" prefix) if (state.disconnectReason === "capacity_full") { debugLog("Branch: capacity_full"); - setStatus("Disconnected: WarDriving app has reached capacity", STATUS_COLORS.error, true); + setDynamicStatus("WarDriving app has reached capacity", STATUS_COLORS.error, true); debugLog("Setting terminal status for capacity full"); } else if (state.disconnectReason === "app_down") { debugLog("Branch: app_down"); - setStatus("Disconnected: WarDriving app is down", STATUS_COLORS.error, true); + setDynamicStatus("WarDriving app is down", STATUS_COLORS.error, true); debugLog("Setting terminal status for app down"); } else if (state.disconnectReason === "slot_revoked") { debugLog("Branch: slot_revoked"); - setStatus("Disconnected: WarDriving slot has been revoked", STATUS_COLORS.error, true); + setDynamicStatus("WarDriving slot has been revoked", STATUS_COLORS.error, true); debugLog("Setting terminal status for slot revocation"); + } else if (state.disconnectReason === "session_id_error") { + debugLog("Branch: session_id_error"); + setDynamicStatus("Session ID error; try reconnecting", STATUS_COLORS.error, true); + debugLog("Setting terminal status for session_id error"); } else if (state.disconnectReason === "public_key_error") { debugLog("Branch: public_key_error"); - setStatus("Disconnected: Unable to read device public key", STATUS_COLORS.error, true); + setDynamicStatus("Unable to read device public key; try again", STATUS_COLORS.error, true); debugLog("Setting terminal status for public key error"); } else if (state.disconnectReason === "channel_setup_error") { debugLog("Branch: channel_setup_error"); const errorMsg = state.channelSetupErrorMessage || "Channel setup failed"; - setStatus(`Disconnected: ${errorMsg}`, STATUS_COLORS.error, true); + setDynamicStatus(errorMsg, STATUS_COLORS.error, true); debugLog("Setting terminal status for channel setup error"); state.channelSetupErrorMessage = null; // Clear after use (also cleared in cleanup as safety net) } else if (state.disconnectReason === "ble_disconnect_error") { debugLog("Branch: ble_disconnect_error"); const errorMsg = state.bleDisconnectErrorMessage || "BLE disconnect failed"; - setStatus(`Disconnected: ${errorMsg}`, STATUS_COLORS.error, true); + setDynamicStatus(errorMsg, STATUS_COLORS.error, true); debugLog("Setting terminal status for BLE disconnect error"); state.bleDisconnectErrorMessage = null; // Clear after use (also cleared in cleanup as safety net) } else if (state.disconnectReason === "normal" || state.disconnectReason === null || state.disconnectReason === undefined) { debugLog("Branch: normal/null/undefined"); - setStatus("Disconnected", STATUS_COLORS.error, true); + setDynamicStatus("Idle"); // Show em dash for normal disconnect } else { debugLog(`Branch: else (unknown reason: ${state.disconnectReason})`); - // For unknown disconnect reasons, show generic disconnected message - debugLog(`Showing generic disconnected message for unknown reason: ${state.disconnectReason}`); - setStatus("Disconnected", STATUS_COLORS.error, true); + // For unknown disconnect reasons, show em dash + debugLog(`Showing em dash for unknown reason: ${state.disconnectReason}`); + setDynamicStatus("Idle"); } setConnectButton(false); @@ -2185,6 +2647,7 @@ async function connect() { state.connection = null; state.channel = null; state.devicePublicKey = null; // Clear public key + state.wardriveSessionId = null; // Clear wardrive session ID state.disconnectReason = null; // Reset disconnect reason state.channelSetupErrorMessage = null; // Clear error message state.bleDisconnectErrorMessage = null; // Clear error message @@ -2209,7 +2672,8 @@ async function connect() { } catch (e) { debugError(`BLE connection failed: ${e.message}`, e); - setStatus("Connection failed", STATUS_COLORS.error); + setConnStatus("Disconnected", STATUS_COLORS.error); + setDynamicStatus("Connection failed", STATUS_COLORS.error); connectBtn.disabled = false; } } @@ -2227,7 +2691,9 @@ async function disconnect() { state.disconnectReason = "normal"; } - setStatus("Disconnecting", STATUS_COLORS.info); + // Set connection bar to "Disconnecting" - will remain until cleanup completes + setConnStatus("Disconnecting", STATUS_COLORS.info); + setDynamicStatus("Idle"); // Clear dynamic status // Release capacity slot if we have a public key if (state.devicePublicKey) { @@ -2283,7 +2749,7 @@ document.addEventListener("visibilitychange", async () => { if (state.running) { debugLog("Stopping auto ping due to page hidden"); stopAutoPing(true); // Ignore cooldown check when page is hidden - setStatus("Lost focus, auto mode stopped", STATUS_COLORS.warning); + setDynamicStatus("Lost focus, auto mode stopped", STATUS_COLORS.warning); } else { debugLog("Releasing wake lock due to page hidden"); releaseWakeLock(); @@ -2294,12 +2760,37 @@ document.addEventListener("visibilitychange", async () => { } }); +/** + * Update Connect button state based on radio power selection + */ +function updateConnectButtonState() { + const radioPowerSelected = getCurrentPowerSetting() !== ""; + const isConnected = !!state.connection; + + if (!isConnected) { + // Only enable Connect if radio power is selected + connectBtn.disabled = !radioPowerSelected; + + // Update dynamic status based on power selection + if (!radioPowerSelected) { + debugLog("Radio power not selected - showing message in status bar"); + setDynamicStatus("Select radio power to connect", STATUS_COLORS.warning); + } else { + debugLog("Radio power selected - clearing message from status bar"); + setDynamicStatus("Idle"); + } + } +} + // ---- Bind UI & init ---- export async function onLoad() { debugLog("wardrive.js onLoad() called - initializing"); - setStatus("Disconnected", STATUS_COLORS.error); + setConnStatus("Disconnected", STATUS_COLORS.error); enableControls(false); updateAutoButton(); + + // Initialize Connect button state based on radio power + updateConnectButtonState(); connectBtn.addEventListener("click", async () => { try { @@ -2310,7 +2801,7 @@ export async function onLoad() { } } catch (e) { debugError(`Connection button error: ${e.message}`, e); - setStatus(e.message || "Connection failed", STATUS_COLORS.error); + setDynamicStatus(e.message || "Connection failed", STATUS_COLORS.error); } }); sendPingBtn.addEventListener("click", () => { @@ -2321,12 +2812,64 @@ export async function onLoad() { debugLog("Auto toggle button clicked"); if (state.running) { stopAutoPing(); - setStatus("Auto mode stopped", STATUS_COLORS.idle); + setDynamicStatus("Auto mode stopped", STATUS_COLORS.idle); } else { startAutoPing(); } }); + // Settings panel toggle (for modernized UI) + const settingsGearBtn = document.getElementById("settingsGearBtn"); + const settingsPanel = document.getElementById("settingsPanel"); + const settingsCloseBtn = document.getElementById("settingsCloseBtn"); + const connectionBar = document.getElementById("connectionBar"); + + if (settingsGearBtn && settingsPanel && connectionBar) { + settingsGearBtn.addEventListener("click", () => { + debugLog("Settings gear button clicked"); + const isHidden = settingsPanel.classList.contains("hidden"); + settingsPanel.classList.toggle("hidden"); + + // Update connection bar border radius based on settings panel state + if (isHidden) { + // Settings panel is opening - remove bottom rounded corners from connection bar + connectionBar.classList.remove("rounded-xl", "rounded-b-xl"); + connectionBar.classList. add("rounded-t-xl", "rounded-b-none"); + } else { + // Settings panel is closing - restore full rounded corners to connection bar + connectionBar. classList.remove("rounded-t-xl", "rounded-b-none"); + connectionBar.classList. add("rounded-xl"); + } + }); + } + + if (settingsCloseBtn && settingsPanel && connectionBar) { + settingsCloseBtn.addEventListener("click", () => { + debugLog("Settings close button clicked"); + settingsPanel.classList. add("hidden"); + // Restore full rounded corners to connection bar + connectionBar.classList.remove("rounded-t-xl", "rounded-b-none"); + connectionBar.classList.add("rounded-xl"); + }); + } + + // Add event listeners to radio power options to update Connect button state + const powerRadios = document.querySelectorAll('input[name="power"]'); + powerRadios.forEach(radio => { + radio.addEventListener("change", () => { + debugLog(`Radio power changed to: ${getCurrentPowerSetting()}`); + updateConnectButtonState(); + }); + }); + + // Session Log event listener + if (logSummaryBar) { + logSummaryBar.addEventListener("click", () => { + debugLog("Log summary bar clicked - toggling session log"); + toggleBottomSheet(); + }); + } + // Prompt location permission early (optional) debugLog("Requesting initial location permission"); try { diff --git a/docs/CONNECTION_WORKFLOW.md b/docs/CONNECTION_WORKFLOW.md index 5fb424d..2fa004e 100644 --- a/docs/CONNECTION_WORKFLOW.md +++ b/docs/CONNECTION_WORKFLOW.md @@ -41,7 +41,8 @@ - Returns the application to idle state, ready for a new connection **Expected App State After Disconnect:** -- Status: "Disconnected" (red) +- Connection Status: "Disconnected" (red) +- Dynamic Status: Em dash (`—`) for normal disconnect, or error message for error disconnects - All controls disabled except "Connect" button - GPS tracking stopped - Auto-ping mode disabled @@ -85,14 +86,16 @@ connectBtn.addEventListener("click", async () => { - Checks `navigator.bluetooth` exists - Alerts user if not supported - Fails fast if unavailable - - **Status**: N/A (alert shown) + - **Connection Status**: N/A + - **Dynamic Status**: N/A (alert shown) 2. **Open BLE Connection** - Calls `WebBleConnection.open()` (web_ble_connection.js:15-41) - Shows browser's native device picker - Filters for MeshCore BLE service UUID - User selects device or cancels - - **Status**: `"Connecting"` (blue) + - **Connection Status**: `"Connecting"` (blue) - remains until GPS init completes + - **Dynamic Status**: `"—"` (em dash - cleared) 3. **Initialize BLE** - Connects to GATT server @@ -100,12 +103,14 @@ connectBtn.addEventListener("click", async () => { - Starts notifications on TX characteristic - Sets up frame listener for incoming data - Fires "connected" event - - **Status**: `"Connecting"` (blue, maintained) + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"—"` (em dash) 4. **Device Query** - Sends protocol version query - Non-critical, errors ignored - - **Status**: `"Connecting"` (blue, maintained) + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"—"` (em dash) 5. **Get Device Info** - Retrieves device name, public key (32 bytes), settings @@ -114,16 +119,19 @@ connectBtn.addEventListener("click", async () => { - Stores in `state.devicePublicKey` - Updates UI with device name - Changes button to "Disconnect" (red) - - **Status**: `"Connecting"` (blue, maintained) + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"—"` (em dash) 6. **Sync Device Time** - Sends current Unix timestamp - Device updates its clock - Optional, errors ignored - - **Status**: `"Connecting"` (blue, maintained) + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"—"` (em dash) 7. **Check Capacity** - - **Status**: `"Acquiring wardriving slot"` (blue) + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"Acquiring wardriving slot"` (blue) - POSTs to MeshMapper API: ```json { @@ -136,32 +144,38 @@ connectBtn.addEventListener("click", async () => { - If `allowed: false`: - Sets `state.disconnectReason = "capacity_full"` - Triggers disconnect sequence after 1.5s delay - - **Status**: `"Disconnecting"` (blue) → `"Disconnected: WarDriving app has reached capacity"` (red) + - **Connection Status**: `"Connecting"` → `"Disconnecting"` → `"Disconnected"` (red) + - **Dynamic Status**: `"Acquiring wardriving slot"` → `"WarDriving app has reached capacity"` (red, terminal) - If API error: - Sets `state.disconnectReason = "app_down"` - Triggers disconnect sequence after 1.5s delay (fail-closed) - - **Status**: `"Disconnecting"` (blue) → `"Disconnected: WarDriving app is down"` (red) - - On success → **Status**: `"Acquired wardriving slot"` (green) + - **Connection Status**: `"Connecting"` → `"Disconnecting"` → `"Disconnected"` (red) + - **Dynamic Status**: `"Acquiring wardriving slot"` → `"WarDriving app is down"` (red, terminal) + - On success: + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"Acquired wardriving slot"` (green) 8. **Setup Channel** - - **Status**: `"Looking for #wardriving channel"` (blue) + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"Looking for #wardriving channel"` (blue) - Searches for existing `#wardriving` channel - If found: - - **Status**: `"Channel #wardriving found"` (green) + - **Dynamic Status**: `"Channel #wardriving found"` (green) - Stores channel object in `state.channel` - Updates UI: "#wardriving (CH:X)" - If not found: - - **Status**: `"Channel #wardriving not found"` (blue) + - **Dynamic Status**: `"Channel #wardriving not found"` (blue) - Creates new channel: - Finds empty channel slot - Derives channel key: `SHA-256(#wardriving).slice(0, 16)` - Sends setChannel command - - **Status**: `"Created #wardriving"` (green) + - **Dynamic Status**: `"Created #wardriving"` (green) - Stores channel object in `state.channel` - Updates UI: "#wardriving (CH:X)" 9. **Initialize GPS** - - **Status**: `"Priming GPS"` (blue) + - **Connection Status**: `"Connecting"` (blue, maintained) + - **Dynamic Status**: `"Priming GPS"` (blue) - Requests location permission - Gets initial GPS position (30s timeout) - Starts continuous GPS watch @@ -171,7 +185,8 @@ connectBtn.addEventListener("click", async () => { - Refreshes coverage map if accuracy < 100m 10. **Connection Complete** - - **Status**: `"Connected"` (green) + - **Connection Status**: `"Connected"` (green) - **NOW shown after GPS init** + - **Dynamic Status**: `"—"` (em dash - cleared to show empty state) - Enables all UI controls - Ready for wardriving operations @@ -180,13 +195,13 @@ connectBtn.addEventListener("click", async () => { ### Disconnection Steps (High-Level) 1. **Disconnect Trigger** → User clicks "Disconnect" or error occurs -2. **Status Update** → Shows "Disconnecting" +2. **Status Update** → Connection Status shows "Disconnecting", Dynamic Status cleared to em dash 3. **Capacity Release** → Returns API slot to MeshMapper 4. **Channel Deletion** → Removes #wardriving channel from device 5. **BLE Disconnect** → Closes GATT connection 6. **Cleanup** → Stops timers, GPS, wake locks 7. **State Reset** → Clears all connection state -8. **Disconnected** → Returns to idle state +8. **Disconnected** → Connection Status shows "Disconnected", Dynamic Status shows em dash or error message ### Detailed Disconnection Steps @@ -212,7 +227,8 @@ See `content/wardrive.js` lines 2119-2179 for the main `disconnect()` function. - "slot_revoked" - slot revoked during active session 3. **Update Status** - - Sets status to "Disconnecting" (blue) + - **Connection Status**: `"Disconnecting"` (blue) - remains until cleanup completes + - **Dynamic Status**: `"—"` (em dash - cleared) 4. **Release Capacity** - POSTs to MeshMapper API with `reason: "disconnect"` @@ -230,6 +246,15 @@ See `content/wardrive.js` lines 2119-2179 for the main `disconnect()` function. 7. **Disconnected Event Handler** - Fires on BLE disconnect + - **Connection Status**: `"Disconnected"` (red) - ALWAYS set regardless of reason + - **Dynamic Status**: Set based on `state.disconnectReason` (WITHOUT "Disconnected:" prefix): + - `capacity_full` → `"WarDriving app has reached capacity"` (red) + - `app_down` → `"WarDriving app is down"` (red) + - `slot_revoked` → `"WarDriving slot has been revoked"` (red) + - `public_key_error` → `"Unable to read device public key; try again"` (red) + - `channel_setup_error` → Error message (red) + - `ble_disconnect_error` → Error message (red) + - `normal` / `null` / `undefined` → `"—"` (em dash) - Runs comprehensive cleanup: - Stops auto-ping mode - Clears auto-ping timer @@ -257,7 +282,8 @@ See `content/wardrive.js` lines 2119-2179 for the main `disconnect()` function. - `state.gpsState = "idle"` 10. **Disconnected Complete** - - Status: "Disconnected" (red) or error message + - **Connection Status**: `"Disconnected"` (red) + - **Dynamic Status**: `"—"` (em dash) or error message based on disconnect reason - All resources released - Ready for new connection @@ -278,33 +304,32 @@ When a wardriving slot is revoked during an active session (detected during API - Detected in `postToMeshMapperAPI()` response handler 2. **Initial Status** - - **Status**: `"Error: Posting to API (Revoked)"` (red) + - **Dynamic Status**: `"Error: Posting to API (Revoked)"` (red) - Sets `state.disconnectReason = "slot_revoked"` - Visible for 1.5 seconds 3. **Disconnect Initiated** - Calls `disconnect()` after 1.5s delay - - **Status**: `"Disconnecting"` (blue) + - **Connection Status**: `"Disconnecting"` (blue) + - **Dynamic Status**: `"—"` (em dash - cleared during disconnect) - Proceeds with normal disconnect cleanup 4. **Terminal Status** - Disconnect event handler detects `slot_revoked` reason - - **Status**: `"Disconnected: WarDriving slot has been revoked"` (red) - - This is the final terminal status (does NOT revert to "Idle") + - **Connection Status**: `"Disconnected"` (red) + - **Dynamic Status**: `"WarDriving slot has been revoked"` (red, terminal - NO "Disconnected:" prefix) + - This is the final terminal status **Complete Revocation Flow:** ``` -"Posting to API" (blue) - → "Error: Posting to API (Revoked)" (red, 1.5s) - → "Disconnecting" (blue) - → "Disconnected: WarDriving slot has been revoked" (red, terminal) +Connection Status: (unchanged) → "Disconnecting" → "Disconnected" +Dynamic Status: "Posting to API" → "Error: Posting to API (Revoked)" → "—" → "WarDriving slot has been revoked" ``` **Key Differences from Normal Disconnect:** -- Normal disconnect: ends with "Disconnected" (red) -- Revocation: ends with "Disconnected: WarDriving slot has been revoked" (red) +- Normal disconnect: Dynamic Status shows `"—"` (em dash) +- Revocation: Dynamic Status shows `"WarDriving slot has been revoked"` (red error, no prefix) - Revocation shows intermediate "Error: Posting to API (Revoked)" state -- Terminal status is preserved and does not loop to "Idle" ## Workflow Diagrams @@ -458,7 +483,9 @@ stateDiagram-v2 ### State Management - **Global state**: `wardrive.js` lines 102-136 (`const state = {...}`) -- **Status management**: `wardrive.js:setStatus()` (lines 165-225) +- **Connection Status management**: `wardrive.js:setConnStatus(text, color)` - Updates connection status bar +- **Dynamic Status management**: `wardrive.js:setDynamicStatus(text, color, immediate)` - Updates dynamic status bar +- **Internal status**: `wardrive.js:setStatus()` (lines 165-225) - Internal implementation with minimum visibility - **Button state**: `wardrive.js:setConnectButton()` (lines 495-518) - **Control state**: `wardrive.js:enableControls()` (lines 462-466) diff --git a/docs/STATUS_MESSAGES.md b/docs/STATUS_MESSAGES.md index 3fe6075..7ac3270 100644 --- a/docs/STATUS_MESSAGES.md +++ b/docs/STATUS_MESSAGES.md @@ -4,398 +4,426 @@ This document provides a comprehensive inventory of all status messages displaye ## Overview -All status messages enforce a **minimum visibility duration of 500ms** to ensure readability. This applies to non-timed messages. Countdown timers respect this minimum for their first display, but subsequent updates occur immediately. +The application uses **two independent status bars**: -## Standardization Rules +1. **Connection Status Bar** (`#connectionStatus`) - Shows connection state ONLY +2. **Dynamic App Status Bar** (`#status`) - Shows all non-connection operational messages -Status messages follow these consistent conventions: -- **No trailing punctuation** (no ellipsis or periods for short statuses) -- **Sentence case** capitalization -- **Present progressive tense** (-ing) for ongoing actions -- **Past tense** for completed actions -- **Concise and readable** phrasing +All dynamic status messages enforce a **minimum visibility duration of 500ms** to ensure readability. This applies to non-timed messages. Countdown timers respect this minimum for their first display, but subsequent updates occur immediately. --- -## Status Messages by Category +## Two-Bar System -### 1. Connection Status Messages +### Connection Status Bar (`#connectionStatus`) +- **Purpose**: Display ONLY the connection state of the BLE device +- **Location**: Top status bar with status indicator dot +- **Messages**: Exactly **four fixed states** (see below) +- **Behavior**: Updates immediately (no minimum visibility delay) +- **Controlled by**: `setConnStatus(text, color)` function -#### Connecting -- **Message**: `"Connecting"` -- **Color**: Sky blue (info) -- **Used in**: `connect()` -- **Source**: `content/wardrive.js:2021` -- **Context**: When user clicks Connect button; remains visible during entire connection process (BLE pairing, capacity check, and channel setup) -- **Minimum Visibility**: Natural async timing during full connection process (typically 3-8 seconds including capacity check) +### Dynamic App Status Bar (`#status`) +- **Purpose**: Display all operational messages EXCEPT connection state +- **Location**: Status message box below connection bar +- **Messages**: All progress, error, countdown, and informational messages +- **Behavior**: 500ms minimum visibility for first display, immediate for countdown updates +- **Placeholder**: Shows em dash (`—`) when no message is present +- **Protection**: Connection words (Connected/Connecting/Disconnecting/Disconnected) are blocked +- **Controlled by**: `setDynamicStatus(text, color, immediate)` function + +--- + +## Connection Status Bar Messages -#### Connected +These **four messages** are the ONLY messages that appear in the Connection Status Bar: + +### Connected - **Message**: `"Connected"` - **Color**: Green (success) -- **Used in**: `connect()` -- **Source**: `content/wardrive.js:2080` -- **Context**: After full connection process completes successfully (BLE paired, capacity check passed, channel setup, and GPS initialized) -- **Minimum Visibility**: Persists until user interacts with app buttons (send ping, start auto mode) -- **Note**: This message now only appears after the complete connection handshake, not just after BLE pairing - -#### Disconnecting -- **Message**: `"Disconnecting"` +- **When**: Device is fully connected and ready for wardriving after complete workflow: + 1. User clicks Connect + 2. BLE GATT connection established + 3. Protocol handshake complete + 4. Device info retrieved + 5. Time sync complete + 6. Capacity check passed (API slot acquired) + 7. Channel setup complete (#wardriving found or created) + 8. GPS initialization complete + 9. **Then "Connected" is shown** +- **Source**: `content/wardrive.js` - `connect()` function after GPS init + +### Connecting +- **Message**: `"Connecting"` - **Color**: Sky blue (info) -- **Used in**: `disconnect()` -- **Source**: `content/wardrive.js:2118` -- **Context**: When user clicks Disconnect button or when automatic disconnect is triggered -- **Minimum Visibility**: 500ms minimum enforced +- **When**: During the ENTIRE connection process from step 1 (user clicks Connect) through step 8 (GPS init in progress) +- **Duration**: Remains visible until GPS init completes successfully +- **Source**: `content/wardrive.js` - `connect()` function at start -#### Disconnected +### Disconnected - **Message**: `"Disconnected"` - **Color**: Red (error) -- **Used in**: `connect()`, `disconnect()`, event handlers -- **Source**: `content/wardrive.js:2073`, `content/wardrive.js:2177` -- **Context**: Initial state and when BLE device disconnects normally (user-initiated or device-initiated) -- **Minimum Visibility**: N/A (persists until connection is established) -- **Note**: Only shown for normal disconnections; error disconnections (e.g., app down, capacity full) preserve their specific error message - -#### Connection failed -- **Message**: `"Connection failed"` (or error message) -- **Color**: Red (error) -- **Used in**: `connect()`, event handlers -- **Source**: `content/wardrive.js:2096`, `content/wardrive.js:2190` -- **Context**: BLE connection fails or connection button error -- **Minimum Visibility**: N/A (error state persists) +- **When**: + - Initial state when app loads + - After disconnect sequence completes + - After BLE connection is lost +- **Source**: `content/wardrive.js` - BLE disconnected event handler -#### Channel setup failed -- **Message**: `"Channel setup failed"` (or error message) -- **Color**: Red (error) -- **Used in**: `connect()` -- **Source**: `content/wardrive.js:2063` -- **Context**: Channel creation or lookup fails during connection -- **Minimum Visibility**: N/A (error state persists) - -#### Disconnect failed -- **Message**: `"Disconnect failed"` (or error message) -- **Color**: Red (error) -- **Used in**: `disconnect()` -- **Source**: `content/wardrive.js:2149` -- **Context**: Error during disconnect operation -- **Minimum Visibility**: N/A (error state persists) +### Disconnecting +- **Message**: `"Disconnecting"` +- **Color**: Sky blue (info) +- **When**: During the ENTIRE disconnection process: + 1. User clicks Disconnect (or error triggers disconnect) + 2. Disconnect function called + 3. Capacity slot released (API call) + 4. Channel deleted from device + 5. BLE GATT disconnected + 6. Cleanup operations (timers, GPS, wake locks) + 7. State reset + 8. **Then "Disconnected" is shown** +- **Source**: `content/wardrive.js` - `disconnect()` function --- -### 2. Capacity Check Messages +## Dynamic App Status Bar Messages + +These messages appear in the Dynamic App Status Bar. They NEVER include connection state words. -#### Acquiring wardriving slot +### Message Categories + +#### 1. Capacity Check Messages + +##### BLE Connection Started +- **Message**: `"BLE Connection Started"` +- **Color**: Sky blue (info) +- **When**: At the beginning of the BLE connection process, before device selection dialog appears +- **Source**: `content/wardrive.js:connect()` + +##### Acquiring wardriving slot - **Message**: `"Acquiring wardriving slot"` - **Color**: Sky blue (info) -- **Used in**: `checkCapacity()` -- **Source**: `content/wardrive.js:1033` -- **Context**: When connecting to device, after time sync and before channel setup, checking if a wardriving slot is available -- **Minimum Visibility**: 500ms minimum enforced (or until API response received) +- **When**: During connection, after time sync, checking with MeshMapper API for slot availability +- **Source**: `content/wardrive.js:checkCapacity()` -#### Acquired wardriving slot +##### Acquired wardriving slot - **Message**: `"Acquired wardriving slot"` - **Color**: Green (success) -- **Used in**: `connect()` -- **Source**: `content/wardrive.js:2087` -- **Context**: Capacity check passed successfully, slot acquired from MeshMapper API -- **Minimum Visibility**: 500ms minimum enforced -- **Notes**: This message appears after "Acquiring wardriving slot" when the API confirms slot availability. Fixes spelling from previous "Aquired" typo. - -#### Disconnected: WarDriving app has reached capacity -- **Message**: `"Disconnected: WarDriving app has reached capacity"` +- **When**: Capacity check passed successfully, slot acquired from MeshMapper API +- **Source**: `content/wardrive.js:connect()` + +##### WarDriving app has reached capacity +- **Message**: `"WarDriving app has reached capacity"` - **Color**: Red (error) -- **Used in**: `connect()` (disconnected event handler) -- **Source**: `content/wardrive.js` (disconnected event handler) -- **Context**: Capacity check API denies slot on connect (returns allowed=false) -- **Minimum Visibility**: N/A (error state persists as terminal status) -- **Notes**: This is the final status message when capacity check fails during connection. The complete sequence is: "Connecting" → "Acquiring wardriving slot" → "Disconnecting" → "Disconnected: WarDriving app has reached capacity". Message format standardized with "Disconnected: " prefix to clearly indicate disconnect state. +- **When**: Capacity check API denies slot on connect (returns allowed=false) +- **Terminal State**: Yes (persists until user takes action) +- **Notes**: Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "WarDriving app has reached capacity" (terminal) -#### Error: Posting to API (Revoked) -- **Message**: `"Error: Posting to API (Revoked)"` +##### WarDriving app is down +- **Message**: `"WarDriving app is down"` - **Color**: Red (error) -- **Used in**: `postToMeshMapperAPI()` -- **Source**: `content/wardrive.js:1128` -- **Context**: Intermediate status shown when WarDriving API returns allowed=false during an active session -- **Minimum Visibility**: 1500ms (enforced by setTimeout delay before disconnect) -- **Notes**: This is the first status message shown when slot revocation is detected during API posting. After the delay, the disconnect sequence begins with "Disconnecting", followed by the terminal status "Disconnected: WarDriving slot has been revoked". - -#### Disconnected: WarDriving slot has been revoked -- **Message**: `"Disconnected: WarDriving slot has been revoked"` +- **When**: Capacity check API returns error status or network is unreachable during connect +- **Terminal State**: Yes (persists until user takes action) +- **Notes**: Implements fail-closed policy - connection denied if API fails. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "WarDriving app is down" (terminal) + +##### WarDriving slot has been revoked +- **Message**: `"WarDriving slot has been revoked"` - **Color**: Red (error) -- **Used in**: `connect()` (disconnected event handler) -- **Source**: `content/wardrive.js:2123` -- **Context**: Terminal status shown when WarDriving slot is revoked during an active session -- **Minimum Visibility**: N/A (error state persists as terminal status) -- **Notes**: This is the final status message in the slot revocation flow. The complete sequence is: "Posting to API" → "Error: Posting to API (Revoked)" → "Disconnecting" → "Disconnected: WarDriving slot has been revoked". This message is set by the disconnect event handler when state.disconnectReason is "slot_revoked". Message format standardized with "Disconnected: " prefix to clearly indicate disconnect state. - -#### Disconnected: WarDriving app is down -- **Message**: `"Disconnected: WarDriving app is down"` +- **When**: During active session, API returns allowed=false during ping posting +- **Terminal State**: Yes (persists until user takes action) +- **Sequence**: + 1. "Posting to API" (blue) + 2. "Error: Posting to API (Revoked)" (red, 1.5s) + 3. Connection bar: "Disconnecting" → "Disconnected" + 4. Dynamic bar: "WarDriving slot has been revoked" (terminal) + +##### Error: Posting to API (Revoked) +- **Message**: `"Error: Posting to API (Revoked)"` - **Color**: Red (error) -- **Used in**: `connect()` (disconnected event handler) -- **Source**: `content/wardrive.js` (disconnected event handler) -- **Context**: Capacity check API returns error status or network is unreachable during connect -- **Minimum Visibility**: N/A (error state persists as terminal status) -- **Notes**: Implements fail-closed policy - connection is denied if API fails or is unreachable. The complete sequence is: "Connecting" → "Acquiring wardriving slot" → "Disconnecting" → "Disconnected: WarDriving app is down". Message format standardized with "Disconnected: " prefix to clearly indicate disconnect state. +- **When**: Intermediate status shown when slot revocation detected during API posting +- **Duration**: 1.5 seconds (visible before disconnect begins) +- **Notes**: First status in revocation sequence, followed by disconnect flow -#### Unable to read device public key; try again +##### Unable to read device public key; try again - **Message**: `"Unable to read device public key; try again"` - **Color**: Red (error) -- **Used in**: `connect()` -- **Source**: `content/wardrive.js:2048` -- **Context**: Device public key is missing or invalid when trying to acquire capacity slot -- **Minimum Visibility**: N/A (error state persists until disconnect) +- **When**: Device public key is missing or invalid during connection +- **Terminal State**: Yes +- **Notes**: Triggers automatic disconnect -#### Network issue checking slot, proceeding anyway -- **Message**: `"Network issue checking slot, proceeding anyway"` (DEPRECATED - no longer used) -- **Color**: Amber (warning) -- **Used in**: N/A (removed) -- **Source**: Previously `content/wardrive.js:1051`, `content/wardrive.js:1070` -- **Context**: This message is no longer shown. Network issues now result in connection denial (fail-closed) -- **Notes**: Replaced by fail-closed policy - connection is now denied on network errors - ---- +##### Session ID error; try reconnecting +- **Message**: `"Session ID error; try reconnecting"` +- **Color**: Red (error) +- **When**: + - Capacity check returns allowed=true but session_id is missing during connection + - Attempting to post to MeshMapper API without a valid session_id +- **Terminal State**: Yes (persists until user takes action) +- **Notes**: Implements fail-closed policy - connection/posting denied if session_id is missing. Complete flow: Connection bar shows "Connecting" → "Disconnecting" → "Disconnected". Dynamic bar shows "Acquiring wardriving slot" → "Session ID error; try reconnecting" (terminal) +- **Source**: `content/wardrive.js:checkCapacity()`, `content/wardrive.js:postToMeshMapperAPI()` + +##### Error: No session ID for API post +- **Message**: `"Error: No session ID for API post"` +- **Color**: Red (error) +- **When**: Intermediate status shown when attempting to post to MeshMapper API without a valid session_id +- **Duration**: 1.5 seconds (visible before disconnect begins) +- **Notes**: First status in session_id error sequence during API posting, followed by disconnect flow +- **Source**: `content/wardrive.js:postToMeshMapperAPI()` -### 3. Channel Setup Messages +#### 2. Channel Setup Messages -#### Looking for #wardriving channel +##### Looking for #wardriving channel - **Message**: `"Looking for #wardriving channel"` - **Color**: Sky blue (info) -- **Used in**: `ensureChannel()` -- **Source**: `content/wardrive.js:954` -- **Context**: During connection setup, after capacity check, searching for existing #wardriving channel -- **Minimum Visibility**: 500ms minimum enforced +- **When**: During connection setup, after capacity check, searching for existing channel +- **Source**: `content/wardrive.js:ensureChannel()` -#### Channel #wardriving found +##### Channel #wardriving found - **Message**: `"Channel #wardriving found"` - **Color**: Green (success) -- **Used in**: `ensureChannel()` -- **Source**: `content/wardrive.js:971` -- **Context**: Existing #wardriving channel found on device -- **Minimum Visibility**: 500ms minimum enforced +- **When**: Existing #wardriving channel found on device +- **Source**: `content/wardrive.js:ensureChannel()` -#### Channel #wardriving not found +##### Channel #wardriving not found - **Message**: `"Channel #wardriving not found"` - **Color**: Sky blue (info) -- **Used in**: `ensureChannel()` -- **Source**: `content/wardrive.js:958` -- **Context**: #wardriving channel does not exist, will attempt to create it -- **Minimum Visibility**: 500ms minimum enforced +- **When**: Channel does not exist, will attempt to create it +- **Source**: `content/wardrive.js:ensureChannel()` -#### Created #wardriving +##### Created #wardriving - **Message**: `"Created #wardriving"` - **Color**: Green (success) -- **Used in**: `ensureChannel()` -- **Source**: `content/wardrive.js:962` -- **Context**: Successfully created new #wardriving channel on device -- **Minimum Visibility**: 500ms minimum enforced - ---- +- **When**: Successfully created new #wardriving channel on device +- **Source**: `content/wardrive.js:ensureChannel()` -### 4. GPS Initialization Messages +#### 3. GPS Initialization Messages -#### Priming GPS +##### Priming GPS - **Message**: `"Priming GPS"` - **Color**: Sky blue (info) -- **Used in**: `connect()` -- **Source**: `content/wardrive.js:2101` -- **Context**: Starting GPS initialization during connection setup -- **Minimum Visibility**: 500ms minimum enforced (or until GPS initialization completes) -- **Notes**: This status is shown after channel setup and before the final "Connected" status. +- **When**: Starting GPS initialization during connection setup (after channel setup) +- **Source**: `content/wardrive.js:connect()` ---- +##### GPS error - check permissions +- **Message**: `"GPS error - check permissions"` +- **Color**: Red (error) +- **When**: GPS geolocation watch encounters an error or GPS permission is denied +- **Terminal State**: Depends on context (persists until GPS is re-enabled or permissions granted) +- **Notes**: This error is displayed in the Dynamic Status Bar. The GPS section in the map overlay remains empty (shows "-") until valid coordinates are available. This ensures GPS errors are not shown in the GPS block itself. +- **Source**: `content/wardrive.js:startGeoWatch()`, `primeGpsOnce()` + +##### Waiting for GPS fix +- **Message**: `"Waiting for GPS fix"` +- **Color**: Amber (warning) +- **When**: Auto ping triggered but no GPS lock acquired yet +- **Source**: `content/wardrive.js:getGpsCoordinatesForPing()` + +##### GPS data too old, requesting fresh position +- **Message**: `"GPS data too old, requesting fresh position"` +- **Color**: Amber (warning) +- **When**: GPS data is stale and needs refresh (auto or manual ping modes) +- **Source**: `content/wardrive.js:getGpsCoordinatesForPing()` -### 5. Ping Operation Messages +#### 4. Ping Operation Messages -#### Sending manual ping +##### Sending manual ping - **Message**: `"Sending manual ping"` - **Color**: Sky blue (info) -- **Used in**: `sendPing()` -- **Source**: `content/wardrive.js:1655`, `content/wardrive.js:1662` -- **Context**: When ping button clicked -- **Minimum Visibility**: 500ms minimum enforced +- **When**: User clicks "Send Ping" button +- **Source**: `content/wardrive.js:sendPing()` -#### Sending auto ping +##### Sending auto ping - **Message**: `"Sending auto ping"` - **Color**: Sky blue (info) -- **Used in**: `sendPing()` -- **Source**: `content/wardrive.js:1659` -- **Context**: Auto ping triggers -- **Minimum Visibility**: 500ms minimum enforced +- **When**: Auto ping timer triggers +- **Source**: `content/wardrive.js:sendPing()` -#### Ping sent +##### Ping sent - **Message**: `"Ping sent"` - **Color**: Green (success) -- **Used in**: `sendPing()` -- **Source**: `content/wardrive.js:1749` -- **Context**: After successful ping transmission to mesh device (both manual and auto pings) -- **Minimum Visibility**: 500ms minimum enforced -- **Notes**: Consolidated from separate "Ping sent" and "Auto ping sent" messages - -#### Ping failed -- **Message**: `"Ping failed"` (or error message) +- **When**: After successful ping transmission to mesh device +- **Minimum Visibility**: 500ms enforced +- **Source**: `content/wardrive.js:sendPing()` + +##### Ping failed +- **Message**: `"Ping failed"` or specific error message - **Color**: Red (error) -- **Used in**: `sendPing()` -- **Source**: `content/wardrive.js:1805` -- **Context**: Ping operation encounters an error -- **Minimum Visibility**: N/A (error state persists) +- **When**: Ping operation encounters an error +- **Source**: `content/wardrive.js:sendPing()` -#### Ping skipped, outside of geofenced region +##### Ping skipped, outside of geofenced region - **Message**: `"Ping skipped, outside of geofenced region"` - **Color**: Amber (warning) -- **Used in**: `sendPing()`, `autoCountdownTimer` -- **Source**: `content/wardrive.js:1688`, `content/wardrive.js:297` -- **Context**: GPS coordinates outside Ottawa 150km radius -- **Minimum Visibility**: 500ms minimum enforced +- **When**: GPS coordinates outside Ottawa 150km radius +- **Behavior**: + - In manual mode (auto OFF): Message persists until next action + - In manual mode (auto ON): Message shown briefly, then auto countdown resumes +- **Source**: `content/wardrive.js:sendPing()` -#### Ping skipped, too close to last ping +##### Ping skipped, too close to last ping - **Message**: `"Ping skipped, too close to last ping"` - **Color**: Amber (warning) -- **Used in**: `sendPing()` -- **Source**: `content/wardrive.js:1708` -- **Context**: Current location < 25m from last successful ping -- **Minimum Visibility**: 500ms minimum enforced +- **When**: Current location < 25m from last successful ping +- **Behavior**: + - In manual mode (auto OFF): Message persists until next action + - In manual mode (auto ON): Message shown briefly, then auto countdown resumes +- **Source**: `content/wardrive.js:sendPing()` -#### Wait Xs before sending another ping +##### Wait Xs before sending another ping - **Message**: `"Wait Xs before sending another ping"` (X is dynamic countdown) - **Color**: Amber (warning) -- **Used in**: `sendPing()` -- **Source**: `content/wardrive.js:1646` -- **Context**: User attempts manual ping during 7-second cooldown -- **Minimum Visibility**: 500ms minimum enforced - ---- - -### 6. GPS Status Messages - -#### Waiting for GPS fix -- **Message**: `"Waiting for GPS fix"` -- **Color**: Amber (warning) -- **Used in**: `getGpsCoordinatesForPing()` -- **Source**: `content/wardrive.js:1614` -- **Context**: Auto ping triggered but no GPS lock acquired yet -- **Minimum Visibility**: 500ms minimum enforced - -#### GPS data too old, requesting fresh position -- **Message**: `"GPS data too old, requesting fresh position"` -- **Color**: Amber (warning) -- **Used in**: `getGpsCoordinatesForPing()` -- **Source**: `content/wardrive.js:1625`, `content/wardrive.js:1678` -- **Context**: GPS data is stale and needs refresh (used in both auto and manual ping modes) -- **Minimum Visibility**: 500ms minimum enforced +- **When**: User attempts manual ping during 7-second cooldown +- **Source**: `content/wardrive.js:sendPing()` ---- - -### 7. Countdown Timer Messages +#### 5. Countdown Timer Messages These messages use a hybrid approach: **first display respects 500ms minimum**, then updates occur immediately every second. -#### Listening for heard repeats (Xs) +##### Listening for heard repeats (Xs) - **Message**: `"Listening for heard repeats (Xs)"` (X is dynamic countdown) - **Color**: Sky blue (info) -- **Used in**: `rxListeningCountdownTimer` -- **Source**: `content/wardrive.js:328` -- **Context**: After successful ping, listening for repeater echoes +- **When**: After successful ping, listening for repeater echoes - **Duration**: 7 seconds total - **Minimum Visibility**: 500ms for first message, immediate for countdown updates +- **Source**: `content/wardrive.js:rxListeningCountdownTimer` -#### Finalizing heard repeats +##### Finalizing heard repeats - **Message**: `"Finalizing heard repeats"` - **Color**: Sky blue (info) -- **Used in**: `rxListeningCountdownTimer` -- **Source**: `content/wardrive.js:325` -- **Context**: Countdown reached 0, processing repeater data +- **When**: Countdown reached 0, processing repeater data - **Minimum Visibility**: Immediate (countdown update) +- **Source**: `content/wardrive.js:rxListeningCountdownTimer` -#### Waiting for next auto ping (Xs) +##### Waiting for next auto ping (Xs) - **Message**: `"Waiting for next auto ping (Xs)"` (X is dynamic countdown) - **Color**: Slate (idle) -- **Used in**: `autoCountdownTimer` -- **Source**: `content/wardrive.js:314` -- **Context**: Auto mode active, between pings +- **When**: Auto mode active, between pings - **Duration**: 15s, 30s, or 60s (user-selectable) - **Minimum Visibility**: 500ms for first message, immediate for countdown updates +- **Source**: `content/wardrive.js:autoCountdownTimer` -#### Ping skipped, outside of geofenced region, waiting for next ping (Xs) +##### Ping skipped, outside of geofenced region, waiting for next ping (Xs) - **Message**: `"Ping skipped, outside of geofenced region, waiting for next ping (Xs)"` (X is dynamic countdown) - **Color**: Amber (warning) -- **Used in**: `autoCountdownTimer` -- **Source**: `content/wardrive.js:297` -- **Context**: Auto ping skipped due to geofence, showing countdown +- **When**: Auto ping skipped due to geofence, showing countdown - **Minimum Visibility**: 500ms for first message, immediate for updates +- **Source**: `content/wardrive.js:autoCountdownTimer` -#### Ping skipped, too close to last ping, waiting for next ping (Xs) +##### Ping skipped, too close to last ping, waiting for next ping (Xs) - **Message**: `"Ping skipped, too close to last ping, waiting for next ping (Xs)"` (X is dynamic countdown) - **Color**: Amber (warning) -- **Used in**: `autoCountdownTimer` -- **Source**: `content/wardrive.js:303` -- **Context**: Auto ping skipped due to distance check, showing countdown +- **When**: Auto ping skipped due to distance check, showing countdown - **Minimum Visibility**: 500ms for first message, immediate for updates +- **Source**: `content/wardrive.js:autoCountdownTimer` -#### Skipped (X), next ping (Ys) +##### Skipped (X), next ping (Ys) - **Message**: `"Skipped (X), next ping (Ys)"` (X is skip reason, Y is countdown) - **Color**: Amber (warning) -- **Used in**: `autoCountdownTimer` -- **Source**: `content/wardrive.js:309` -- **Context**: Auto ping skipped for generic reason (e.g., "gps too old"), showing countdown +- **When**: Auto ping skipped for generic reason (e.g., "gps too old") - **Minimum Visibility**: 500ms for first message, immediate for updates +- **Source**: `content/wardrive.js:autoCountdownTimer` ---- - -### 8. API and Map Update Messages +#### 6. API and Map Update Messages -#### Posting to API +##### Posting to API - **Message**: `"Posting to API"` - **Color**: Sky blue (info) -- **Used in**: `postApiAndRefreshMap()` -- **Source**: `content/wardrive.js:1167` -- **Context**: After RX listening window, posting ping data to MeshMapper API +- **When**: After RX listening window, posting ping data to MeshMapper API - **Timing**: Visible during API POST operation (3-second hidden delay + API call time, typically ~3.5-4.5s total) -- **Minimum Visibility**: 500ms minimum enforced (naturally ~4s due to 3s delay + API timing) -- **Notes**: A 3-second hidden delay occurs before the actual API call to ensure good visibility +- **Source**: `content/wardrive.js:postApiAndRefreshMap()` -#### Idle -- **Message**: `"Idle"` +##### — (em dash) +- **Message**: `"—"` (em dash character) - **Color**: Slate (idle) -- **Used in**: `postApiAndRefreshMap()` -- **Source**: `content/wardrive.js:1203` -- **Context**: Manual mode after API post completes -- **Minimum Visibility**: 500ms minimum enforced -- **Note**: No longer shown after initial connection; "Connected" status is displayed instead and persists until user action +- **When**: + - Manual mode after API post completes + - After successful connection (shows "Connected" in connection bar) + - Normal disconnect (shows "Disconnected" in connection bar) + - Any time there is no active message to display +- **Purpose**: Placeholder to indicate "no message" state +- **Source**: Multiple locations - `content/wardrive.js` ---- - -### 9. Auto Mode Messages +#### 7. Auto Mode Messages -#### Auto mode stopped +##### Auto mode stopped - **Message**: `"Auto mode stopped"` - **Color**: Slate (idle) -- **Used in**: `disconnect()` (event handler for stopping auto mode) -- **Source**: `content/wardrive.js:2247` -- **Context**: User clicks "Stop Auto Ping" button -- **Minimum Visibility**: 500ms minimum enforced +- **When**: User clicks "Stop Auto Ping" button +- **Source**: `content/wardrive.js:autoToggleBtn click handler` -#### Lost focus, auto mode stopped +##### Lost focus, auto mode stopped - **Message**: `"Lost focus, auto mode stopped"` - **Color**: Amber (warning) -- **Used in**: `disconnect()` (page visibility handler) -- **Source**: `content/wardrive.js:2209` -- **Context**: Browser tab hidden while auto mode running -- **Minimum Visibility**: 500ms minimum enforced +- **When**: Browser tab hidden while auto mode running +- **Source**: `content/wardrive.js:visibilitychange handler` -#### Wait Xs before toggling auto mode +##### Wait Xs before toggling auto mode - **Message**: `"Wait Xs before toggling auto mode"` (X is dynamic countdown) - **Color**: Amber (warning) -- **Used in**: `stopAutoPing()`, `startAutoPing()` -- **Source**: `content/wardrive.js:1928`, `content/wardrive.js:1988` -- **Context**: User attempts to toggle auto mode during cooldown period -- **Minimum Visibility**: 500ms minimum enforced +- **When**: User attempts to toggle auto mode during cooldown period +- **Source**: `content/wardrive.js:stopAutoPing()`, `startAutoPing()` + +#### 8. Error Messages + +##### Select radio power to connect +- **Message**: `"Select radio power to connect"` +- **Color**: Amber (warning) +- **When**: On app load or when disconnected, if no radio power option is selected +- **Terminal State**: Yes (persists until radio power is selected) +- **Notes**: Displayed in Dynamic Status Bar as a warning message to guide user that Connect button is disabled. Once radio power is selected, status changes to "Idle" (em dash) and Connect button becomes enabled. +- **Source**: `content/wardrive.js:updateConnectButtonState()` + +##### Connection failed +- **Message**: `"Connection failed"` or specific error message +- **Color**: Red (error) +- **When**: BLE connection fails or connection button error +- **Source**: `content/wardrive.js:connect()`, event handlers --- ## Implementation Details +### Status Setter Functions + +#### setConnStatus(text, color) +```javascript +/** + * Set connection status bar message + * Updates the #connectionStatus element with one of four fixed states + */ +function setConnStatus(text, color) { + // Updates connection bar immediately (no minimum visibility delay) + connectionStatusEl.textContent = text; + connectionStatusEl.className = `font-medium ${color}`; + // Also updates status indicator dot color +} +``` + +#### setDynamicStatus(text, color, immediate) +```javascript +/** + * Set dynamic status bar message + * Uses 500ms minimum visibility for first display, immediate for countdown updates + * Blocks connection words and shows em dash for empty messages + */ +function setDynamicStatus(text, color, immediate) { + // Normalize empty/null/whitespace to em dash + if (!text || text.trim() === '') { + text = '—'; + } + + // Block connection words from dynamic bar + const connectionWords = ['Connected', 'Connecting', 'Disconnecting', 'Disconnected']; + if (connectionWords.includes(text)) { + debugWarn(`Connection word blocked from dynamic bar`); + text = '—'; + } + + // Use existing setStatus implementation with minimum visibility + setStatus(text, color, immediate); +} +``` + ### Minimum Visibility Enforcement -The `setStatus()` function implements minimum visibility using: +The `setStatus()` function (internal) implements minimum visibility: ```javascript const MIN_STATUS_VISIBILITY_MS = 500; // 500ms minimum @@ -416,8 +444,7 @@ function setStatus(text, color, immediate = false) { } // Otherwise, queue the message with appropriate delay - const delayNeeded = MIN_STATUS_VISIBILITY_MS - timeSinceLastSet; - // ... queue message ... + // (last-write-wins strategy) } ``` @@ -427,46 +454,39 @@ Countdown timers use a hybrid approach: - **First update**: Respects 500ms minimum visibility of previous message - **Subsequent updates**: Immediate (using `immediate = true` flag) -This ensures that important status messages (like "Ping sent") are visible for at least 500ms before being replaced by countdown timers, while still allowing countdown updates to occur smoothly every second. +This ensures important status messages (like "Ping sent") are visible for at least 500ms before being replaced by countdown timers, while allowing smooth countdown updates every second. -### Message Queue Strategy +--- -When multiple messages arrive within the 500ms window: -- Only the **most recent** message is kept in the queue -- Previous queued messages are discarded (last-write-wins) -- This prevents a backlog of stale messages +## Standardization Rules -Example: -``` -Time 0ms: "Message A" displayed -Time 100ms: "Message B" queued (will display at 500ms) -Time 200ms: "Message C" queued (replaces B, will display at 500ms) -Result: "Message A" (visible 500ms) → "Message C" -``` +Status messages follow these consistent conventions: +- **No trailing punctuation** (no ellipsis or periods for short statuses) +- **Sentence case** capitalization +- **Present progressive tense** (-ing) for ongoing actions +- **Past tense** for completed actions +- **Concise and readable** phrasing +- **No "Disconnected:" prefix** - error reasons shown without prefix in dynamic bar --- ## Summary -**Total Status Messages**: 38 unique message patterns -- **Connection**: 7 messages -- **Capacity Check**: 7 messages (1 deprecated, includes new "Acquired wardriving slot" and "Error: Posting to API (Revoked)") -- **Channel Setup**: 4 messages (new section for channel lookup and creation) -- **GPS Initialization**: 1 message (new "Priming GPS") -- **Ping Operation**: 6 messages (consolidated "Ping sent" for both manual and auto) -- **GPS Status**: 2 messages -- **Countdown Timers**: 6 message patterns (with dynamic countdown values) -- **API/Map**: 2 messages -- **Auto Mode**: 3 messages - -**Minimum Visibility**: All non-countdown messages enforce **500ms minimum visibility**. Countdown messages respect this minimum on first display, then update immediately. - -**Standardization**: All messages follow consistent conventions: -- No trailing punctuation -- Sentence case capitalization -- Present progressive tense (-ing) for ongoing actions -- Past tense for completed actions -- Consistent "X failed" format for error messages -- Consistent tone (direct, technical) - removed "Please" from wait messages -- Proper compound words ("geofenced" not "geo fenced") -- Correct spelling: "Acquired" (not "Aquired") +**Connection Status Bar**: 4 fixed messages (Connected, Connecting, Disconnected, Disconnecting) + +**Dynamic App Status Bar**: ~30+ unique message patterns covering: +- Capacity check: 9 messages (including session_id error messages) +- Channel setup: 4 messages +- GPS initialization: 3 messages +- Ping operations: 6 messages +- Countdown timers: 6 message patterns +- API/Map: 2 messages (including em dash placeholder) +- Auto mode: 3 messages +- Errors: Various context-specific messages + +**Key Behaviors**: +- Connection bar updates immediately +- Dynamic bar enforces 500ms minimum visibility (except countdown updates) +- Em dash (`—`) placeholder for empty dynamic status +- Connection words blocked from dynamic bar +- All error reasons appear WITHOUT "Disconnected:" prefix diff --git a/index-new.html b/index-new.html new file mode 100644 index 0000000..fd369bc --- /dev/null +++ b/index-new.html @@ -0,0 +1,237 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2818 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue;\f1\fnil\fcharset77 ZapfDingbatsITC;} +{\colortbl;\red255\green255\blue255;} +{\*\expandedcolortbl;;} +\margl1440\margr1440\vieww11520\viewh8400\viewkind0 +\deftab560 +\pard\pardeftab560\slleading20\partightenfactor0 + +\f0\fs26 \cf0 \ +\ +\ + \ + \ + \ + MeshCore Wardrive (Ottawa)\ + \ + \ + \ + \ + \ + \ + \ + \ +\ + \ + \ + \ +\ + \ + \ +\ + \ +\ +\ +\ +
\ +
\ +\ + \ +
\ +

\ + MeshCore\ + MeshCore Wardrive\ +

\ + v1.0\ +
\ +\ + \ +
\ +
\ + \uc0\u9679 \ + Disconnected\ +
\ + \ +
\ +\ + \ + \ +\ + \ + \ +\ + \ +
\ + \ + \ + \ + \ +
\ + \ +
\ + \'b1-\ +
\ + \ + \ +
\ +
-
\ +
-
\ +
\ + \ + \ +
\ + -\ +
\ +
\ +
\ +\ + \ +
\ +

\ + Select radio power to connect\ +

\ +\ + \ + \ +\ + \ + \ +
\ +\ + \ +
\ +
\ +

Session Pings

\ +
\ +
    \ + \ +
\ +
\ + \ + \ +
\ +

Notes

\ +
    \ +
  • Requires Bluetooth and Location permissions
  • \ +
  • Keep app in foreground with screen on & unlocked
  • \ +
  • YOW region only
  • \ +
  • Sends location to #wardriving for coverage map
  • \ +
  • \uc0\u9888 \u65039 Not supported in Safari \'97 Use Bluefy on iOS
  • \ +
\ +

\ + Fork of kallanreed/mesh-map,\ + modified for meshmapper.net\ +

\ +
\ +\ +
\ +
\ + \ + \ + \ + \ +\ +} \ No newline at end of file diff --git a/index.html b/index.html index 0a6b374..5723b2f 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,3 @@ - @@ -13,6 +12,7 @@ +
-
- - -
-

-
-

- MeshCore Wardrive

-
+
+ + +
+

+ MeshCore + MeshCore Wardrive

+ v1.0
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Status:Disconnected
Device:-
Channel:-
GPS:-
Accuracy:-
Distance from last ping:-
- - -
- - - - -
- -
- Auto interval: - - - - - - + +
+
+

Settings

+ +
+ + +
+ +
+ + + +
- - -
- Radio power: - - - - - - - -
- - -
- -

- This preview recenters after each ping is sent. -

-
+
- -
-
-

Session Pings

-
-
    - -
+ +
+
- - -
-
- This project is a fork of kallanreed/mesh-map. + + +
+ + + + +
+ +
+ ±- +
+ + +
+ ∆- +
-
- MrAlders0n forked this and modified it to work with the Ottawa-built - meshmapper.net. + + +
+
+
-
+
-
- Only for YOW region. - See here for more info. +
+ + +
+ +
+ +
-
- Sends location to #wardriving to build the - coverage map. + + + +
+ + +
+ +
+
+

Session Log

+ | + 0 pings + +
+
+ + + + +
-
- - Does not work in Safari. Use - Bluefy on iOS. - + + + -
+
+ + +
+

Notes

+
    +
  • Requires Bluetooth and Location permissions
  • +
  • Keep app in foreground with screen on & unlocked
  • +
  • YOW region only
  • +
  • Sends location to #wardriving for coverage map
  • +
  • ⚠️ Not supported in Safari — Use Bluefy on iOS
  • +
+

+ Fork of kallanreed/mesh-map, + modified for meshmapper.net +

+

diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b414156 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1131 @@ +{ + "name": "meshcore-gome-wardriver", + "version": "1.4.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "meshcore-gome-wardriver", + "version": "1.4.1", + "license": "MIT", + "devDependencies": { + "@tailwindcss/cli": "^4.1.0", + "tailwindcss": "^4.1.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.18.tgz", + "integrity": "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "enhanced-resolve": "^5.18.3", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.1.18" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9d570c2 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "meshcore-gome-wardriver", + "version": "1.4.1", + "description": "Browser-based Progressive Web App for wardriving with MeshCore devices", + "private": true, + "scripts": { + "build:css": "npx @tailwindcss/cli -i content/tailwind-in.css -o content/tailwind.css", + "watch:css": "npx @tailwindcss/cli -i content/tailwind-in.css -o content/tailwind.css --watch" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.1.0", + "tailwindcss": "^4.1.0" + }, + "keywords": [ + "meshcore", + "wardrive", + "bluetooth", + "pwa" + ], + "author": "MrAlders0n", + "license": "MIT" +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..d461b79 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./index.html", + "./index-new.html", + "./content/**/*.js", + "./content/**/*.html" + ] +}