Building Glass on the Street
Minneapolis has a car break-in problem, and it really feels like city hall does not care. I believe there is a pattern on when/where these break ins are happening but no way to share or visualize the information. You hear about the smashed window on your own block or the next block and think 'Isn't there something we can do about this?'. We decided to write a tracking website called Glass on the Street as a way to have users enter when they see or have been victim to the someone riffeling through their car or smashing their car windows for a smash-and-grab.
I ran the planning through Claude Code before writing anything, mostly because this is a pretty decent sized application and I don't have a lot of extra time. Some questions I asked Claude before we began: what counts as a "break-in" (broken glass only, or does someone rifling through an unlocked car count too), how to keep location information without sharing the exact location of the user - privacy first and how to make sure the thing didn't come out looking like every other AI-scaffolded dashboard with a sidebar of stat cards and a purple gradient.
Here's the planning that I went through with Claude to design the system and website (coming soon).
The stack
ASP.NET Core MVC and MySQL. I've been writing .NET for a long time and didn't see a reason to reach for anything else.
Report flow
Date, location, and what happened, required. What happened is either "window smashed" or "rifled through, nothing broken." I think this is important information to have because in my experience if the door was locked, there was a 50/50 chance the window was going to get smashed.
Location comes from tapping the map or searching an address, which gets geocoded through Nominatim. Everything else, time of day, whether anything got stolen, whether it's already reported to police, cross streets, a photo, is optional.
Privacy
We don't store any user or location data other than the closest intersection to the incident reported. The point you tap only exists for the length of the HTTP request, long enough to get snapped to the block and then it's gone. What gets written to the table is the block point and nothing else.
The map
MapLibre GL for now. Vector tiles mean I can actually restyle the thing instead of living with default markers and a default basemap. Pins toggle to a heatmap once there's enough data for that to be useful. Date filtering is presets (7/30/90 days, all time) plus a custom range, and there's a stat line above the map showing count and percent change.
Trust and spam
Rate limiting on submissions and moderation. A flag button on every pin so bad entries can get reported by users so it's not just caught by me. There's a basic-auth admin view to pull flagged reports.
Design
The one hard requirement was this couldn't look like something an AI scaffolded in ten minutes. I went editorial style instead. Something closer to how NYT or Axios present a data story than how a vibed SaaS dashboard does.
Next up is decifering the City of Minneapolis data portal and if it actually has the theft-from-vehicle/damage of motor vehicle dataset I want to seed it with which I haven't verified yet.
Here's the plan I worked from, for anyone curious what that looks like from the Claude Code perspective.
# Minneapolis Car Break-In Map — Production Build Plan
## Context
The project owner and the developer building it want a real site (not
another mockup) where Minneapolis residents report car break-ins by date
and location, shown on a filterable city map, so neighbors/block
clubs/journalists/city officials can see where break-ins cluster and
whether they're trending up or down. A clickable HTML prototype already
existed but got set aside in favor of building the real thing from scratch.
Two hard requirements, both from the original pitch, which took priority
over an earlier planning doc wherever they conflicted:
1. The report form's incident-type field must distinguish **window
smashed** vs. **rifled through (unlocked, no glass broken)** — an
earlier draft of the form dropped this; it's being restored.
2. The site must not look like a generic AI-generated dashboard. Chosen
direction: **data-journalism / editorial** (think NYT/Axios graphics) —
restrained chrome, a real typographic hierarchy, the map presented like
a piece of a published story rather than a SaaS tool.
Stack: **ASP.NET Core MVC (.NET 10) + MySQL**. Map library is
**MapLibre GL JS** (chosen over Leaflet specifically because vector-tile
styling lets us fully own the map's look instead of inheriting Leaflet's
default appearance, which is one of the "AI prototype" tells).
The project name is **Glass on the Street**.
## Scope for this build (v1)
Required, per the original pitch (priority source):
- Report form: date, location (required); incident type — window smashed
vs. rifled/unlocked (required); time of day, photo upload
(optional/nice).
- Map with pins, switchable to heatmap once volume is high.
- Date filter: presets (7/30/90 days, all) + custom range.
- Stats callout ("N reports in last 30 days, up X% vs. prior period").
- Privacy: never expose the precise address publicly — snap to
block/nearest intersection before it's ever returned by the API.
- Trust/spam basics: rate limiting, geofencing to the Minneapolis metro,
CAPTCHA on submit, admin delete, user-facing flag button.
- Post-submission link to MPD's online reporting.
Explicitly deferred to v2 (nice-to-have or unverified in the source
material, not cut — just not blocking v1):
- Importing Minneapolis open data (theft-from-vehicle incidents) to seed
the map. Dataset/fields haven't been verified yet. The schema supports
an importer being dropped in later (a `SourceType` column distinguishing
`UserReport` from `OfficialImport`), but the importer itself isn't
built in v1.
- Richer stats (top neighborhoods, time-of-day breakdown).
- Typed-address geocoding as an alternative to tap-to-place — actually
included in v1 since typing an address is table stakes for a public
form.
**Why MVC + Web API controllers in one project**: Razor serves the
server-rendered pages (landing, map shell, report form, admin), while the
map's live pin data and form submission go through thin JSON API
endpoints consumed by client-side JS (MapLibre needs GeoJSON, not
server-rendered HTML). No SPA framework needed.
## Report flow
1. Date (required)
2. Location: tap-to-place on the MapLibre map, or type an address/
intersection (geocoded via `GeocodingService` → Nominatim). Submission
rejected server-side if outside a Minneapolis metro bounding box.
3. Incident type: window smashed / rifled through — required, single
choice.
4. Optional: time of day, items stolen, police reported, cross streets,
photo upload.
5. CAPTCHA before submit.
6. On success: confirmation screen; if not reported to police, show the
MPD online-reporting link.
Server-side on POST: validate, geofence check, rate-limit check, snap
`Precise*` → `Display*` before persisting either — both columns get
written, but only `Display*` is ever read back out.
## Map page
- MapLibre GL JS, custom vector style — muted paper-like background,
restrained road/label styling, custom pin glyphs distinguishing
incident type, no default teardrop markers.
- Pins/heatmap toggle.
- Date filter: preset buttons (7/30/90/all) + custom from/to.
- Stats line above the map: count + percent change vs. prior equal-length
period.
## Design direction (data-journalism / editorial)
- Typography: a serif or distinctive display face for headlines/stat
callouts, a plain workhorse sans for form/UI text — deliberately not
the default Inter-everywhere look.
- Layout: the map reads as an illustration embedded in a page with real
margins and a masthead-style header, not a full-bleed app shell with a
sidebar of cards.
- Color: muted, ink/newsprint-leaning palette with one or two accent
colors reserved for the two incident-type pin markers.
- No generic dashboard chrome.
## Admin
Basic-auth-gated `/admin` route: list all reports with flag counts,
delete a report, view flag queue. No public account system needed since
reports are anonymous.
## Out of scope / explicitly noted as unverified
- Minneapolis open data import: dataset and fields not yet verified
against the actual open data portal. v2.
- Hosting target not yet decided.
## Verification
- `dotnet build` and `dotnet test` cover `LocationPrivacyService`
(snapping rounds correctly and consistently) and `GeofenceService`
(accepts in-metro coordinates, rejects out-of-metro).
- Manual pass: submit a report through the form (both incident types,
with and without optional fields, typed address and tap-to-place),
confirm it appears on the map only after refresh/filter, confirm the
precise coordinate never appears in any API response or page source,
confirm date-range filters and the stats percent-change update
correctly, confirm rate limiting kicks in on rapid repeated submits,
confirm the flag button and admin delete work.
Right now the whole thing runs against a MySQL container on my local machine. Hosting's the next decision and then it's about getting some eyes on the thing. Getting block clubs and a couple of reporters to actually use it, too. The map's only as good as the pins on it.
Till next time!