Skip to content

Add safety heatmap page — safest areas visualization#123

Closed
amastbau wants to merge 3 commits intomaorcc:mainfrom
amastbau:feature/safety-heatmap
Closed

Add safety heatmap page — safest areas visualization#123
amastbau wants to merge 3 commits intomaorcc:mainfrom
amastbau:feature/safety-heatmap

Conversation

@amastbau
Copy link
Copy Markdown

@amastbau amastbau commented Apr 3, 2026

Summary

New page at /safety.html showing a color-coded heatmap of alert frequency per location, making it easy to see which areas in Israel have the fewest alerts.

  • Green circles: safest areas (0-5 alerts in the selected period)
  • Yellow: moderate alert zones (31-50)
  • Red: high-alert areas (80+)

Features

  • Adjustable time range (1-30 days) via slider
  • Top 10 safest locations sidebar
  • Alert distribution histogram
  • Click any location dot for exact alert count
  • Uses existing /api/day-history endpoint — no new backend needed
  • Dark theme consistent with the main map
  • Mobile-friendly responsive layout
  • RTL Hebrew UI

How it works

Fetches daily history for the selected range, aggregates alert counts per location using the existing oref_points.json coordinates, and renders L.circleMarker with color/radius based on alert count. Safe areas are drawn on top so they're always visible.

Screenshots

Access via /safety.html or link from the main map.

Test plan

  • Verify data loads for 1, 7, and 30 day ranges
  • Verify color coding matches legend
  • Verify popup shows correct count per location
  • Test on mobile viewport

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Hebrew RTL interactive safety map with dark basemap, marker color/size by alerts
    • Date-range slider (1–30 days, default 7), red/yellow/green filters, and total vs. average display mode
    • Ranked "10 safest areas" list, RTL popups, adaptive legend, and alert distribution chart
    • Loading overlay and live subtitle totals
  • Improvements

    • Graceful error handling with user-visible messages and resilient data rendering

New page at /safety.html that visualizes alert frequency per location
as a color-coded map:
- Green: safest areas (0-5 alerts)
- Yellow: moderate (31-50)
- Red: high alert zones (80+)

Features:
- Adjustable time range slider (1-30 days)
- Top 10 safest locations panel
- Alert distribution histogram
- Click any location for details
- Uses existing /api/day-history endpoint
- Dark theme matching the main app

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f52ca494-e56f-41dc-9f41-ffb83361076b

📥 Commits

Reviewing files that changed from the base of the PR and between a7b137f and 8592a09.

📒 Files selected for processing (1)
  • web/safety.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/safety.html

📝 Walkthrough

Walkthrough

Added a new Hebrew RTL Leaflet safety map page (web/safety.html) that loads locations from /oref_points.json, fetches per-day alerts from /api/day-history for a selectable 1–30 day window (default 7), classifies/aggregates alerts, and renders markers, popups, top-10 list, and distribution chart.

Changes

Cohort / File(s) Summary
Safety Map Page
web/safety.html
New standalone Hebrew RTL Leaflet page. Loads oref_points.json, requests per-day alert history via /api/day-history for a configurable date window, classifies alerts into red/yellow/green, aggregates counts per location, updates circle marker color/size and RTL popups, rebuilds top-10 safe locations list, updates legend/subtitle and distribution chart, shows a loading overlay, and handles fetch errors by logging and returning empty day arrays when needed.

Sequence Diagram

sequenceDiagram
    actor User
    participant Page as Safety Page
    participant Server as API Server
    participant Map as Leaflet Map
    participant Chart as Visualization

    User->>Page: Open `safety.html`
    Page->>Server: GET `/oref_points.json`
    Server-->>Page: Coordinates
    Page->>Map: Initialize dark RTL map & base layer
    Map-->>Page: Ready

    User->>Page: Change day range / filters
    activate Page
    Note over Page: Show loading overlay\nincrement request id
    loop for each day in range
        Page->>Server: GET `/api/day-history?date=...`
        Server-->>Page: Day alerts (or error -> empty)
        Page->>Page: Classify alerts and aggregate per location
    end
    Page->>Map: Update circle markers (color & radius)
    Page->>Chart: Update top-10 list & distribution chart
    Page->>Page: Update subtitle/legend, hide loading overlay
    deactivate Page
    Page-->>User: Updated map, list, and chart
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 קופץ על המפה בלילה שקטה,
סופר ימים, אותות, וכל אפקטה,
עיגולים זוהרים בצבע ובקוטר,
עשרה מקומות שקטים — לב מלא חוטר,
היי, שלום קטן ממני אל המפה החכמה.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: adding a safety heatmap page to visualize safest areas, which matches the PR's core objective of creating a new /safety.html visualization page.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
web/safety.html (1)

