Minneapolis has a car break-in problem

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!

This blog doesn't have a comments section. Reply by email - I'd love to hear what you think.