Remove Google Sheets mirror entirely (#13)
Test backoffice (smb-crm) / test (push) Has been cancelled

Postgres is now the sole source of truth: delete sheets.py and
import_from_sheets.py, strip mirror_entity/mirror_async/_mirror_worker and
POST /api/sync from app.py, drop the tab/mirror keys from db.py's TABLES.
Re-point n8n/renewal-reminder.json at the CRM's own HTTP API (GET
/api/clients, POST /api/activity_log) instead of the Sheets nodes, and drop
SHEET_ID/GOOGLE_SA_JSON from deploy env/compose and requests from
requirements.txt (PyJWT stays — still used by booking_api.py). Updates
docs/README/playbooks accordingly and closes the old #5 (atomic mirror) as
moot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 11:29:57 +02:00
parent 16500c4392
commit 319218ce21
18 changed files with 76 additions and 451 deletions
+5 -12
View File
@@ -1,8 +1,8 @@
"""Postgres access + the table/column contract shared across the app.
The TABLES map is the single definition of which entities exist, their columns,
and their primary key. Read/CRUD endpoints, the importer and the Sheets mirror
all derive from it so they can never drift apart.
and their primary key. Read/CRUD endpoints all derive from it so they can
never drift apart.
"""
import os
import re
@@ -13,10 +13,9 @@ from psycopg.rows import dict_row
DATABASE_URL = os.environ["DATABASE_URL"]
# entity -> (sheet tab, primary key, ordered columns)
# entity -> (primary key, ordered columns)
TABLES = {
"clients": {
"tab": "Clients",
"pk": "client_id",
"cols": ["client_id", "business_name", "owner_name", "email", "phone",
"niche", "tier", "status", "domain", "stack_notes", "vault_ref",
@@ -28,7 +27,6 @@ TABLES = {
"bools": [],
},
"leads": {
"tab": "Leads",
"pk": "lead_id",
"cols": ["lead_id", "received_at", "client_id", "source", "name",
"contact", "service_interest", "message", "status", "notified"],
@@ -38,7 +36,6 @@ TABLES = {
"bools": ["notified"],
},
"projects": {
"tab": "Projects",
"pk": "project_id",
"cols": ["project_id", "client_id", "deliverable", "tier", "checklist",
"go_live_date", "status"],
@@ -48,7 +45,6 @@ TABLES = {
"bools": [],
},
"bookings": {
"tab": "Bookings",
"pk": "booking_id",
"cols": ["booking_id", "created_at", "client_id", "customer_name",
"customer_contact", "service", "start_time", "end_time",
@@ -59,7 +55,6 @@ TABLES = {
"bools": [],
},
"invoices": {
"tab": "Invoices",
"pk": "invoice_id",
"cols": ["invoice_id", "client_id", "issued_date", "due_date",
"amount_eur", "period", "status", "paid_date"],
@@ -69,7 +64,6 @@ TABLES = {
"bools": [],
},
"activity_log": {
"tab": "Activity Log",
"pk": "id",
"cols": ["ts", "workflow", "client_id", "action", "detail", "result"],
"dates": [],
@@ -78,14 +72,13 @@ TABLES = {
"bools": [],
},
"credentials": {
# No "tab": never mirrored to Sheets (see "mirror" below) — secrets stay in Postgres only.
# Secrets stay in Postgres only.
"pk": "cred_id",
"cols": ["cred_id", "client_id", "label", "username", "secret", "notes", "created_at"],
"dates": [],
"timestamps": ["created_at"],
"numbers": [],
"bools": [],
"mirror": False,
},
}
@@ -94,7 +87,7 @@ def connect():
return psycopg.connect(DATABASE_URL, row_factory=dict_row)
# ---- coercion: Sheet strings / JSON values -> typed Python for Postgres ----
# ---- coercion: JSON request values -> typed Python for Postgres ----
def parse_date(v):
if v in (None, ""):