Complete Phase 3, Phase 5 polish and hardening

Phase 3 — Investments:
- Multi-currency support: holdings track purchase currency, FX rates convert to base for totals
- Capital gains report using UK Section 104 pool method, grouped by tax year
- Capital Gains tab added to Reports page

Phase 5 — Polish & Hardening:
- Mobile-responsive layout: bottom nav, sidebar hidden on mobile, logo in TopBar, compact header buttons, hover-only actions now always visible on touch
- Backup system: encrypted GPG backups via backup.sh, nightly scheduler job, admin API (list/trigger/download/restore), Settings UI with drag-to-restore confirmation
- Docker entrypoint with gosu privilege drop to fix bind-mount ownership on fresh deployments
- OWASP fixes: refresh token now bound to its session (new refresh_token_hash column + migration), CSRF secure flag tied to environment, IP-level rate limiting on login, TOTPEnableRequest Pydantic schema replaces raw dict
- AES-256-GCM key rotation script (rotate_keys.py) with dry-run mode and atomic DB transaction
- CLAUDE.md added for AI-assisted development context
- README updated: correct reverse proxy port, accurate backup/restore commands, key rotation instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-22 14:59:11 +00:00
parent 74e57a35c0
commit fe4e69b9ad
40 changed files with 2079 additions and 127 deletions

32
frontend/src/api/admin.ts Normal file
View file

@ -0,0 +1,32 @@
import { api } from "./client";
export interface BackupFile {
filename: string;
size_bytes: number;
created_at: string;
}
export async function listBackups(): Promise<BackupFile[]> {
const r = await api.get("/admin/backups");
return r.data;
}
export async function triggerBackup(): Promise<{ ok: boolean; message: string }> {
const r = await api.post("/admin/backup");
return r.data;
}
export async function downloadBackup(filename: string): Promise<void> {
const r = await api.get(`/admin/backups/${filename}`, { responseType: "blob" });
const url = URL.createObjectURL(r.data);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
export async function restoreBackup(filename: string): Promise<{ ok: boolean; message: string }> {
const r = await api.post(`/admin/restore/${filename}`);
return r.data;
}

View file

@ -61,11 +61,23 @@ export interface PricePoint {
volume: number | null;
}
export interface PerformanceMetrics {
twrr: number | null;
total_return: number;
total_return_pct: number;
currency: string;
}
export async function getPortfolio(): Promise<PortfolioSummary> {
const r = await api.get("/investments/portfolio");
return r.data;
}
export async function getPerformance(): Promise<PerformanceMetrics> {
const r = await api.get("/investments/performance");
return r.data;
}
export async function searchAssets(q: string): Promise<AssetSearchResult[]> {
const r = await api.get("/assets/search", { params: { q } });
return r.data;
@ -87,6 +99,11 @@ export async function createHolding(data: {
return r.data;
}
export async function updateHolding(id: string, data: { quantity: number; avg_cost_basis: number }): Promise<HoldingResponse> {
const r = await api.patch(`/investments/holdings/${id}`, data);
return r.data;
}
export async function deleteHolding(id: string): Promise<void> {
await api.delete(`/investments/holdings/${id}`);
}
@ -109,3 +126,33 @@ export async function getHoldingTransactions(holdingId: string): Promise<Investm
const r = await api.get(`/investments/holdings/${holdingId}/transactions`);
return r.data;
}
export interface CapitalGainsDisposal {
date: string;
symbol: string;
asset_name: string;
quantity: number;
proceeds: number;
cost: number;
gain: number;
currency: string;
}
export interface TaxYearSummary {
tax_year: string;
disposals: CapitalGainsDisposal[];
total_proceeds: number;
total_cost: number;
total_gain: number;
currency: string;
}
export interface CapitalGainsReport {
tax_years: TaxYearSummary[];
currency: string;
}
export async function getCapitalGains(): Promise<CapitalGainsReport> {
const r = await api.get("/investments/capital-gains");
return r.data;
}

View file

@ -122,6 +122,25 @@ export async function getSpendingTrends(months = 6): Promise<SpendingTrendsRepor
return r.data;
}
export interface SavingsRatePoint {
month: string;
income: number;
expenses: number;
savings: number;
savings_rate: number;
}
export interface SavingsRateReport {
points: SavingsRatePoint[];
avg_savings_rate: number;
currency: string;
}
export async function getSavingsRate(months = 12): Promise<SavingsRateReport> {
const r = await api.get("/reports/savings-rate", { params: { months } });
return r.data;
}
export interface BalanceSheetAccount {
id: string;
name: string;

View file

@ -61,6 +61,7 @@ export interface TransactionFilters {
date_from?: string;
date_to?: string;
search?: string;
is_recurring?: boolean;
page?: number;
page_size?: number;
}