Remove Instructions/Feedback + lp_profiles; sync retry, purge, mobile fixes (v0.1.0:104)
Removals (net -570 lines): - Delete the Instructions and Feedback (feature_requests) pages + backend. - Retire lp_profiles + investor_type across server, ingest, and seeds; migration 0008 drops both empty tables (a sanctioned one-off exception to never-hard-delete). 0001's lp_profiles ALTER is removed so a fresh DB doesn't break the migration chain (live DBs already applied it). Fixes: - Email sync: a transient timeout no longer terminally parks a mailbox; the scheduler retries 'retrying' each cycle and re-includes errored accounts on an hourly backoff, so stuck mailboxes self-heal. - Mobile Contacts: page through the full directory (server caps 500/page) -- one fetch silently truncated at 720, hiding people from the list and from search. - Mobile email review: clock icon to set a reminder inline; approval cards show date/time. New: - Admin-only purge of soft-deleted rows (Settings -> Admin; type-to-confirm, refuses any row still linked to live data). Tests: 45/45 (adds test_sync_ready + test_purge_soft_deleted). Reviewer pass applied (NULL reminders.contact_id on contact purge). Bumped to v0.1.0:104.
This commit is contained in:
@@ -12,7 +12,7 @@ Asserts the SAFE fix:
|
||||
3. a grid contact that can't be PROVABLY matched mints NOTHING (no duplicate
|
||||
person, no cross-firm name guess) — the count stays correct,
|
||||
4. targeted cleanup soft-deletes a stale grid-only "twin" (person with no
|
||||
contacts link) and a superseded 'lp'/'organization' row, with no enrichment,
|
||||
contacts link), with no enrichment,
|
||||
5. cleanup PRESERVES a grid-only person that carries enrichment (guardrail #3),
|
||||
6. a re-emitted id is UN-tombstoned (no permanent burial),
|
||||
7. re-running is idempotent.
|
||||
@@ -58,10 +58,9 @@ CREATE TABLE contacts (
|
||||
CREATE TABLE organizations (id TEXT PRIMARY KEY, name TEXT, email TEXT);
|
||||
CREATE TABLE fundraising_investors (id TEXT PRIMARY KEY, investor_name TEXT);
|
||||
CREATE TABLE fundraising_contacts (id TEXT PRIMARY KEY, full_name TEXT, email TEXT, investor_id TEXT, contact_id TEXT);
|
||||
CREATE TABLE lp_profiles (id TEXT PRIMARY KEY, contact_id TEXT, deleted_at TEXT);
|
||||
"""
|
||||
|
||||
SEEDED = ("per_TWIN", "per_ENR", "lp_OLD")
|
||||
SEEDED = ("per_TWIN", "per_ENR")
|
||||
|
||||
|
||||
def seed(db):
|
||||
@@ -94,16 +93,14 @@ def seed(db):
|
||||
"('per_ENR','person','Enriched Orphan','entity_resolution','warm')")
|
||||
c.execute("INSERT INTO entity_links (id, canonical_id, source_model, source_id, match_value, match_kind, confidence, created_at) "
|
||||
"VALUES ('l_enr','per_ENR','fundraising_contacts','gy','enr','name_org',0.8,'t')")
|
||||
# Superseded pre-:48 kind -> prune
|
||||
c.execute("INSERT INTO canonical_entities (id, entity_kind, display_name, source) VALUES "
|
||||
"('lp_OLD','lp','Old LP Row','entity_resolution')")
|
||||
c.commit()
|
||||
c.close()
|
||||
|
||||
|
||||
def resolved_persons(db):
|
||||
c = sqlite3.connect(db)
|
||||
q = "SELECT COUNT(*) FROM canonical_entities WHERE entity_kind='person' AND deleted_at IS NULL AND id NOT IN (?,?,?)"
|
||||
ph = ",".join("?" * len(SEEDED))
|
||||
q = f"SELECT COUNT(*) FROM canonical_entities WHERE entity_kind='person' AND deleted_at IS NULL AND id NOT IN ({ph})"
|
||||
n = c.execute(q, SEEDED).fetchone()[0]
|
||||
c.close()
|
||||
return n
|
||||
@@ -127,10 +124,11 @@ def grid_match_kinds(db):
|
||||
def minted_from_grid(db):
|
||||
"""Persons minted directly from a grid row (the bug). Should be 0 after the fix."""
|
||||
c = sqlite3.connect(db)
|
||||
n = c.execute("""SELECT COUNT(DISTINCT l.canonical_id) FROM entity_links l
|
||||
ph = ",".join("?" * len(SEEDED))
|
||||
n = c.execute(f"""SELECT COUNT(DISTINCT l.canonical_id) FROM entity_links l
|
||||
JOIN canonical_entities ce ON ce.id=l.canonical_id AND ce.deleted_at IS NULL
|
||||
WHERE l.source_model='fundraising_contacts' AND l.match_kind IN ('name_org','exact_email')
|
||||
AND l.canonical_id NOT IN (?,?,?)""", SEEDED).fetchone()[0]
|
||||
AND l.canonical_id NOT IN ({ph})""", SEEDED).fetchone()[0]
|
||||
c.close()
|
||||
return n
|
||||
|
||||
@@ -162,12 +160,11 @@ def main():
|
||||
check(mk.get("grid_assoc", 0) == 2, f"two grid contacts matched back via grid_assoc (got {mk.get('grid_assoc',0)})")
|
||||
check(mk.get("grid_link", 0) == 1, f"one grid contact linked via explicit contact_id (grid_link==1, got {mk.get('grid_link',0)})")
|
||||
|
||||
# Targeted cleanup: stale grid-only twin + superseded 'lp' row tombstoned...
|
||||
# Targeted cleanup: stale grid-only twin tombstoned...
|
||||
check(deleted_at(db, "per_TWIN") is not None, "stale grid-only twin 'per_TWIN' tombstoned")
|
||||
check(deleted_at(db, "lp_OLD") is not None, "superseded 'lp' row 'lp_OLD' tombstoned")
|
||||
# ...enriched grid-only person preserved.
|
||||
check(deleted_at(db, "per_ENR") is None, "enriched grid-only person 'per_ENR' PRESERVED (has segment)")
|
||||
check(counts1.get("pruned_stale", 0) == 2, f"exactly 2 stale rows pruned (got {counts1.get('pruned_stale')})")
|
||||
check(counts1.get("pruned_stale", 0) == 1, f"exactly 1 stale row pruned (got {counts1.get('pruned_stale')})")
|
||||
|
||||
# Un-tombstone: soft-delete a real contact-person, then re-run -> it comes back.
|
||||
alice = er._eid("per", "e|alice@x.com")
|
||||
|
||||
Reference in New Issue
Block a user