diff --git a/sfm/database.py b/sfm/database.py index c1f596c..c0e8654 100644 --- a/sfm/database.py +++ b/sfm/database.py @@ -631,6 +631,26 @@ class SeismoDb: ).fetchall() return [dict(r) for r in rows] + def propagate_review_to_twins(self, event_id: str, *, window_seconds: int = 300) -> list[str]: + """ + Copy this event's `false_trigger`/`reviewed_real` columns onto each + of its histogram/waveform twins (see `find_twins`), so flagging one + twin flags both. Returns the list of twin ids updated. + """ + row = self.get_event(event_id) + if not row: + return [] + ft = 1 if row.get("false_trigger") else 0 + real = 1 if row.get("reviewed_real") else 0 + twins = self.find_twins(event_id, window_seconds=window_seconds) + moved = [] + with self._connect() as conn: + for tw in twins: + conn.execute("UPDATE events SET false_trigger=?, reviewed_real=? WHERE id=?", + (ft, real, tw["id"])) + moved.append(tw["id"]) + return moved + def set_false_trigger(self, event_id: str, value: bool) -> bool: """Set or clear the false_trigger flag on an event. Returns True if found.""" with self._connect() as conn: diff --git a/sfm/server.py b/sfm/server.py index ea0c92a..6e2ea1a 100644 --- a/sfm/server.py +++ b/sfm/server.py @@ -2481,6 +2481,13 @@ def db_event_sidecar_patch(event_id: str, body: SidecarPatchBody) -> dict: if body.review is not None: _get_db().update_event_review(event_id, new_sidecar.get("review", {})) + # Propagate the review to the event's histogram/waveform twin(s) so + # flagging one flags both (column-level; twins share serial+PVS+near time). + try: + _get_db().propagate_review_to_twins(event_id) + except Exception as exc: + log.warning("twin review-propagation failed for %s: %s", event_id, exc) + return new_sidecar diff --git a/tests/test_twin_propagation.py b/tests/test_twin_propagation.py new file mode 100644 index 0000000..c3f7386 --- /dev/null +++ b/tests/test_twin_propagation.py @@ -0,0 +1,33 @@ +import sqlite3 +from sfm.database import SeismoDb +from minimateplus.models import Event, Timestamp + + +def _ins(db, key, serial, pvs, ts): + ev = Event(index=0) + ev._waveform_key = bytes.fromhex(key) + ev.timestamp = ts + # peak_vector_sum comes from peak_values; simplest: insert then UPDATE pvs directly + db.insert_events([ev], serial=serial) + row = [r for r in db.query_events(serial=serial) if r["waveform_key"] == key][0] + with sqlite3.connect(db.db_path) as c: + c.execute("UPDATE events SET peak_vector_sum=? WHERE id=?", (pvs, row["id"])) + return row["id"] + + +def test_propagate_copies_flags_to_twins(tmp_path): + db = SeismoDb(tmp_path / "s.db") + base = Timestamp(raw=b"", flag=0x10, year=2026, unknown_byte=0, month=2, day=25, hour=20, minute=19, second=5) + twin = Timestamp(raw=b"", flag=0x10, year=2026, unknown_byte=0, month=2, day=25, hour=20, minute=19, second=45) + other = Timestamp(raw=b"", flag=0x10, year=2026, unknown_byte=0, month=2, day=25, hour=20, minute=19, second=44) + + primary_id = _ins(db, "01110001", "BE1", 0.4763, base) + twin_id = _ins(db, "01110002", "BE1", 0.4763, twin) # twin: same serial+pvs, 40s apart + non_twin_id = _ins(db, "01110003", "BE1", 0.9999, other) # near time but different pvs + + db.update_event_review(primary_id, {"false_trigger": True}) + moved = db.propagate_review_to_twins(primary_id) + + assert twin_id in moved + assert db.get_event(twin_id)["false_trigger"] == 1 + assert db.get_event(non_twin_id)["false_trigger"] == 0