18-27: Add a small-screen layout mode for fixed panels.

Current fixed panel layout can overlap on mobile widths. A compact stacked layout will improve usability.

Example mobile media-query
+@media (max-width: 768px) {
+  `#controls`, `#stats-box`, `#chart-box`, `#back-link` {
+    position: static;
+    max-width: none;
+    margin: 8px;
+  }
+  `#map` {
+    height: calc(100% - 260px);
+  }
+}

Also applies to: 85-97, 145-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/safety.html` around lines 18 - 27, The fixed `#controls` panel can overlap
on small screens; add a mobile media query (e.g. `@media` (max-width: 600px)) that
makes `#controls` and the other fixed-panel blocks referenced in the diff switch
to a compact stacked layout: keep position: fixed but set left: 12px (or 8px)
and right: auto, reduce top/right offsets if needed, set max-width: calc(100% -
24px) (or width: 100%), decrease gap (e.g. gap: 6px), and ensure child panels
inside these selectors use width: 100% and box-sizing: border-box so they stack
cleanly; apply the same media-query adjustments to the other fixed-panel rule
blocks mentioned (lines 85-97 and 145-156) to keep consistent behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/safety.html`:
- Around line 325-359: The loadData function can have overlapping async runs
that let a slower call overwrite newer results; add a request-token guard:
generate a unique token at start of loadData (e.g., const reqId = Symbol() or a
monotonically increasing number), store it to a shared variable like
currentRequestId, and before applying results (setting alertData and calling
renderMap and hiding loading) verify reqId === currentRequestId; if not, abort
applying the stale data. Apply the same token check at any other completion
paths mentioned (e.g., the code around lines 380-383) so only the most recent
loadData run updates UI state (currentDays, alertData, renderMap, and hiding
`#loading`).
- Around line 362-371: The fetch chain that loads /oref_points.json currently
has no error handling so failures leave initialization and the loading overlay
stuck; add a .catch handler to the promise chain (or use try/catch with
async/await) around the fetch → r.json sequence to set locationPoints to an
empty object or a sensible default, log the error, and ensure loadData(7) is
called regardless of success or failure; also ensure any loading overlay is
cleared in the error path (call the existing hide/remove overlay helper or
ensure loadData/its finally block clears it). Use the existing symbols
fetch('/oref_points.json'), locationPoints, and loadData to implement this
recovery path.
- Line 16: The CSS rule `.leaflet-interactive:focus { outline: none; }` removes
keyboard focus indication and must be replaced with a visible, high-contrast
focus style; update the `.leaflet-interactive` focus selector (preferably
`.leaflet-interactive:focus, .leaflet-interactive:focus-visible`) to provide a
clear outline or box-shadow (e.g., 2px solid or an accessible box-shadow color)
and ensure the color/contrast meets accessibility standards so keyboard users
can see focus on interactive map elements.
- Around line 196-201: The legend shows red as "80+" but the rendering logic
still treats 80 as orange; update the threshold checks used by the marker color
and histogram binning so that 80 is classified as red. Specifically, find the
functions that determine bar/marker colors (e.g., getColorForAlertCount,
markerColorForAlerts, or the histogram binning logic) and change the orange
upper-bound check from <= 80 (or < 81) to < 80, and/or change the red check to
use >= 80 (instead of > 80); ensure all places that compute colors/bins (markers
and histogram rendering) use the same comparison so 80 maps to the red "#f44336"
legend entry.

---

Nitpick comments:
In `@web/safety.html`:
- Around line 18-27: The fixed `#controls` panel can overlap on small screens; add
a mobile media query (e.g. `@media` (max-width: 600px)) that makes `#controls` and
the other fixed-panel blocks referenced in the diff switch to a compact stacked
layout: keep position: fixed but set left: 12px (or 8px) and right: auto, reduce
top/right offsets if needed, set max-width: calc(100% - 24px) (or width: 100%),
decrease gap (e.g. gap: 6px), and ensure child panels inside these selectors use
width: 100% and box-sizing: border-box so they stack cleanly; apply the same
media-query adjustments to the other fixed-panel rule blocks mentioned (lines
85-97 and 145-156) to keep consistent behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f5663781-12f2-4d95-b3b9-81ed0cea8028

📥 Commits

Reviewing files that changed from the base of the PR and between 48aed8b and 14bbb94.

📒 Files selected for processing (1)
  • web/safety.html

