+
+
+
+
+
±-
-
-
-
-
-
+
+
+
From 13224f735fd0de6cd784a74fe03b18e53cc5bdd2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 13:59:22 +0000
Subject: [PATCH 64/92] Rebuild Tailwind CSS to include z-10 and top-2 classes
The z-10 and top-2 classes used in the GPS overlay elements were not
present in the compiled CSS. Rebuilt tailwind.css to include these
classes so the top-right overlays display correctly.
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/tailwind.css | 58 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
diff --git a/content/tailwind.css b/content/tailwind.css
index d96994b..ad6f0fb 100644
--- a/content/tailwind.css
+++ b/content/tailwind.css
@@ -37,6 +37,8 @@
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
+ --text-base: 1rem;
+ --text-base--line-height: calc(1.5 / 1);
--text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem;
@@ -228,6 +230,9 @@
.static {
position: static;
}
+ .top-2 {
+ top: calc(var(--spacing) * 2);
+ }
.right-2 {
right: calc(var(--spacing) * 2);
}
@@ -237,6 +242,9 @@
.left-2 {
left: calc(var(--spacing) * 2);
}
+ .z-10 {
+ z-index: 10;
+ }
.mt-0\.5 {
margin-top: calc(var(--spacing) * 0.5);
}
@@ -264,6 +272,9 @@
.inline {
display: inline;
}
+ .h-6 {
+ height: calc(var(--spacing) * 6);
+ }
.h-8 {
height: calc(var(--spacing) * 8);
}
@@ -273,6 +284,9 @@
.min-h-screen {
min-height: 100vh;
}
+ .w-6 {
+ width: calc(var(--spacing) * 6);
+ }
.w-8 {
width: calc(var(--spacing) * 8);
}
@@ -306,6 +320,9 @@
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ .flex-col {
+ flex-direction: column;
+ }
.flex-wrap {
flex-wrap: wrap;
}
@@ -371,14 +388,34 @@
.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-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);
}
@@ -448,6 +485,9 @@
.py-0\.5 {
padding-block: calc(var(--spacing) * 0.5);
}
+ .py-1\.5 {
+ padding-block: calc(var(--spacing) * 1.5);
+ }
.py-2 {
padding-block: calc(var(--spacing) * 2);
}
@@ -457,6 +497,9 @@
.text-center {
text-align: center;
}
+ .text-right {
+ text-align: right;
+ }
.font-mono {
font-family: var(--font-mono);
}
@@ -464,6 +507,10 @@
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
+ .text-base {
+ font-size: var(--text-base);
+ line-height: var(--tw-leading, var(--text-base--line-height));
+ }
.text-lg {
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
@@ -622,6 +669,17 @@
background-color: var(--color-emerald-600);
}
}
+ .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-space-y-reverse {
syntax: "*";
From 88d94dabc0feea9c3cc72923d411bc165a9191a3 Mon Sep 17 00:00:00 2001
From: MrAlders0n
Date: Sun, 21 Dec 2025 09:06:24 -0500
Subject: [PATCH 65/92] Update distance UI format to include delta symbol for
clarity
---
content/wardrive.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/content/wardrive.js b/content/wardrive.js
index 1ee4514..a7df39e 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -753,7 +753,7 @@ function updateDistanceUi() {
if (distance === null) {
distanceInfoEl.textContent = "-";
} else {
- distanceInfoEl.textContent = `${Math.round(distance)}m away`;
+ distanceInfoEl.textContent = `∆${Math.round(distance)}m`;
}
}
From e0318a3839265bc47bc59d9940954fd10551f3f9 Mon Sep 17 00:00:00 2001
From: MrAlders0n
Date: Sun, 21 Dec 2025 09:08:18 -0500
Subject: [PATCH 66/92] Update distance display to include delta symbol for
clarity
---
index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/index.html b/index.html
index a99abd6..40f5fbb 100644
--- a/index.html
+++ b/index.html
@@ -157,7 +157,7 @@ Settings<
- -
+ ∆-
From 53366cb29016c98d8573eaadc5f224476c0a29c7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 14:09:53 +0000
Subject: [PATCH 67/92] Initial plan
From 84e11804ddf55c430157e99eba0ad7d5196bf1fb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 14:14:12 +0000
Subject: [PATCH 68/92] Fix dynamic status bar font to match connection status
bar
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/wardrive.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/content/wardrive.js b/content/wardrive.js
index a7df39e..e639cca 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -229,7 +229,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;
From 281a3c607141a6557a2f8d8e7c5fe4c752f4580d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 14:34:31 +0000
Subject: [PATCH 69/92] Initial plan
From 441d7c219ff99e6876a84e4cc70171ea20f74c10 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 14:37:32 +0000
Subject: [PATCH 70/92] Fix manual ping skip status during auto mode
When a manual ping is blocked (GPS stale, outside geofence, or too close to last ping) during auto mode, the UI now resumes the auto ping countdown instead of staying stuck on the skip message.
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/wardrive.js | 27 +++++++++++++++++++++++++++
docs/STATUS_MESSAGES.md | 6 ++++++
2 files changed, 33 insertions(+)
diff --git a/content/wardrive.js b/content/wardrive.js
index 65f5a84..63a6631 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -1874,6 +1874,15 @@ async function sendPing(manual = false) {
if (!manual && state.running) {
scheduleNextAutoPing();
}
+ // For manual ping during auto mode, resume the paused countdown
+ if (manual && state.running) {
+ debugLog("Manual ping failed (no GPS) during auto mode - resuming auto countdown");
+ const resumed = resumeAutoCountdown();
+ if (!resumed) {
+ debugLog("No paused countdown to resume, scheduling new auto ping");
+ scheduleNextAutoPing();
+ }
+ }
return;
}
@@ -1890,6 +1899,15 @@ async function sendPing(manual = false) {
if (manual) {
// Manual ping: show skip message that persists
setDynamicStatus("Ping skipped, outside of geofenced region", STATUS_COLORS.warning);
+ // If auto mode is running, resume the paused countdown
+ 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();
+ }
+ }
} else if (state.running) {
// Auto ping: schedule next ping and show countdown with skip message
scheduleNextAutoPing();
@@ -1910,6 +1928,15 @@ async function sendPing(manual = false) {
if (manual) {
// Manual ping: show skip message that persists
setDynamicStatus("Ping skipped, too close to last ping", STATUS_COLORS.warning);
+ // If auto mode is running, resume the paused countdown
+ 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();
+ }
+ }
} else if (state.running) {
// Auto ping: schedule next ping and show countdown with skip message
scheduleNextAutoPing();
diff --git a/docs/STATUS_MESSAGES.md b/docs/STATUS_MESSAGES.md
index d44ecc9..5098d10 100644
--- a/docs/STATUS_MESSAGES.md
+++ b/docs/STATUS_MESSAGES.md
@@ -234,12 +234,18 @@ These messages appear in the Dynamic App Status Bar. They NEVER include connecti
- **Message**: `"Ping skipped, outside of geofenced region"`
- **Color**: Amber (warning)
- **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
- **Message**: `"Ping skipped, too close to last ping"`
- **Color**: Amber (warning)
- **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
From 1d62bf3e85a98a7505c3ffbac79d345029e840ce Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 14:40:57 +0000
Subject: [PATCH 71/92] Refactor: Extract duplicate countdown resume logic
Created handleManualPingFailureDuringAutoMode() helper function to eliminate code duplication across three manual ping failure paths.
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/wardrive.js | 42 +++++++++++++++++++-----------------------
1 file changed, 19 insertions(+), 23 deletions(-)
diff --git a/content/wardrive.js b/content/wardrive.js
index 63a6631..cf8751a 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -392,6 +392,21 @@ function resumeAutoCountdown() {
return false;
}
+/**
+ * Handle manual ping failure 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
+ */
+function handleManualPingFailureDuringAutoMode() {
+ 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;
@@ -1875,13 +1890,8 @@ async function sendPing(manual = false) {
scheduleNextAutoPing();
}
// For manual ping during auto mode, resume the paused countdown
- if (manual && state.running) {
- debugLog("Manual ping failed (no GPS) during auto mode - resuming auto countdown");
- const resumed = resumeAutoCountdown();
- if (!resumed) {
- debugLog("No paused countdown to resume, scheduling new auto ping");
- scheduleNextAutoPing();
- }
+ if (manual) {
+ handleManualPingFailureDuringAutoMode();
}
return;
}
@@ -1900,14 +1910,7 @@ async function sendPing(manual = false) {
// Manual ping: show skip message that persists
setDynamicStatus("Ping skipped, outside of geofenced region", STATUS_COLORS.warning);
// If auto mode is running, resume the paused countdown
- 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();
- }
- }
+ handleManualPingFailureDuringAutoMode();
} else if (state.running) {
// Auto ping: schedule next ping and show countdown with skip message
scheduleNextAutoPing();
@@ -1929,14 +1932,7 @@ async function sendPing(manual = false) {
// Manual ping: show skip message that persists
setDynamicStatus("Ping skipped, too close to last ping", STATUS_COLORS.warning);
// If auto mode is running, resume the paused countdown
- 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();
- }
- }
+ handleManualPingFailureDuringAutoMode();
} else if (state.running) {
// Auto ping: schedule next ping and show countdown with skip message
scheduleNextAutoPing();
From ded68e1137730a81a8a8b002a547b3748eac77f6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 14:42:03 +0000
Subject: [PATCH 72/92] Add JSDoc documentation to helper function
Added comprehensive JSDoc comments to handleManualPingFailureDuringAutoMode() following codebase conventions.
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/wardrive.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/content/wardrive.js b/content/wardrive.js
index cf8751a..f1f2658 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -395,6 +395,13 @@ function resumeAutoCountdown() {
/**
* Handle manual ping failure 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, 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 handleManualPingFailureDuringAutoMode() {
if (state.running) {
From 8491055e5c2a223314c17a98be3da13e66dc5e6a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 14:43:36 +0000
Subject: [PATCH 73/92] Improve function naming for clarity
Renamed handleManualPingFailureDuringAutoMode to handleManualPingBlockedDuringAutoMode to better reflect that it handles blocked pings, not failures. Updated JSDoc to clarify it handles GPS unavailable, geofence, and distance checks.
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/wardrive.js | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/content/wardrive.js b/content/wardrive.js
index f1f2658..abcfc7c 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -393,17 +393,17 @@ function resumeAutoCountdown() {
}
/**
- * Handle manual ping failure during auto mode by resuming the paused countdown
+ * 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, this function:
+ * 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 handleManualPingFailureDuringAutoMode() {
+function handleManualPingBlockedDuringAutoMode() {
if (state.running) {
debugLog("Manual ping blocked during auto mode - resuming auto countdown");
const resumed = resumeAutoCountdown();
@@ -1898,7 +1898,7 @@ async function sendPing(manual = false) {
}
// For manual ping during auto mode, resume the paused countdown
if (manual) {
- handleManualPingFailureDuringAutoMode();
+ handleManualPingBlockedDuringAutoMode();
}
return;
}
@@ -1917,7 +1917,7 @@ async function sendPing(manual = false) {
// Manual ping: show skip message that persists
setDynamicStatus("Ping skipped, outside of geofenced region", STATUS_COLORS.warning);
// If auto mode is running, resume the paused countdown
- handleManualPingFailureDuringAutoMode();
+ handleManualPingBlockedDuringAutoMode();
} else if (state.running) {
// Auto ping: schedule next ping and show countdown with skip message
scheduleNextAutoPing();
@@ -1939,7 +1939,7 @@ async function sendPing(manual = false) {
// Manual ping: show skip message that persists
setDynamicStatus("Ping skipped, too close to last ping", STATUS_COLORS.warning);
// If auto mode is running, resume the paused countdown
- handleManualPingFailureDuringAutoMode();
+ handleManualPingBlockedDuringAutoMode();
} else if (state.running) {
// Auto ping: schedule next ping and show countdown with skip message
scheduleNextAutoPing();
From a13be9f45cf0e31f0b18abc6291090dad55dc4c8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 17:13:59 +0000
Subject: [PATCH 74/92] Initial plan
From 4607b5d7479856eb728d484b337c366da5d5d0b9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 17:17:59 +0000
Subject: [PATCH 75/92] Add session_id support to wardrive client
- Add state.wardriveSessionId to store session ID from capacity check
- Modify checkCapacity() to capture session_id from API response
- Add validation: fail and disconnect if allowed=true but session_id missing
- Add session_id parameter to all postToMeshMapperAPI() calls
- Add validation: fail and disconnect if posting without valid session_id
- Clear session_id on disconnect in both cleanupAllTimers() and disconnect handler
- Add debug logging for session_id lifecycle (receive, store, attach, clear)
- Add new error disconnect reasons: "session_id_error"
- Update STATUS_MESSAGES.md with new status messages
- Implement fail-closed policy for missing session_id
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/wardrive.js | 52 +++++++++++++++++++++++++++++++++++++----
docs/STATUS_MESSAGES.md | 22 +++++++++++++++--
2 files changed, 68 insertions(+), 6 deletions(-)
diff --git a/content/wardrive.js b/content/wardrive.js
index abcfc7c..b699165 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -143,7 +143,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: {
@@ -505,6 +506,9 @@ function cleanupAllTimers() {
// Clear device public key
state.devicePublicKey = null;
+
+ // Clear wardrive session ID
+ state.wardriveSessionId = null;
}
function enableControls(connected) {
@@ -1174,11 +1178,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;
@@ -1205,6 +1231,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,
@@ -1214,10 +1252,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",
@@ -2264,6 +2303,10 @@ async function connect() {
debugLog("Branch: slot_revoked");
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");
setDynamicStatus("Unable to read device public key; try again", STATUS_COLORS.error, true);
@@ -2295,6 +2338,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
diff --git a/docs/STATUS_MESSAGES.md b/docs/STATUS_MESSAGES.md
index 5098d10..7ac3270 100644
--- a/docs/STATUS_MESSAGES.md
+++ b/docs/STATUS_MESSAGES.md
@@ -149,6 +149,24 @@ These messages appear in the Dynamic App Status Bar. They NEVER include connecti
- **Terminal State**: Yes
- **Notes**: Triggers automatic disconnect
+##### 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()`
+
#### 2. Channel Setup Messages
##### Looking for #wardriving channel
@@ -456,8 +474,8 @@ Status messages follow these consistent conventions:
**Connection Status Bar**: 4 fixed messages (Connected, Connecting, Disconnected, Disconnecting)
-**Dynamic App Status Bar**: ~30 unique message patterns covering:
-- Capacity check: 7 messages
+**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
From 6a6a64cfda974b6be1ebd5ac5cfa79127d46a4b5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 17:57:20 +0000
Subject: [PATCH 76/92] Initial plan
From 5765899f31241a3823749049f90a362d6402ec69 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 18:02:54 +0000
Subject: [PATCH 77/92] Add mobile-friendly bottom sheet session log UI with
chip-based layout
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/style.css | 92 ++++++++++++
content/tailwind.css | 247 ++++++++++++++++++++++++++++++++
content/wardrive.js | 328 +++++++++++++++++++++++++++++++++++++++----
index.html | 51 ++++++-
4 files changed, 689 insertions(+), 29 deletions(-)
diff --git a/content/style.css b/content/style.css
index 0d0c3cc..c4c1498 100644
--- a/content/style.css
+++ b/content/style.css
@@ -177,4 +177,96 @@ body,
.mesh-control .top-rpt-row div:last-child {
flex: 0 0 auto;
white-space: nowrap;
+}
+
+/* Mobile Session Log Bottom Sheet */
+#logBottomSheet.open {
+ transform: translateY(0);
+}
+
+/* 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;
+}
+
+/* Desktop log compatibility */
+@media (min-width: 768px) {
+ #logSummaryBar {
+ display: none;
+ }
}
\ No newline at end of file
diff --git a/content/tailwind.css b/content/tailwind.css
index ad6f0fb..fc7ebc7 100644
--- a/content/tailwind.css
+++ b/content/tailwind.css
@@ -27,6 +27,7 @@
--color-slate-300: oklch(86.9% 0.022 252.894);
--color-slate-400: oklch(70.4% 0.04 256.788);
--color-slate-500: oklch(55.4% 0.046 257.417);
+ --color-slate-600: oklch(44.6% 0.043 257.281);
--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);
@@ -50,7 +51,11 @@
--tracking-wide: 0.025em;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
+ --radius-2xl: 1rem;
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
--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);
}
@@ -204,6 +209,9 @@
}
}
@layer utilities {
+ .collapse {
+ visibility: collapse;
+ }
.visible {
visibility: visible;
}
@@ -230,21 +238,39 @@
.static {
position: static;
}
+ .inset-x-0 {
+ inset-inline: calc(var(--spacing) * 0);
+ }
.top-2 {
top: calc(var(--spacing) * 2);
}
+ .right-0 {
+ right: calc(var(--spacing) * 0);
+ }
.right-2 {
right: calc(var(--spacing) * 2);
}
+ .bottom-0 {
+ bottom: calc(var(--spacing) * 0);
+ }
.bottom-2 {
bottom: calc(var(--spacing) * 2);
}
+ .left-0 {
+ left: calc(var(--spacing) * 0);
+ }
.left-2 {
left: calc(var(--spacing) * 2);
}
.z-10 {
z-index: 10;
}
+ .z-40 {
+ z-index: 40;
+ }
+ .z-50 {
+ z-index: 50;
+ }
.mt-0\.5 {
margin-top: calc(var(--spacing) * 0.5);
}
@@ -272,6 +298,12 @@
.inline {
display: inline;
}
+ .h-1 {
+ height: calc(var(--spacing) * 1);
+ }
+ .h-4 {
+ height: calc(var(--spacing) * 4);
+ }
.h-6 {
height: calc(var(--spacing) * 6);
}
@@ -281,15 +313,24 @@
.max-h-48 {
max-height: calc(var(--spacing) * 48);
}
+ .max-h-\[70vh\] {
+ max-height: 70vh;
+ }
.min-h-screen {
min-height: 100vh;
}
+ .w-4 {
+ width: calc(var(--spacing) * 4);
+ }
.w-6 {
width: calc(var(--spacing) * 6);
}
.w-8 {
width: calc(var(--spacing) * 8);
}
+ .w-12 {
+ width: calc(var(--spacing) * 12);
+ }
.w-full {
width: 100%;
}
@@ -308,6 +349,16 @@
.grow {
flex-grow: 1;
}
+ .translate-y-full {
+ --tw-translate-y: 100%;
+ translate: var(--tw-translate-x) var(--tw-translate-y);
+ }
+ .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;
}
@@ -344,6 +395,9 @@
.gap-2 {
gap: calc(var(--spacing) * 2);
}
+ .gap-3 {
+ gap: calc(var(--spacing) * 3);
+ }
.gap-4 {
gap: calc(var(--spacing) * 4);
}
@@ -382,12 +436,19 @@
.rounded {
border-radius: 0.25rem;
}
+ .rounded-full {
+ border-radius: calc(infinity * 1px);
+ }
.rounded-lg {
border-radius: var(--radius-lg);
}
.rounded-xl {
border-radius: var(--radius-xl);
}
+ .rounded-t-2xl {
+ border-top-left-radius: var(--radius-2xl);
+ border-top-right-radius: var(--radius-2xl);
+ }
.rounded-t-xl {
border-top-left-radius: var(--radius-xl);
border-top-right-radius: var(--radius-xl);
@@ -440,12 +501,21 @@
.bg-sky-600 {
background-color: var(--color-sky-600);
}
+ .bg-slate-600 {
+ background-color: var(--color-slate-600);
+ }
.bg-slate-800\/80 {
background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 80%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-slate-800) 80%, transparent);
}
}
+ .bg-slate-800\/95 {
+ background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 95%, transparent);
+ @supports (color: color-mix(in lab, red, red)) {
+ background-color: color-mix(in oklab, var(--color-slate-800) 95%, transparent);
+ }
+ }
.bg-slate-900 {
background-color: var(--color-slate-900);
}
@@ -491,9 +561,15 @@
.py-2 {
padding-block: calc(var(--spacing) * 2);
}
+ .py-3 {
+ padding-block: calc(var(--spacing) * 3);
+ }
.pt-2 {
padding-top: calc(var(--spacing) * 2);
}
+ .pb-3 {
+ padding-bottom: calc(var(--spacing) * 3);
+ }
.text-center {
text-align: center;
}
@@ -592,6 +668,10 @@
.underline {
text-decoration-line: underline;
}
+ .shadow-2xl {
+ --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25));
+ 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,);
}
@@ -600,6 +680,24 @@
-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-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));
+ }
+ .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));
+ }
+ .duration-300 {
+ --tw-duration: 300ms;
+ transition-duration: 300ms;
+ }
+ .ease-out {
+ --tw-ease: var(--ease-out);
+ transition-timing-function: var(--ease-out);
+ }
.hover\:bg-amber-500 {
&:hover {
@media (hover: hover) {
@@ -642,6 +740,13 @@
}
}
}
+ .hover\:bg-slate-800 {
+ &:hover {
+ @media (hover: hover) {
+ background-color: var(--color-slate-800);
+ }
+ }
+ }
.hover\:text-slate-200 {
&:hover {
@media (hover: hover) {
@@ -680,6 +785,51 @@
line-height: var(--tw-leading, var(--text-xl--line-height));
}
}
+ .md\:block {
+ @media (width >= 48rem) {
+ display: block;
+ }
+ }
+ .md\:hidden {
+ @media (width >= 48rem) {
+ display: none;
+ }
+ }
+}
+@property --tw-translate-x {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-y {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-translate-z {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0;
+}
+@property --tw-rotate-x {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-rotate-y {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-rotate-z {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-skew-x {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-skew-y {
+ syntax: "*";
+ inherits: false;
}
@property --tw-space-y-reverse {
syntax: "*";
@@ -703,6 +853,71 @@
syntax: "*";
inherits: false;
}
+@property --tw-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-shadow-alpha {
+ syntax: "
";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-inset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-shadow-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-shadow-alpha {
+ syntax: "";
+ inherits: false;
+ initial-value: 100%;
+}
+@property --tw-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-inset-ring-color {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-inset-ring-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
+@property --tw-ring-inset {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ring-offset-width {
+ syntax: "";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --tw-ring-offset-color {
+ syntax: "*";
+ inherits: false;
+ initial-value: #fff;
+}
+@property --tw-ring-offset-shadow {
+ syntax: "*";
+ inherits: false;
+ initial-value: 0 0 #0000;
+}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -792,14 +1007,44 @@
syntax: "*";
inherits: false;
}
+@property --tw-duration {
+ syntax: "*";
+ inherits: false;
+}
+@property --tw-ease {
+ syntax: "*";
+ inherits: false;
+}
@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-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-translate-z: 0;
+ --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-tracking: 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-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
@@ -822,6 +1067,8 @@
--tw-backdrop-opacity: initial;
--tw-backdrop-saturate: initial;
--tw-backdrop-sepia: initial;
+ --tw-duration: initial;
+ --tw-ease: initial;
}
}
}
diff --git a/content/wardrive.js b/content/wardrive.js
index b699165..9950e02 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -119,6 +119,23 @@ setConnStatus("Disconnected", STATUS_COLORS.error);
const intervalSelect = $("intervalSelect"); // 15 / 30 / 60 seconds
const powerSelect = $("powerSelect"); // "", "0.3w", "0.6w", "1.0w"
+// Mobile Session Log Bottom Sheet selectors
+const logSummaryBar = $("logSummaryBar");
+const logBottomSheet = $("logBottomSheet");
+const logCollapseBtn = $("logCollapseBtn");
+const logScrollContainer = $("logScrollContainer");
+const logCount = $("logCount");
+const logLastTime = $("logLastTime");
+const logLastSnr = $("logLastSnr");
+const sessionPingsDesktop = $("sessionPingsDesktop");
+
+// Session log state
+const sessionLogState = {
+ entries: [], // Array of parsed log entries
+ isExpanded: false,
+ autoScroll: true
+};
+
// ---- State ----
const state = {
connection: null,
@@ -1729,6 +1746,228 @@ 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);
+
+ 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) {
+ 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);
+ } else {
+ entry.events.forEach(event => {
+ const chip = createChipElement(event.type, event.value);
+ chipsRow.appendChild(chip);
+ });
+ }
+
+ logEntry.appendChild(topRow);
+ logEntry.appendChild(chipsRow);
+
+ 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 = '—';
+ return;
+ }
+
+ const lastEntry = sessionLogState.entries[count - 1];
+ const date = new Date(lastEntry.timestamp);
+ logLastTime.textContent = date.toLocaleTimeString();
+
+ // Show SNR from first event if available
+ if (lastEntry.events.length > 0) {
+ const firstEvent = lastEntry.events[0];
+ logLastSnr.textContent = `${firstEvent.type} ${firstEvent.value.toFixed(1)}`;
+ logLastSnr.className = `text-xs font-mono ${getSnrSeverityClass(firstEvent.value).replace('snr-', 'text-')}`;
+ } else {
+ logLastSnr.textContent = 'None';
+ logLastSnr.className = 'text-xs font-mono text-slate-500';
+ }
+}
+
+/**
+ * Render all log entries to the mobile view
+ */
+function renderLogEntries() {
+ if (!sessionPingsEl) return;
+
+ sessionPingsEl.innerHTML = '';
+
+ // Render newest first for mobile
+ const entries = [...sessionLogState.entries].reverse();
+
+ entries.forEach(entry => {
+ const element = createLogEntryElement(entry);
+ sessionPingsEl.appendChild(element);
+ });
+
+ // Auto-scroll to top (newest)
+ if (sessionLogState.autoScroll && logScrollContainer) {
+ logScrollContainer.scrollTop = 0;
+ }
+}
+
+/**
+ * Toggle bottom sheet expanded/collapsed
+ */
+function toggleBottomSheet() {
+ sessionLogState.isExpanded = !sessionLogState.isExpanded;
+
+ if (logBottomSheet) {
+ if (sessionLogState.isExpanded) {
+ logBottomSheet.classList.add('open');
+ } else {
+ logBottomSheet.classList.remove('open');
+ }
+ }
+}
+
+/**
+ * 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();
+
+ // Also update desktop log if it exists
+ if (sessionPingsDesktop) {
+ const li = document.createElement('li');
+ li.textContent = logLine;
+ sessionPingsDesktop.appendChild(li);
+ sessionPingsDesktop.scrollTop = sessionPingsDesktop.scrollHeight;
+ }
+ }
+}
+
// ---- Ping ----
/**
* Acquire fresh GPS coordinates and update state
@@ -1849,7 +2088,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
@@ -1860,39 +2099,55 @@ 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();
+ }
+ }
+
+ // Also update desktop log if it exists
+ if (sessionPingsDesktop) {
+ const listItems = sessionPingsDesktop.querySelectorAll('li');
+ const lastItem = listItems[listItems.length - 1];
+ if (lastItem && lastItem.textContent.includes('...')) {
+ lastItem.textContent = `${logData.timestamp} | ${logData.lat},${logData.lon} | ${repeaterStr}`;
+ }
+ }
debugLog(`Updated ping log entry with repeater telemetry: ${repeaterStr}`);
}
@@ -2553,6 +2808,31 @@ export async function onLoad() {
});
});
+ // Mobile Session Log Bottom Sheet event listeners
+ if (logSummaryBar) {
+ logSummaryBar.addEventListener("click", () => {
+ debugLog("Log summary bar clicked - expanding bottom sheet");
+ toggleBottomSheet();
+ });
+ }
+
+ if (logCollapseBtn) {
+ logCollapseBtn.addEventListener("click", () => {
+ debugLog("Log collapse button clicked");
+ toggleBottomSheet();
+ });
+ }
+
+ // Close bottom sheet when tapping outside
+ if (logBottomSheet) {
+ logBottomSheet.addEventListener("click", (e) => {
+ if (e.target === logBottomSheet) {
+ debugLog("Clicked outside bottom sheet - collapsing");
+ toggleBottomSheet();
+ }
+ });
+ }
+
// Prompt location permission early (optional)
debugLog("Requesting initial location permission");
try {
diff --git a/index.html b/index.html
index 40f5fbb..2d62757 100644
--- a/index.html
+++ b/index.html
@@ -190,14 +190,55 @@ Settings<
-
-
+
+
+
+
+ 0 pings
+ |
+ No data
+
+
+
+
+
+
+
+
+
-
Session Pings
+ Session Pings
-
From 89ac5d3f6d1acbab7fa6d0e44eca870723f1dfeb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 18:08:37 +0000
Subject: [PATCH 78/92] Complete mobile session log UI with bottom sheet and
SNR-colored chips
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
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
From dd86e977a0f1dd9bf86542525a5d866515b03843 Mon Sep 17 00:00:00 2001
From: MrAlders0n
Date: Sun, 21 Dec 2025 13:16:39 -0500
Subject: [PATCH 79/92] Fix mobile session log bottom sheet transform property
for improved positioning
---
content/style.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/content/style.css b/content/style.css
index c4c1498..a23b781 100644
--- a/content/style.css
+++ b/content/style.css
@@ -181,7 +181,7 @@ body,
/* Mobile Session Log Bottom Sheet */
#logBottomSheet.open {
- transform: translateY(0);
+ translate: 0 0;
}
/* Log Entry Styling */
From f09a1af4825fab99fc51ac9821cc590acc4b52e4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 18:22:44 +0000
Subject: [PATCH 80/92] Initial plan
From 39cd890dd3045d9d516bf5ffff4a86ebf8329c0a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 21 Dec 2025 18:30:58 +0000
Subject: [PATCH 81/92] Fix session log expand/collapse and move to static
position
Co-authored-by: MrAlders0n <55921894+MrAlders0n@users.noreply.github.com>
---
content/style.css | 15 ++++------
content/wardrive.js | 69 ++++++++++++++++++---------------------------
index.html | 62 +++++++++++++---------------------------
3 files changed, 53 insertions(+), 93 deletions(-)
diff --git a/content/style.css b/content/style.css
index a23b781..7127bf9 100644
--- a/content/style.css
+++ b/content/style.css
@@ -179,9 +179,13 @@ body,
white-space: nowrap;
}
-/* Mobile Session Log Bottom Sheet */
+/* Session Log - Static Expandable Section */
#logBottomSheet.open {
- translate: 0 0;
+ display: block !important;
+}
+
+#logExpandArrow.expanded {
+ transform: rotate(180deg);
}
/* Log Entry Styling */
@@ -262,11 +266,4 @@ body,
.snr-green .chipSnr {
color: #4ade80;
font-weight: 600;
-}
-
-/* Desktop log compatibility */
-@media (min-width: 768px) {
- #logSummaryBar {
- display: none;
- }
}
\ No newline at end of file
diff --git a/content/wardrive.js b/content/wardrive.js
index 9950e02..5fb0e05 100644
--- a/content/wardrive.js
+++ b/content/wardrive.js
@@ -119,15 +119,13 @@ setConnStatus("Disconnected", STATUS_COLORS.error);
const intervalSelect = $("intervalSelect"); // 15 / 30 / 60 seconds
const powerSelect = $("powerSelect"); // "", "0.3w", "0.6w", "1.0w"
-// Mobile Session Log Bottom Sheet selectors
+// Session Log selectors
const logSummaryBar = $("logSummaryBar");
const logBottomSheet = $("logBottomSheet");
-const logCollapseBtn = $("logCollapseBtn");
const logScrollContainer = $("logScrollContainer");
const logCount = $("logCount");
const logLastTime = $("logLastTime");
const logLastSnr = $("logLastSnr");
-const sessionPingsDesktop = $("sessionPingsDesktop");
// Session log state
const sessionLogState = {
@@ -1906,14 +1904,23 @@ function updateLogSummary() {
}
/**
- * Render all log entries to the mobile view
+ * Render all log entries to the session log
*/
function renderLogEntries() {
if (!sessionPingsEl) return;
sessionPingsEl.innerHTML = '';
- // Render newest first for mobile
+ 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);
+ return;
+ }
+
+ // Render newest first
const entries = [...sessionLogState.entries].reverse();
entries.forEach(entry => {
@@ -1928,7 +1935,7 @@ function renderLogEntries() {
}
/**
- * Toggle bottom sheet expanded/collapsed
+ * Toggle session log expanded/collapsed
*/
function toggleBottomSheet() {
sessionLogState.isExpanded = !sessionLogState.isExpanded;
@@ -1936,8 +1943,20 @@ function toggleBottomSheet() {
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');
}
}
}
@@ -1957,14 +1976,6 @@ function addLogEntry(timestamp, lat, lon, eventsStr) {
sessionLogState.entries.push(entry);
renderLogEntries();
updateLogSummary();
-
- // Also update desktop log if it exists
- if (sessionPingsDesktop) {
- const li = document.createElement('li');
- li.textContent = logLine;
- sessionPingsDesktop.appendChild(li);
- sessionPingsDesktop.scrollTop = sessionPingsDesktop.scrollHeight;
- }
}
}
@@ -2140,15 +2151,6 @@ function updatePingLogWithRepeaters(logData, repeaters) {
}
}
- // Also update desktop log if it exists
- if (sessionPingsDesktop) {
- const listItems = sessionPingsDesktop.querySelectorAll('li');
- const lastItem = listItems[listItems.length - 1];
- if (lastItem && lastItem.textContent.includes('...')) {
- lastItem.textContent = `${logData.timestamp} | ${logData.lat},${logData.lon} | ${repeaterStr}`;
- }
- }
-
debugLog(`Updated ping log entry with repeater telemetry: ${repeaterStr}`);
}
@@ -2808,30 +2810,13 @@ export async function onLoad() {
});
});
- // Mobile Session Log Bottom Sheet event listeners
+ // Session Log event listener
if (logSummaryBar) {
logSummaryBar.addEventListener("click", () => {
- debugLog("Log summary bar clicked - expanding bottom sheet");
- toggleBottomSheet();
- });
- }
-
- if (logCollapseBtn) {
- logCollapseBtn.addEventListener("click", () => {
- debugLog("Log collapse button clicked");
+ debugLog("Log summary bar clicked - toggling session log");
toggleBottomSheet();
});
}
-
- // Close bottom sheet when tapping outside
- if (logBottomSheet) {
- logBottomSheet.addEventListener("click", (e) => {
- if (e.target === logBottomSheet) {
- debugLog("Clicked outside bottom sheet - collapsing");
- toggleBottomSheet();
- }
- });
- }
// Prompt location permission early (optional)
debugLog("Requesting initial location permission");
diff --git a/index.html b/index.html
index 2d62757..9ec5469 100644
--- a/index.html
+++ b/index.html
@@ -190,56 +190,34 @@ Settings<
-
-
-
-
- 0 pings
- |
- No data
-
-
-
-
-
-