sync: encrypted cloud backup for portfolios + settings UX rework
Adds opt-in client-side-encrypted portfolio sync (paid). Browser
PBKDF2(PIN) → AES-GCM, server HKDF(pepper, user_id) outer wrap;
server stores opaque bytes only. Sliding-window rate limit on GET.
- new portfolio_sync table (migration 0015)
- POST/GET/DELETE /api/portfolio/sync + /status
- app/services/portfolio_sync.py crypto + rate limit
- app/routers/sync.py paid-gated
- app/static/js/portfolio-sync.js WebCrypto wrapper
- settings page: enable/disable + PIN modal
- PORTFOLIO_SYNC_PEPPER setting (warn on startup if missing)
Settings + import rework:
- /upload merged into /settings#import (legacy route 302s)
- drop CSV → auto-parse → preview → Import only / Import & sync
- nav slimmed to Dashboard / News / Log
- Settings + Logout moved to a user dropdown
- brand logo links to /
Collateral fixes:
- settings 500: re-fetch User in current session before mutating
referral_code (assign_code_if_missing was refreshing a User
loaded in the auth dep's now-closed session)
- csv_import: distinct error for unfunded T212 pies (all qty=0)
- db.py: drop pool_pre_ping (aiomysql 0.3.2 incompat); pin
isolation_level=READ COMMITTED to avoid gap-lock deadlocks
- alembic env: disable_existing_loggers=False so in-process
migrations don't silence uvicorn's loggers
- docker-compose.override.yml: dev-only volume mount + --reload
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
89632e9937
commit
f326b41a08
23 changed files with 1637 additions and 95 deletions
|
|
@ -82,7 +82,9 @@ a:hover { text-decoration: underline; }
|
|||
.app-header .brand {
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
.app-header .brand:hover { color: var(--text); }
|
||||
.app-header .brand::before { content: "▰ "; opacity: 0.6; }
|
||||
.app-header nav a {
|
||||
margin-left: 18px;
|
||||
|
|
@ -1034,19 +1036,55 @@ details[open] .pf-analysis__head-left::before { content: "▾ "; }
|
|||
border-color: var(--accent) !important;
|
||||
}
|
||||
|
||||
/* User chip in header */
|
||||
/* Import preview action row — two stacked buttons with an explainer. */
|
||||
.import-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.import-choice { flex: 1 1 240px; min-width: 220px; }
|
||||
.import-choice button { width: 100%; }
|
||||
.import-choice .settings-row__hint {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* User chip in header — now a button that toggles a dropdown menu. */
|
||||
.user-menu { position: relative; margin-left: 8px; }
|
||||
.user-chip {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
color: var(--muted);
|
||||
margin-left: 8px;
|
||||
letter-spacing: 0.04em;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.user-chip a {
|
||||
color: var(--muted);
|
||||
border-bottom: 1px dotted var(--muted);
|
||||
.user-chip:hover { color: var(--accent); }
|
||||
.user-menu__caret { margin-left: 4px; opacity: 0.6; }
|
||||
.user-menu__panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
background: var(--surface-1, var(--surface-2));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
|
||||
z-index: 200;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.user-chip a:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.user-menu__item {
|
||||
display: block;
|
||||
padding: 8px 14px;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
}
|
||||
.user-menu__item:hover { background: var(--surface-2); color: var(--accent); }
|
||||
|
||||
/* --- Upload page (drag-drop CSV) ------------------------------------- */
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue