Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5845fc5694 | ||
|
|
ddf13402d0 | ||
|
|
20c5799a8d | ||
|
|
e90a73ff09 | ||
|
|
e0fdc4fbbc | ||
|
|
65705bf995 |
+15
-13
@@ -307,28 +307,30 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy firestore rules and indexes
|
- name: Deploy firestore rules and indexes
|
||||||
env:
|
env:
|
||||||
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
|
FIREBASE_SA_KEY: ${{ secrets.FIREBASE_SA_KEY }}
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
# server-26#51: this used to run over SSH on the deploy VM, gated
|
# server-26#51: this used to run over SSH on the deploy VM, gated
|
||||||
# on the VM having firebase-tools installed. It never did, so it
|
# on the VM having firebase-tools installed. It never did, so it
|
||||||
# silently warned-and-skipped on every single deploy for weeks.
|
# silently warned-and-skipped on every single deploy for weeks.
|
||||||
# Running it here instead means the only prerequisite is a secret
|
# Auth is a dedicated service account (drb-ci-firestore-deploy,
|
||||||
# -- FIREBASE_TOKEN, from `firebase login:ci` -- rather than
|
# roles: Firebase Rules Admin, Cloud Datastore Index Admin,
|
||||||
# something installed by hand on a machine this pipeline doesn't
|
# Service Usage Consumer), its JSON key stored as the
|
||||||
# otherwise touch. A missing token now fails this job LOUDLY
|
# FIREBASE_SA_KEY secret. Not `firebase login:ci`: those tokens are
|
||||||
# (picked up by notify-failure) instead of a buried warning line
|
# deprecated and carry the full permissions of whoever minted them.
|
||||||
# nobody reads in the app deploy's logs.
|
# A missing key fails this job LOUDLY (picked up by notify-failure).
|
||||||
if [ -z "$FIREBASE_TOKEN" ]; then
|
if [ -z "$FIREBASE_SA_KEY" ]; then
|
||||||
echo "FIREBASE_TOKEN secret is not set -- cannot deploy Firestore rules/indexes." >&2
|
echo "FIREBASE_SA_KEY secret is not set -- cannot deploy Firestore rules/indexes." >&2
|
||||||
echo "Generate one with 'firebase login:ci' and add it as a Gitea Actions secret." >&2
|
echo "Add the drb-ci-firestore-deploy service account's JSON key as a Gitea Actions secret." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
export GOOGLE_APPLICATION_CREDENTIALS="$RUNNER_TEMP/firebase-sa.json"
|
||||||
|
trap 'rm -f "$GOOGLE_APPLICATION_CREDENTIALS"' EXIT
|
||||||
|
( umask 077 && printf '%s' "$FIREBASE_SA_KEY" > "$GOOGLE_APPLICATION_CREDENTIALS" )
|
||||||
npm install -g firebase-tools
|
npm install -g firebase-tools
|
||||||
cd infra/firestore
|
cd infra/firestore
|
||||||
firebase deploy --only firestore:rules,firestore:indexes \
|
firebase deploy --only firestore:rules,firestore:indexes \
|
||||||
--project ${{ secrets.FIREBASE_PROJECT_ID }} \
|
--project ${{ secrets.FIREBASE_PROJECT_ID }} --non-interactive
|
||||||
--token "$FIREBASE_TOKEN" --non-interactive
|
|
||||||
|
|
||||||
notify-failure:
|
notify-failure:
|
||||||
name: Report a failed deploy
|
name: Report a failed deploy
|
||||||
@@ -371,7 +373,7 @@ jobs:
|
|||||||
# failed before any deploy was attempted" text even when the app
|
# failed before any deploy was attempted" text even when the app
|
||||||
# deployed fine and only the Firestore rules/indexes push failed.
|
# deployed fine and only the Firestore rules/indexes push failed.
|
||||||
if deploy_result != "failure" and rules_result == "failure":
|
if deploy_result != "failure" and rules_result == "failure":
|
||||||
detail = "App deploy succeeded; Firestore rules/indexes deploy FAILED (server-26#51). Rules may be stale — check FIREBASE_TOKEN and the job log."
|
detail = "App deploy succeeded; Firestore rules/indexes deploy FAILED (server-26#51). Rules may be stale — check the FIREBASE_SA_KEY secret and the job log."
|
||||||
|
|
||||||
# server-26#65: the old text here unconditionally claimed
|
# server-26#65: the old text here unconditionally claimed
|
||||||
# "production is still running the previous build" -- true only
|
# "production is still running the previous build" -- true only
|
||||||
|
|||||||
@@ -343,9 +343,21 @@ def clean_location(value) -> Optional[str]:
|
|||||||
s = str(value).strip()
|
s = str(value).strip()
|
||||||
if not s or not _LOCATION_WORD_RE.search(s):
|
if not s or not _LOCATION_WORD_RE.search(s):
|
||||||
return None
|
return None
|
||||||
|
if _RADIO_CODE_RE.match(s):
|
||||||
|
return None
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# Status/disposition codes the extractor sometimes returns as a location:
|
||||||
|
# "96 times 5" (a disposition code read aloud) titled a 09-22 replay stop
|
||||||
|
# "Traffic Stop at 96 times 5" (server-26#170); "10-8", "signal 99", "code 4"
|
||||||
|
# are the same shape.
|
||||||
|
_RADIO_CODE_RE = re.compile(
|
||||||
|
r"^\s*(?:\d{1,3}\s*(?:times|x)\s*\d{1,3}|10[\s-]?\d{1,3}|(?:signal|code|condition)\s+\d{1,3})\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def location_is_unit(location, units) -> bool:
|
def location_is_unit(location, units) -> bool:
|
||||||
"""
|
"""
|
||||||
True when a location label is really one of the incident's own unit
|
True when a location label is really one of the incident's own unit
|
||||||
|
|||||||
@@ -517,12 +517,33 @@ _NO_BACKSTOP_TG = re.compile(r"\b(mta|rail|railroad|train|transit|bridges? and t
|
|||||||
r"rescue|ambulance|dpw|public works)\b", re.IGNORECASE)
|
r"rescue|ambulance|dpw|public works)\b", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
# A plate read aloud — two or more phonetic letters then 3-7 digits:
|
||||||
|
# "Frank David Boy, 4514", "Lincoln, Charlie, Robert, 7-4-0-7". On a patrol
|
||||||
|
# channel that is a unit running a car it has stopped. Held-out replay of
|
||||||
|
# 09-21 (server-26#170): Ossining's Post 4 stops were read out only as plates
|
||||||
|
# and never became incidents.
|
||||||
|
_PHONETIC = (r"(?:adam|alpha|baker|boy|bravo|charlie|charles|david|delta|eddie|edward|echo|frank|"
|
||||||
|
r"george|golf|henry|hotel|ida|india|john|juliet|king|kilo|lincoln|lima|larry|mary|"
|
||||||
|
r"michael|mike|nora|nancy|november|ocean|oscar|peter|paul|papa|queen|robert|romeo|"
|
||||||
|
r"sam|sierra|tom|tango|union|uniform|victor|william|whiskey|x-ray|xray|young|yankee|zebra|zulu)")
|
||||||
|
_PLATE_READ = re.compile(rf"\b{_PHONETIC}(?:[,\s]+{_PHONETIC}){{1,3}}[,\s]+\d(?:[\s-]?\d){{2,6}}\b",
|
||||||
|
re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def _self_initiated_backstop(
|
def _self_initiated_backstop(
|
||||||
text: str, tags: list, incident_type: Optional[str], severity: str,
|
text: str, tags: list, incident_type: Optional[str], severity: str,
|
||||||
talkgroup_name: Optional[str] = None,
|
talkgroup_name: Optional[str] = None,
|
||||||
) -> tuple[list, Optional[str], str]:
|
) -> tuple[list, Optional[str], str]:
|
||||||
if talkgroup_name and _NO_BACKSTOP_TG.search(talkgroup_name):
|
if talkgroup_name and _NO_BACKSTOP_TG.search(talkgroup_name):
|
||||||
return tags, incident_type, severity
|
return tags, incident_type, severity
|
||||||
|
# A plate read only stands for a stop when extraction found no other
|
||||||
|
# event in the call: the plate on an MVA, a tow or a parked-car complaint
|
||||||
|
# belongs to that event, not to a new stop.
|
||||||
|
if not tags and _PLATE_READ.search(text or ""):
|
||||||
|
tags = ["traffic-stop"]
|
||||||
|
incident_type = incident_type or "police"
|
||||||
|
if severity == "routine":
|
||||||
|
severity = "minor"
|
||||||
for pattern, tag in _SELF_INITIATED:
|
for pattern, tag in _SELF_INITIATED:
|
||||||
m = pattern.search(text or "")
|
m = pattern.search(text or "")
|
||||||
if not m or _NEGATED.search(text[: m.start()]):
|
if not m or _NEGATED.search(text[: m.start()]):
|
||||||
|
|||||||
@@ -140,3 +140,24 @@ def test_short_stop_report_opens_a_scene():
|
|||||||
return await intelligence.extract_scenes("c1", "Adam 3 on a stop.", "Ch 1 (Patched with 155.310)")
|
return await intelligence.extract_scenes("c1", "Adam 3 on a stop.", "Ch 1 (Patched with 155.310)")
|
||||||
scenes = asyncio.run(run())
|
scenes = asyncio.run(run())
|
||||||
assert len(scenes) == 1 and scenes[0]["tags"] == ["traffic-stop"] and scenes[0]["incident_type"] == "police"
|
assert len(scenes) == 1 and scenes[0]["tags"] == ["traffic-stop"] and scenes[0]["incident_type"] == "police"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plate_read_on_a_patrol_channel_is_a_stop():
|
||||||
|
from app.internal.intelligence import _self_initiated_backstop as b
|
||||||
|
ch = "Ossining - Police Dispatch"
|
||||||
|
for t in ("Post 4. 52-62, 3-3. Hemlock Circle. Frank David Boy, 4514. 10-8.",
|
||||||
|
"4, Ossining. 52-22, Ramapo, New York. Lincoln, Charlie, Robert, 7-4-0-7 on a Chevy.",
|
||||||
|
"New York, Mary, Charlie, Nora, 5-8-6-7."):
|
||||||
|
assert b(t, [], None, "routine", ch) == (["traffic-stop"], "police", "minor"), t
|
||||||
|
# a plate on a call that is already about something else stays with it
|
||||||
|
assert b("MVA, plate Mary George Sam 2740", ["mva"], "accident", "moderate", ch) == (["mva"], "accident", "moderate")
|
||||||
|
# not on rail/bridge channels, and not without digits
|
||||||
|
assert b("Frank David Boy 4514", [], None, "routine", "MTA Bridges and Tunnels - Whitestone") == ([], None, "routine")
|
||||||
|
assert b("Charlie, David, go ahead.", [], None, "routine", ch) == ([], None, "routine")
|
||||||
|
|
||||||
|
|
||||||
|
def test_radio_codes_are_not_locations():
|
||||||
|
for junk in ("96 times 5", "96 x 1", "10-8", "Signal 99", "code 4"):
|
||||||
|
assert ic.clean_location(junk) is None, junk
|
||||||
|
for place in ("West Main Street", "Route 9", "96 Main Street", "Exit 17 southbound"):
|
||||||
|
assert ic.clean_location(place) == place, place
|
||||||
|
|||||||
@@ -8,13 +8,11 @@
|
|||||||
// hand-set in the Firebase console: unversioned, unreviewed, unknown. See
|
// hand-set in the Firebase console: unversioned, unreviewed, unknown. See
|
||||||
// SAAS_PLAN.md B1.
|
// SAAS_PLAN.md B1.
|
||||||
//
|
//
|
||||||
// DEPLOY IS A MANUAL, OUT-OF-BAND STEP — nothing in CI or this codebase
|
// DEPLOYED BY CI on every push to main (.gitea/workflows/deploy.yml, job
|
||||||
// pushes these rules to Firebase:
|
// deploy-firestore-rules, service-account auth via the FIREBASE_SA_KEY
|
||||||
// firebase deploy --only firestore:rules --project <project-id>
|
// secret — server-26#51). That job is separate from the app deploy, so a
|
||||||
// (from this directory, or point --config at infra/firestore/firebase.json
|
// green app deploy does NOT mean these rules are live: check that job too.
|
||||||
// from the repo root). Do this before or immediately after the code that
|
// Editing rules in the Firebase console is overwritten by the next push.
|
||||||
// starts stamping org_id ships — until these rules are live, the
|
|
||||||
// console-configured rules are still what's actually enforced.
|
|
||||||
//
|
//
|
||||||
// MODEL: c2-core (firebase-admin SDK, server-side) bypasses these rules
|
// MODEL: c2-core (firebase-admin SDK, server-side) bypasses these rules
|
||||||
// entirely and is the sole writer for every collection below — that was
|
// entirely and is the sole writer for every collection below — that was
|
||||||
|
|||||||
Reference in New Issue
Block a user