Comment on lines +362 to +371
// Load location points then start
fetch('/oref_points.json')
.then(r => r.json())
.then(points => {
locationPoints = {};
for (const [name, coords] of Object.entries(points)) {
locationPoints[name] = [coords[0], coords[1]];
}
return loadData(7);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle initial points-load failures to avoid a stuck loading state.

If /oref_points.json fails, initialization has no recovery path and the loading overlay can remain indefinitely.

Bootstrap error handling
 fetch('/oref_points.json')
   .then(r => r.json())
   .then(points => {
     locationPoints = {};
     for (const [name, coords] of Object.entries(points)) {
       locationPoints[name] = [coords[0], coords[1]];
     }
     return loadData(7);
-  });
+  })
+  .catch((err) => {
+    console.error('Failed to initialize safety map:', err);
+    const loading = document.getElementById('loading');
+    loading.textContent = 'שגיאה בטעינת נתונים';
+    loading.style.display = 'block';
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/safety.html` around lines 362 - 371, The fetch chain that loads
/oref_points.json currently has no error handling so failures leave
initialization and the loading overlay stuck; add a .catch handler to the
promise chain (or use try/catch with async/await) around the fetch → r.json
sequence to set locationPoints to an empty object or a sensible default, log the
error, and ensure loadData(7) is called regardless of success or failure; also
ensure any loading overlay is cleared in the error path (call the existing
hide/remove overlay helper or ensure loadData/its finally block clears it). Use
the existing symbols fetch('/oref_points.json'), locationPoints, and loadData to
implement this recovery path.

Features:
- Add alert type filters (red/yellow/green) to control which alerts are counted
- Yellow filter allows excluding preliminary warnings like early Iran alerts

Fixes:
- Fix race condition when rapidly changing filters/slider
- Add error handling for location points fetch failure
- Fix accessibility: replace outline:none with visible focus style
- Fix color threshold: 80 alerts now correctly shows as red (was orange)
- Add mobile responsive layout for better small-screen UX

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
web/safety.html (1)

259-260: ⚠️ Potential issue | 🟡 Minor

Update range labels to match the < 80 boundary.

The code now correctly treats 80 as red, but labels still say 51-80. This should be 51-79 in legend and chart labels to avoid UI inconsistency.

Suggested fix
-    <div class="row"><div class="bar" style="background:`#ff9800`"></div> רב התרעות (51-80)</div>
+    <div class="row"><div class="bar" style="background:`#ff9800`"></div> רב התרעות (51-79)</div>
-  const bucketLabels = ['0-5', '6-15', '16-30', '31-50', '51-80', '80+'];
+  const bucketLabels = ['0-5', '6-15', '16-30', '31-50', '51-79', '80+'];

Also applies to: 387-387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/safety.html` around lines 259 - 260, Update the range label that
currently reads "51-80" to "51-79" so it matches the logic that treats 80 as the
red category; locate the two legend/chart label occurrences (the HTML divs
containing the text "רב התרעות (51-80)" and the duplicate at the other
occurrence) and change their displayed range text to "51-79" to keep UI labels
consistent with the category boundaries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/safety.html`:
- Around line 329-331: The function that maps alert titles currently defaults
unknown titles to return 'red' without logging; update the mapping logic that
contains the return 'red' to also emit a console.warn (or processLogger.warn)
with the unknown title (e.g., "Unknown alert title: <title>, defaulting to red")
before returning 'red' so classifier drift is recorded; locate the branch that
falls through to return 'red' and add the warning using the alert title
variable.
- Around line 324-327: The yellow-alert matcher is missing the required
substring "להישאר בסמיכות", causing misclassification; update the conditional
checks that inspect the normalized variable (the normalized.includes(...)
checks) to also return 'yellow' when normalized.includes('להישאר בסמיכות') (in
addition to or instead of the existing 'להישאר בקרבתו' check) so yellow alert
titles match correctly.

---

Duplicate comments:
In `@web/safety.html`:
- Around line 259-260: Update the range label that currently reads "51-80" to
"51-79" so it matches the logic that treats 80 as the red category; locate the
two legend/chart label occurrences (the HTML divs containing the text "רב התרעות
(51-80)" and the duplicate at the other occurrence) and change their displayed
range text to "51-79" to keep UI labels consistent with the category boundaries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9eaf1949-57a8-48fa-9354-75931ac55d48

📥 Commits

Reviewing files that changed from the base of the PR and between 14bbb94 and a7b137f.

📒 Files selected for processing (1)
  • web/safety.html

Comment on lines +324 to +327
if (normalized.includes('בדקות הקרובות צפויות להתקבל התרעות')) return 'yellow';
if (normalized.includes('לשפר את המיקום למיגון המיטבי')) return 'yellow';
if (normalized.includes('יש לשהות בסמיכות למרחב המוגן')) return 'yellow';
if (normalized.includes('להישאר בקרבתו')) return 'yellow';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add the required yellow substring match for להישאר בסמיכות.

Line 327 matches להישאר בקרבתו, but the required yellow matcher is להישאר בסמיכות; this can misclassify yellow alerts as red and skew counts.

Suggested fix
-  if (normalized.includes('להישאר בקרבתו')) return 'yellow';
+  if (normalized.includes('להישאר בסמיכות')) return 'yellow';
+  if (normalized.includes('להישאר בקרבתו')) return 'yellow';

As per coding guidelines, "Match yellow alert titles by exact string or substring: לשפר את המיקום למיגון המיטבי, להישאר בסמיכות".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/safety.html` around lines 324 - 327, The yellow-alert matcher is missing
the required substring "להישאר בסמיכות", causing misclassification; update the
conditional checks that inspect the normalized variable (the
normalized.includes(...) checks) to also return 'yellow' when
normalized.includes('להישאר בסמיכות') (in addition to or instead of the existing
'להישאר בקרבתו' check) so yellow alert titles match correctly.

Comment on lines +329 to +331
// Red (danger) — default for most alerts
return 'red';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Log unknown alert titles before defaulting to red.

Line 330 defaults unknown titles to red, but there is no warning log, which makes classifier drift harder to detect.

Suggested fix
-  // Red (danger) — default for most alerts
-  return 'red';
+  // Red (danger) — default for unknown alerts
+  console.warn('Unknown alert title:', normalized);
+  return 'red';

As per coding guidelines, "Default unknown alert titles to red and log a console warning".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/safety.html` around lines 329 - 331, The function that maps alert titles
currently defaults unknown titles to return 'red' without logging; update the
mapping logic that contains the return 'red' to also emit a console.warn (or
processLogger.warn) with the unknown title (e.g., "Unknown alert title: <title>,
defaulting to red") before returning 'red' so classifier drift is recorded;
locate the branch that falls through to return 'red' and add the warning using
the alert title variable.

- Toggle between total alerts and average alerts per day
- Per-day mode normalizes counts for fair comparison across time ranges
- Legend, chart, popups, and stats all adapt to selected mode
- Tighter per-day thresholds (0.1, 0.3, 0.7, 1.5, 3) for better color spread

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Repository owner deleted a comment from coderabbitai bot Apr 3, 2026
@maorcc
Copy link
Copy Markdown
Owner

maorcc commented Apr 5, 2026

היי @amastbau, תודה על העבודה, הדף בנוי טוב והרעיון מעניין.

אני מתלבט לגבי מיזוג, ורוצה להיות ישיר לגבי הסיבה: לדעתי הפיצ'ר פונה לקהל יעד שונה ממה שמפת העורף נועדה לו. המפה בנויה לאנשים שנמצאים באזור פעיל או בסמוך אליו, כלי בזמן אמת, וגם פיצ'רי ההיסטוריה קיימים כדי לשחזר "מה קרה בשעה האחרונה". המשתמש שהמפה מכוונת אליו בודק אותה תוך כדי התרעה או מיד אחריה.

המפת חום הזו היא בעצם כלי לניתוח היסטורי, שעונה על שאלה כמו "אילו אזורים היו הכי שקטים בחודש האחרון?" זו שאלה לגיטימית ושימושית, אבל מי ששואל אותה הוא חוקר, עיתונאי, או מישהו שמתכנן לאן לעבור לגור. זה קהל יעד שונה, ואני חושב שהוא ראוי לכלי שבנוי סביבו מלכתחילה.

גם הכיוון הוויזואלי שונה, עם הרקע הכהה, הסליידרים, תרשים ההתפלגות. זה קרוב יותר ברוחו לוויזואליזציות הסטטיסטיות שיובל הרפז מפרסם (למשל alarms_by_year) ו-Oct7 Database. אני חושב שהעבודה שלך יכולה להתאים טוב לצד הפרויקטים האלה.

אם תרצה, אשמח לחבר אותך עם יובל, הוא עובד בדיוק בתחום הזה ואולי יהיה שיתוף פעולה מעניין.

@amastbau
Copy link
Copy Markdown
Author

amastbau commented Apr 7, 2026

קיבל את ה דף בשמחה :D באמת מקום טוב יותר.תודה על ההפניה.סוגר את זה.
אוהב מאוד את הפרויקט הזה שלך פה.משתמש במפה כל הזמן.גם בנסיעה.מאוד מאוד נוח!

@maorcc

@amastbau amastbau closed this Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants