Add temporary admin contacts-census diagnostic (v0.1.0:105)
A throwaway, admin-only diagnostic for the deferred contacts <-> fundraising_contacts consolidation: GET /api/admin/contacts-census + a Settings -> Admin "Run census" button report the A/B/C populations (linked / contacts-only / pill-only) plus the communications/opportunities repointing surface. Counts only, no PII -- mirrors backend/scripts/contacts_census.sql so the numbers can be read off the box without a shell. All pieces are tagged TEMPORARY; delete the endpoint + route + button after the census is captured. No schema change. 45/45 tests, render-smoke green.
This commit is contained in:
@@ -115,5 +115,5 @@ _**Box live at v0.1.0:104 (deployed + verified 2026-06-20)** — clean StartOS m
|
|||||||
- **New (v0.1.0:104):** admin-only **Purge Deleted Data** (Settings → Admin) — guarded, type-to-confirm hard-delete of soft-deleted rows; see the soft-delete convention + `test_purge_soft_deleted.py`.
|
- **New (v0.1.0:104):** admin-only **Purge Deleted Data** (Settings → Admin) — guarded, type-to-confirm hard-delete of soft-deleted rows; see the soft-delete convention + `test_purge_soft_deleted.py`.
|
||||||
- **Verification:** **45/45** backend, render-smoke green, reviewer-agent APPROVE after fixing **1 blocker** (contact purge left a dangling `reminders.contact_id` — now NULLed + test-guarded). New UI behavior is **live-smoke / on-device only** (jsdom can't drive touch).
|
- **Verification:** **45/45** backend, render-smoke green, reviewer-agent APPROVE after fixing **1 blocker** (contact purge left a dangling `reminders.contact_id` — now NULLed + test-guarded). New UI behavior is **live-smoke / on-device only** (jsdom can't drive touch).
|
||||||
- **Bug A — Grant is handling:** `odell/marty/finance/ten31@` can't enroll for email capture ("could not resolve user_id") because the enroll flow requires a CRM `users` row; Grant is creating user accounts for those mailboxes.
|
- **Bug A — Grant is handling:** `odell/marty/finance/ten31@` can't enroll for email capture ("could not resolve user_id") because the enroll flow requires a CRM `users` row; Grant is creating user accounts for those mailboxes.
|
||||||
- **Next:** (A) confirm the two stuck mailboxes pulled current + Grant's 4 new mailbox users enroll; (B) **retire `contact_type`** — replace the Contacts Investors/Prospects tabs + TYPE badge with grid-derived `existing_investor`/`pipeline_stage`, then drop the column (see ROADMAP); (C) **contacts ↔ `fundraising_contacts` consolidation** — census-first (count A/linked, B/contacts-only, C/pill-only on the box; see ROADMAP); (D) carried: bell approve-on-phone → Matrix-thread-clears round-trip spot-check.
|
- **Next:** (A) confirm the two stuck mailboxes pulled current + Grant's 4 new mailbox users enroll; (B) **retire `contact_type`** — replace the Contacts Investors/Prospects tabs + TYPE badge with grid-derived `existing_investor`/`pipeline_stage`, then drop the column (see ROADMAP); (C) **contacts ↔ `fundraising_contacts` consolidation** — census-first (count A/linked, B/contacts-only, C/pill-only on the box; see ROADMAP). **A TEMPORARY admin census ships in v0.1.0:105 to read A/B/C off the box: `GET /api/admin/contacts-census` + a Settings → Admin "Run census" button (mirrors `backend/scripts/contacts_census.sql`). DELETE the endpoint + handler + route + button once the numbers are captured** (all tagged `TEMPORARY` in code). (D) carried: bell approve-on-phone → Matrix-thread-clears round-trip spot-check.
|
||||||
- **Open / risks:** the Contacts pagination, the purge, and the email-sync auto-recovery are **live-smoke / not yet device-confirmed**. Carried: **Claude/Architect path unverified live on the box**; vision OCR small-in-frame misread (`mara.com→marac.com`); doc drift — `crm-overview.md` narrative + `EVALUATION.md` still describe `lp_profiles` (the active API/schema claims were fixed; the deeper Phase-0 narrative is deferred to a doc pass).
|
- **Open / risks:** the Contacts pagination, the purge, and the email-sync auto-recovery are **live-smoke / not yet device-confirmed**. Carried: **Claude/Architect path unverified live on the box**; vision OCR small-in-frame misread (`mara.com→marac.com`); doc drift — `crm-overview.md` narrative + `EVALUATION.md` still describe `lp_profiles` (the active API/schema claims were fixed; the deeper Phase-0 narrative is deferred to a doc pass).
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@
|
|||||||
|
|
||||||
- **Retire `contacts.contact_type`** (the Contacts Investors/Prospects tabs + TYPE badge). It's a legacy binary that's set mechanically — `'investor'` just means "exists in the grid" (stamped unconditionally by `_upsert_contact_from_fundraising`), `'prospect'` means "imported/added, not in the grid" — and is superseded by the grid-derived signals `contact_grid_signals()` already injects (`existing_investor`/`committed`, `pipeline_stage`). Plan: replace the tabs + TYPE badge with those signals, repoint the dashboard `total_lps`/`total_prospects` counts, then drop the column. Live UI change → its own small design pass. (Grant: "I want to delete it, next session.")
|
- **Retire `contacts.contact_type`** (the Contacts Investors/Prospects tabs + TYPE badge). It's a legacy binary that's set mechanically — `'investor'` just means "exists in the grid" (stamped unconditionally by `_upsert_contact_from_fundraising`), `'prospect'` means "imported/added, not in the grid" — and is superseded by the grid-derived signals `contact_grid_signals()` already injects (`existing_investor`/`committed`, `pipeline_stage`). Plan: replace the tabs + TYPE badge with those signals, repoint the dashboard `total_lps`/`total_prospects` counts, then drop the column. Live UI change → its own small design pass. (Grant: "I want to delete it, next session.")
|
||||||
|
|
||||||
- **Consolidate `contacts` ↔ `fundraising_contacts` into one linked model.** Goal (Grant): everyone in `contacts` maps to a `fundraising_investors` row (an individual maps to their own row). Today `contacts` is the canonical person directory (FK target for `communications`/`opportunities`); `fundraising_contacts.contact_id` (migration `0004`) points INTO it; the mobile Contacts page reads `contacts`. Three populations: **A** linked (grid pill ↔ contact), **B** `contacts`-only (imported prospects / manual adds — need a grid row), **C** pill-only (`fundraising_contacts.contact_id IS NULL` — need a contact row). **Census-first:** before designing any migration, count A/B/C on the box — Grant runs the SQL himself (he is **not** providing a DB copy), so hand him a counts-only script. The census decides whether this is a ~20-row cleanup or a ~300-row structural migration with `communications`/`opportunities` repointing. Then Grant reconciles B (add grid rows/pills) and C (add contact rows) and ensures all are linked.
|
- **Consolidate `contacts` ↔ `fundraising_contacts` into one linked model.** Goal (Grant): everyone in `contacts` maps to a `fundraising_investors` row (an individual maps to their own row). Today `contacts` is the canonical person directory (FK target for `communications`/`opportunities`); `fundraising_contacts.contact_id` (migration `0004`) points INTO it; the mobile Contacts page reads `contacts`. Three populations: **A** linked (grid pill ↔ contact), **B** `contacts`-only (imported prospects / manual adds — need a grid row), **C** pill-only (`fundraising_contacts.contact_id IS NULL` — need a contact row). **Census-first:** before designing any migration, count A/B/C on the box — Grant runs the SQL himself (he is **not** providing a DB copy), so hand him a counts-only script. The census decides whether this is a ~20-row cleanup or a ~300-row structural migration with `communications`/`opportunities` repointing. Then Grant reconciles B (add grid rows/pills) and C (add contact rows) and ensures all are linked. **(v0.1.0:105) A TEMPORARY admin census ships to read A/B/C off the box without shell access: `GET /api/admin/contacts-census` (`handle_contacts_census`) + a Settings → Admin "Run census" button, mirroring `backend/scripts/contacts_census.sql` (counts only). DELETE the endpoint + route + button after the numbers are captured — all tagged `TEMPORARY` in code.**
|
||||||
|
|
||||||
### Follow-ups/reminders + NL search + bot grid-mutations (agreed plan, 2026-06-18)
|
### Follow-ups/reminders + NL search + bot grid-mutations (agreed plan, 2026-06-18)
|
||||||
*Agreed with Grant 2026-06-18. Three workstreams, sequenced **W1 → W2 → W3**. **Overarching constraint (Grant):** the dominant risk is **leaking LP data (names, $, notes, contacts) to third-party LLMs — NOT write-safety.** A wrong number is recoverable; investor substance reaching Claude is not. Consequences: W2 keeps LP rows off Claude (only the question text + schema vocabulary leave the box; entity names resolved locally); W3 keeps bot mutation-parsing on local Qwen. Because this DB *logs* commitments/pipeline but doesn't move money, a bot mutation is low-stakes → **any team member may approve one in Matrix**; the guardrail is "the bot can't silently mass-change numbers," enforced by the per-mutation human approval gate, not a tight money gate.*
|
*Agreed with Grant 2026-06-18. Three workstreams, sequenced **W1 → W2 → W3**. **Overarching constraint (Grant):** the dominant risk is **leaking LP data (names, $, notes, contacts) to third-party LLMs — NOT write-safety.** A wrong number is recoverable; investor substance reaching Claude is not. Consequences: W2 keeps LP rows off Claude (only the question text + schema vocabulary leave the box; entity names resolved locally); W3 keeps bot mutation-parsing on local Qwen. Because this DB *logs* commitments/pipeline but doesn't move money, a bot mutation is low-stakes → **any team member may approve one in Matrix**; the guardrail is "the bot can't silently mass-change numbers," enforced by the per-mutation human approval gate, not a tight money gate.*
|
||||||
|
|||||||
@@ -2359,6 +2359,8 @@ class CRMHandler(BaseHTTPRequestHandler):
|
|||||||
return self.handle_get_digest_policy(user)
|
return self.handle_get_digest_policy(user)
|
||||||
if path == '/api/admin/soft-deleted':
|
if path == '/api/admin/soft-deleted':
|
||||||
return self.handle_list_soft_deleted(user)
|
return self.handle_list_soft_deleted(user)
|
||||||
|
if path == '/api/admin/contacts-census': # TEMPORARY — remove after the consolidation census
|
||||||
|
return self.handle_contacts_census(user)
|
||||||
if path == '/api/fundraising/relational-summary':
|
if path == '/api/fundraising/relational-summary':
|
||||||
return self.handle_get_fundraising_relational_summary(user)
|
return self.handle_get_fundraising_relational_summary(user)
|
||||||
if path == '/api/fundraising/automations':
|
if path == '/api/fundraising/automations':
|
||||||
@@ -4868,6 +4870,39 @@ class CRMHandler(BaseHTTPRequestHandler):
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
# ─── TEMPORARY (v0.1.0:105) — DELETE AFTER the contacts<->fundraising_contacts census ──
|
||||||
|
# A throwaway admin diagnostic for the deferred consolidation (ROADMAP backlog): mirrors
|
||||||
|
# backend/scripts/contacts_census.sql so the A/B/C populations can be read from the box
|
||||||
|
# without shell access. Counts only — no names/PII. Remove this handler + its route + the
|
||||||
|
# Settings "Contacts census" button once the numbers are captured.
|
||||||
|
def handle_contacts_census(self, user):
|
||||||
|
if not require_admin(user):
|
||||||
|
return self.send_error_json("Admin required", 403)
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
def n(sql):
|
||||||
|
return conn.execute(sql).fetchone()[0]
|
||||||
|
no_pill = ("NOT EXISTS (SELECT 1 FROM fundraising_contacts fc WHERE fc.contact_id = c.id)")
|
||||||
|
return self.send_json({"data": {
|
||||||
|
"total_live_contacts": n("SELECT COUNT(*) FROM contacts WHERE deleted_at IS NULL"),
|
||||||
|
"A_linked": n("SELECT COUNT(*) FROM contacts c WHERE c.deleted_at IS NULL "
|
||||||
|
"AND EXISTS (SELECT 1 FROM fundraising_contacts fc WHERE fc.contact_id = c.id)"),
|
||||||
|
"B_contacts_only": n(f"SELECT COUNT(*) FROM contacts c WHERE c.deleted_at IS NULL AND {no_pill}"),
|
||||||
|
"B_investor": n(f"SELECT COUNT(*) FROM contacts c WHERE c.deleted_at IS NULL AND c.contact_type='investor' AND {no_pill}"),
|
||||||
|
"B_prospect": n(f"SELECT COUNT(*) FROM contacts c WHERE c.deleted_at IS NULL AND c.contact_type='prospect' AND {no_pill}"),
|
||||||
|
"B_with_live_communication": n(f"SELECT COUNT(*) FROM contacts c WHERE c.deleted_at IS NULL AND {no_pill} "
|
||||||
|
"AND EXISTS (SELECT 1 FROM communications cm WHERE cm.contact_id=c.id AND cm.deleted_at IS NULL)"),
|
||||||
|
"B_with_live_opportunity": n(f"SELECT COUNT(*) FROM contacts c WHERE c.deleted_at IS NULL AND {no_pill} "
|
||||||
|
"AND EXISTS (SELECT 1 FROM opportunities o WHERE o.contact_id=c.id AND o.deleted_at IS NULL)"),
|
||||||
|
"C_pill_only": n("SELECT COUNT(*) FROM fundraising_contacts WHERE contact_id IS NULL"),
|
||||||
|
"dangling_pills": n("SELECT COUNT(*) FROM fundraising_contacts fc WHERE fc.contact_id IS NOT NULL "
|
||||||
|
"AND NOT EXISTS (SELECT 1 FROM contacts c WHERE c.id=fc.contact_id AND c.deleted_at IS NULL)"),
|
||||||
|
"total_grid_pills": n("SELECT COUNT(*) FROM fundraising_contacts"),
|
||||||
|
"total_grid_rows": n("SELECT COUNT(*) FROM fundraising_investors"),
|
||||||
|
}})
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def handle_decide_activity_proposal(self, user, proposal_id, decision, body):
|
def handle_decide_activity_proposal(self, user, proposal_id, decision, body):
|
||||||
if not require_admin(user):
|
if not require_admin(user):
|
||||||
return self.send_error_json("Admin required", 403)
|
return self.send_error_json("Admin required", 403)
|
||||||
|
|||||||
@@ -10699,6 +10699,31 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TEMPORARY (v0.1.0:105) — DELETE AFTER the contacts<->fundraising_contacts census.
|
||||||
|
// Admin button that runs GET /api/admin/contacts-census and shows the counts (no PII), so the
|
||||||
|
// A/B/C populations can be read off the box without shell access. Remove with the endpoint.
|
||||||
|
const ContactsCensus = ({ token }) => {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const run = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try { const r = await api('/api/admin/contacts-census', {}, token); setData(r.data || {}); }
|
||||||
|
catch (err) { setData({ error: getErrorMessage(err, 'failed') }); }
|
||||||
|
finally { setLoading(false); }
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: '20px', borderBottom: '1px solid var(--border)', paddingBottom: '16px' }}>
|
||||||
|
<div style={{ fontWeight: 600, marginBottom: '8px' }}>Contacts census <span style={{ fontSize: '11px', color: 'var(--text-muted)', fontWeight: 400 }}>(temporary — sizes the contacts ↔ grid consolidation)</span></div>
|
||||||
|
<button type="button" className="button-secondary" onClick={run} disabled={loading} style={{ marginBottom: '10px' }}>
|
||||||
|
{loading ? <Spinner /> : 'Run census'}
|
||||||
|
</button>
|
||||||
|
{data && (
|
||||||
|
<pre style={{ fontSize: '12px', color: 'var(--text-secondary)', background: 'var(--bg-input)', border: '1px solid var(--border)', borderRadius: '8px', padding: '10px', overflowX: 'auto', margin: 0 }}>{JSON.stringify(data, null, 2)}</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsPage = ({ token, onShowToast, user, onOpenAirtableImport }) => {
|
const SettingsPage = ({ token, onShowToast, user, onOpenAirtableImport }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [inviteForm, setInviteForm] = useState({
|
const [inviteForm, setInviteForm] = useState({
|
||||||
@@ -11698,6 +11723,8 @@
|
|||||||
|
|
||||||
<PurgeDeletedData token={token} onShowToast={onShowToast} />
|
<PurgeDeletedData token={token} onShowToast={onShowToast} />
|
||||||
|
|
||||||
|
<ContactsCensus token={token} />{/* TEMPORARY — remove after the consolidation census */}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 600, marginBottom: '10px' }}>Fundraising State Ops</div>
|
<div style={{ fontWeight: 600, marginBottom: '10px' }}>Fundraising State Ops</div>
|
||||||
<div style={{ marginBottom: '12px', padding: '10px', border: '1px solid var(--border)', borderRadius: '8px' }}>
|
<div style={{ marginBottom: '12px', padding: '10px', border: '1px solid var(--border)', borderRadius: '8px' }}>
|
||||||
|
|||||||
@@ -69,8 +69,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
|
|||||||
// * 0.1.0:101 (Mobile UX batch 1 [Grant device feedback]: [1] inline ✕ clear button on the Grid/Contacts search + reminder/quick-log investor pickers [ClearableInput]; [2] Grid investor-detail contact pills are tappable — name deep-links to the Contacts detail [new Grid→Contacts one-shot action], email opens mailto; [4a] mobile Pipeline is a full-height flex column so the whole area above the now bottom-pinned dots is the swipe target, each stage page scrolling its cards; [4b] expected-amount entry — optional amount when adding to the pipeline from the Grid detail [feeds pipeline/link], editable amount on the Pipeline card detail [PUT /api/opportunities/{id}]; [5] bottom sheets lift above the on-screen keyboard [visualViewport] so the reminder investor-picker results stay visible. Grid contact-name search [#3] already worked. CSS+React only; no schema change; no migration; no new dependency)
|
// * 0.1.0:101 (Mobile UX batch 1 [Grant device feedback]: [1] inline ✕ clear button on the Grid/Contacts search + reminder/quick-log investor pickers [ClearableInput]; [2] Grid investor-detail contact pills are tappable — name deep-links to the Contacts detail [new Grid→Contacts one-shot action], email opens mailto; [4a] mobile Pipeline is a full-height flex column so the whole area above the now bottom-pinned dots is the swipe target, each stage page scrolling its cards; [4b] expected-amount entry — optional amount when adding to the pipeline from the Grid detail [feeds pipeline/link], editable amount on the Pipeline card detail [PUT /api/opportunities/{id}]; [5] bottom sheets lift above the on-screen keyboard [visualViewport] so the reminder investor-picker results stay visible. Grid contact-name search [#3] already worked. CSS+React only; no schema change; no migration; no new dependency)
|
||||||
// * 0.1.0:102 (Mobile email-approval bell [#6]: an admin-only bell in the mobile top bar [left of the camera] with an iPhone-style count badge surfaces the SAME pending email-capture proposals the web "Email Capture" panel + the Matrix review room decide. Tap → card list of proposals → tap one → review screen [investor name + subject + summary + editable proposed note] → Approve & log to grid / Reject. Reuses the existing GET /api/activity/proposals + POST .../{id}/approve|dismiss [require_admin]; bidirectional sync is automatic — an app decision flips the proposal status and the bot's poll redacts the Matrix thread, while a Matrix/web decision drops the proposal from the pending list the bell polls [45s], clearing the badge. No LLM round-trip [edit-then-approve like the web panel]; mobile-gated so the hidden desktop top bar doesn't poll. Frontend-only; no schema change; no migration; no new dependency)
|
// * 0.1.0:102 (Mobile email-approval bell [#6]: an admin-only bell in the mobile top bar [left of the camera] with an iPhone-style count badge surfaces the SAME pending email-capture proposals the web "Email Capture" panel + the Matrix review room decide. Tap → card list of proposals → tap one → review screen [investor name + subject + summary + editable proposed note] → Approve & log to grid / Reject. Reuses the existing GET /api/activity/proposals + POST .../{id}/approve|dismiss [require_admin]; bidirectional sync is automatic — an app decision flips the proposal status and the bot's poll redacts the Matrix thread, while a Matrix/web decision drops the proposal from the pending list the bell polls [45s], clearing the badge. No LLM round-trip [edit-then-approve like the web panel]; mobile-gated so the hidden desktop top bar doesn't poll. Frontend-only; no schema change; no migration; no new dependency)
|
||||||
// * 0.1.0:103 (Reminders require a due date [Grant feedback]: every reminder-create flow now pre-fills the due date to +1 week [editable] and blocks an empty save — a date-less reminder has no urgency [it falls to the "Later"/"No date" bucket, out of the overdue/today/this-week rollups + daily digest]. Applies to ALL create surfaces via a shared `reminderDefaultDue()` helper — mobile: the add-investor sheet [date auto-fills when you start the optional reminder], the standalone Reminders "New reminder" sheet, the Grid-detail "Set a reminder" card; desktop: the Reminders page "+ New reminder" + the grid reminder modal. Edit paths also pre-fill the default for legacy date-less reminders. Frontend-only; no schema/migration/dependency change)
|
// * 0.1.0:103 (Reminders require a due date [Grant feedback]: every reminder-create flow now pre-fills the due date to +1 week [editable] and blocks an empty save — a date-less reminder has no urgency [it falls to the "Later"/"No date" bucket, out of the overdue/today/this-week rollups + daily digest]. Applies to ALL create surfaces via a shared `reminderDefaultDue()` helper — mobile: the add-investor sheet [date auto-fills when you start the optional reminder], the standalone Reminders "New reminder" sheet, the Grid-detail "Set a reminder" card; desktop: the Reminders page "+ New reminder" + the grid reminder modal. Edit paths also pre-fill the default for legacy date-less reminders. Frontend-only; no schema/migration/dependency change)
|
||||||
// * Current: 0.1.0:104 (Remove the Instructions + Feedback [feature_requests] pages + backend, and retire the empty lp_profiles table + investor_type — a one-off sanctioned exception to never-hard-delete; in-app migration 0008 drops lp_profiles + feature_requests, and 0001's lp_profiles ALTER was removed so a fresh DB doesn't break the migration chain. Fixes: email sync no longer terminally parks a mailbox on a transient timeout [auto-retry + hourly backoff → stuck mailboxes self-heal]; mobile Contacts pages through ALL contacts [a single 500-row fetch truncated at 720, hiding people from the list + search]; a clock icon on the mobile email Review-log sets a reminder inline; email-approval cards show date/time. New: admin-only purge of soft-deleted rows [type-to-confirm; refuses any row still linked to live data])
|
// * 0.1.0:104 (Remove the Instructions + Feedback [feature_requests] pages + backend, and retire the empty lp_profiles table + investor_type — a one-off sanctioned exception to never-hard-delete; in-app migration 0008 drops lp_profiles + feature_requests, and 0001's lp_profiles ALTER was removed so a fresh DB doesn't break the migration chain. Fixes: email sync no longer terminally parks a mailbox on a transient timeout [auto-retry + hourly backoff → stuck mailboxes self-heal]; mobile Contacts pages through ALL contacts [a single 500-row fetch truncated at 720, hiding people from the list + search]; a clock icon on the mobile email Review-log sets a reminder inline; email-approval cards show date/time. New: admin-only purge of soft-deleted rows [type-to-confirm; refuses any row still linked to live data])
|
||||||
export const PACKAGE_VERSION = '0.1.0:104'
|
// * Current: 0.1.0:105 (TEMPORARY diagnostic — admin contacts census [GET /api/admin/contacts-census + a Settings → Admin "Run census" button] reporting the A/B/C populations [counts only, no PII] for the deferred contacts<->fundraising_contacts consolidation; mirrors backend/scripts/contacts_census.sql. DELETE the endpoint + route + button after the numbers are captured — all tagged TEMPORARY in code. No schema change)
|
||||||
|
export const PACKAGE_VERSION = '0.1.0:105'
|
||||||
|
|
||||||
export const DATA_MOUNT_PATH = '/data'
|
export const DATA_MOUNT_PATH = '/data'
|
||||||
export const WEB_PORT = 8080
|
export const WEB_PORT = 8080
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ import { v_0_1_0_101 } from './v0.1.0.101'
|
|||||||
import { v_0_1_0_102 } from './v0.1.0.102'
|
import { v_0_1_0_102 } from './v0.1.0.102'
|
||||||
import { v_0_1_0_103 } from './v0.1.0.103'
|
import { v_0_1_0_103 } from './v0.1.0.103'
|
||||||
import { v_0_1_0_104 } from './v0.1.0.104'
|
import { v_0_1_0_104 } from './v0.1.0.104'
|
||||||
|
import { v_0_1_0_105 } from './v0.1.0.105'
|
||||||
|
|
||||||
export const versionGraph = VersionGraph.of({
|
export const versionGraph = VersionGraph.of({
|
||||||
current: v_0_1_0_104,
|
current: v_0_1_0_105,
|
||||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64, v_0_1_0_65, v_0_1_0_66, v_0_1_0_67, v_0_1_0_68, v_0_1_0_69, v_0_1_0_70, v_0_1_0_71, v_0_1_0_72, v_0_1_0_73, v_0_1_0_74, v_0_1_0_75, v_0_1_0_76, v_0_1_0_77, v_0_1_0_78, v_0_1_0_79, v_0_1_0_80, v_0_1_0_81, v_0_1_0_82, v_0_1_0_83, v_0_1_0_84, v_0_1_0_85, v_0_1_0_86, v_0_1_0_87, v_0_1_0_88, v_0_1_0_89, v_0_1_0_90, v_0_1_0_91, v_0_1_0_92, v_0_1_0_93, v_0_1_0_94, v_0_1_0_95, v_0_1_0_96, v_0_1_0_97, v_0_1_0_98, v_0_1_0_99, v_0_1_0_100, v_0_1_0_101, v_0_1_0_102, v_0_1_0_103],
|
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64, v_0_1_0_65, v_0_1_0_66, v_0_1_0_67, v_0_1_0_68, v_0_1_0_69, v_0_1_0_70, v_0_1_0_71, v_0_1_0_72, v_0_1_0_73, v_0_1_0_74, v_0_1_0_75, v_0_1_0_76, v_0_1_0_77, v_0_1_0_78, v_0_1_0_79, v_0_1_0_80, v_0_1_0_81, v_0_1_0_82, v_0_1_0_83, v_0_1_0_84, v_0_1_0_85, v_0_1_0_86, v_0_1_0_87, v_0_1_0_88, v_0_1_0_89, v_0_1_0_90, v_0_1_0_91, v_0_1_0_92, v_0_1_0_93, v_0_1_0_94, v_0_1_0_95, v_0_1_0_96, v_0_1_0_97, v_0_1_0_98, v_0_1_0_99, v_0_1_0_100, v_0_1_0_101, v_0_1_0_102, v_0_1_0_103, v_0_1_0_104],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
// v0.1.0:105 — TEMPORARY diagnostic. An admin-only contacts census (GET /api/admin/contacts-census +
|
||||||
|
// a Settings → Admin "Run census" button) reports the A/B/C populations for the deferred
|
||||||
|
// contacts <-> fundraising_contacts consolidation — counts only, no PII, mirrors
|
||||||
|
// backend/scripts/contacts_census.sql. To be REMOVED in a later release once the numbers are
|
||||||
|
// captured (everything is tagged TEMPORARY in code). No schema change.
|
||||||
|
export const v_0_1_0_105 = VersionInfo.of({
|
||||||
|
version: '0.1.0:105',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: 'Temporary admin contacts-census diagnostic (counts only) for the upcoming contacts/grid consolidation; no schema change.',
|
||||||
|
},
|
||||||
|
migrations: { up: async () => {}, down: async () => {} },
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user