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

View file

@ -1,8 +1,11 @@
import { useState, useEffect } from "react";
import { X, Search, Loader2 } from "lucide-react";
import { searchAssets, createHolding, addInvestmentTransaction, AssetSearchResult } from "@/api/investments";
import { useUiStore } from "@/store/uiStore";
import { format } from "date-fns";
const COMMON_CURRENCIES = ["GBP", "USD", "EUR", "JPY", "CAD", "AUD", "CHF"];
interface Account { id: string; name: string; type: string; }
interface Props {
@ -12,6 +15,7 @@ interface Props {
}
export default function AddHoldingModal({ accounts, onClose, onSuccess }: Props) {
const baseCurrency = useUiStore(s => s.currency);
const [query, setQuery] = useState("");
const [results, setResults] = useState<AssetSearchResult[]>([]);
const [searching, setSearching] = useState(false);
@ -27,6 +31,7 @@ export default function AddHoldingModal({ accounts, onClose, onSuccess }: Props)
account_id: investAccounts[0]?.id ?? "",
quantity: "",
price: "",
currency: baseCurrency,
fees: "0",
date: format(new Date(), "yyyy-MM-dd"),
});
@ -58,7 +63,7 @@ export default function AddHoldingModal({ accounts, onClose, onSuccess }: Props)
asset_id: selected.id,
quantity: qty,
avg_cost_basis: price,
currency: selected.currency,
currency: form.currency,
});
await addInvestmentTransaction({
holding_id: holding.id,
@ -66,7 +71,7 @@ export default function AddHoldingModal({ accounts, onClose, onSuccess }: Props)
quantity: qty,
price: price,
fees: parseFloat(form.fees) || 0,
currency: selected.currency,
currency: form.currency,
date: form.date,
});
onSuccess();
@ -156,13 +161,22 @@ export default function AddHoldingModal({ accounts, onClose, onSuccess }: Props)
</div>
<div>
<label className="text-xs font-medium block mb-1">Price paid *</label>
<input
type="number" min="0" step="any"
value={form.price}
onChange={(e) => setForm(f => ({ ...f, price: e.target.value }))}
className="w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="150.00"
/>
<div className="flex gap-1">
<select
value={form.currency}
onChange={(e) => setForm(f => ({ ...f, currency: e.target.value }))}
className="rounded-md border border-input bg-background px-1.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-ring"
>
{COMMON_CURRENCIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<input
type="number" min="0" step="any"
value={form.price}
onChange={(e) => setForm(f => ({ ...f, price: e.target.value }))}
className="w-full rounded-md border border-input bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="150.00"
/>
</div>
</div>
<div>
<label className="text-xs font-medium block mb-1">Fees</label>
@ -176,6 +190,12 @@ export default function AddHoldingModal({ accounts, onClose, onSuccess }: Props)
</div>
</div>
{selected && form.currency !== selected.currency && (
<p className="text-xs text-muted-foreground bg-secondary/50 rounded-md px-3 py-2">
Price in <strong>{form.currency}</strong> asset trades in <strong>{selected.currency}</strong>. Current values will be converted using live exchange rates.
</p>
)}
<div>
<label className="text-sm font-medium block mb-1.5">Purchase date *</label>
<input