06/03 Optimize app

This commit is contained in:
2026-06-03 16:06:30 -04:00
parent 4af31a611e
commit ec1c0fc7c1
14 changed files with 447 additions and 12 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
Migration: add security columns.
users — totp_secret, totp_enabled
schwab_connections — refresh_token_expires_at
Run once: python scripts/add_security_columns.py
Safe to re-run — skips columns that already exist.
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app
from app.extensions import db
app = create_app()
COLUMNS = [
("users", "totp_secret",
"VARCHAR(64) NULL DEFAULT NULL"),
("users", "totp_enabled",
"TINYINT(1) NOT NULL DEFAULT 0"),
("schwab_connections", "refresh_token_expires_at",
"DATETIME NULL DEFAULT NULL"),
]
with app.app_context():
with db.engine.connect() as conn:
for table, column, definition in COLUMNS:
exists = conn.execute(db.text(
"SELECT COUNT(*) FROM information_schema.columns "
"WHERE table_schema = DATABASE() "
"AND table_name = :t AND column_name = :c"
), {"t": table, "c": column}).scalar()
if exists:
print(f" {table}.{column} already exists — skipped.")
else:
conn.execute(db.text(
f"ALTER TABLE {table} ADD COLUMN {column} {definition}"
))
conn.commit()
print(f" Added {table}.{column}.")
print("Done.")
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Schwab daily auto-sync — balance, positions, and transactions.
Add to crontab (crontab -e):
# Sync Schwab every day at 7 AM
0 7 * * * /home/pfm/venv/bin/python /home/pfm/web/scripts/sync_schwab.py >> /home/pfm/web/logs/sync_schwab.log 2>&1
Or run manually:
python scripts/sync_schwab.py
"""
import sys
import os
import logging
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app
from app.extensions import db
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s|%(levelname)s|%(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
log = logging.getLogger(__name__)
app = create_app()
with app.app_context():
from app.models.schwab_connection import SchwabConnection, SchwabAccount
from app.services.schwab_service import (
sync_account_snapshot, sync_preview, import_transactions,
)
connection = SchwabConnection.query.filter_by(is_active=True).first()
if not connection:
log.warning('[sync_schwab] No active Schwab connection — skipping.')
sys.exit(0)
# Warn if refresh token is close to expiry
if connection.refresh_token_expires_at:
days_left = (connection.refresh_token_expires_at - datetime.utcnow()).days
if days_left <= 2:
log.warning('[sync_schwab] Schwab refresh token expires in %d day(s)! '
'Log in and reconnect at /schwab/connect.', days_left)
accounts = SchwabAccount.query.filter(
SchwabAccount.pfm_account_id != None,
SchwabAccount.is_active == True,
).all()
if not accounts:
log.warning('[sync_schwab] No mapped Schwab accounts found.')
sys.exit(0)
log.info('[sync_schwab] Syncing %d account(s)…', len(accounts))
for sa in accounts:
sa.connection = connection # always use active connection
# ── 1. Balance + positions ────────────────────────────────────────────
try:
bal_updated, pos_synced = sync_account_snapshot(sa)
log.info('[sync_schwab] %s — balance updated=%s positions=%d',
sa.account_name, bal_updated, pos_synced)
except Exception as e:
log.error('[sync_schwab] snapshot failed for %s: %s', sa.account_name, e,
exc_info=True)
# ── 2. New transactions ───────────────────────────────────────────────
try:
preview = sync_preview(sa)
if preview:
imported, skipped = import_transactions(preview, sa)
log.info('[sync_schwab] %s — transactions: imported=%d skipped=%d',
sa.account_name, imported, skipped)
else:
log.info('[sync_schwab] %s — no new transactions.', sa.account_name)
except Exception as e:
log.error('[sync_schwab] transaction sync failed for %s: %s',
sa.account_name, e, exc_info=True)
log.info('[sync_schwab] Done.')