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:
parent
74e57a35c0
commit
fe4e69b9ad
40 changed files with 2079 additions and 127 deletions
32
frontend/src/api/admin.ts
Normal file
32
frontend/src/api/admin.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export interface TransactionFilters {
|
|||
date_from?: string;
|
||||
date_to?: string;
|
||||
search?: string;
|
||||
is_recurring?: boolean;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue