Release 0.2.22: capital chart, Investor View, GP stakes, doc folders
Snapshot commit bringing the uncommitted phase-2 work into version control
together with four new features and the 0.2.22 version bump.
New features:
- Investor capital-over-time chart (value, paid-in, distributions per
quarter), rendered from existing capital-account history.
- Admin Investor View: read-only reconstruction of an investor's portal
(GET /api/users/{id}/investor-view), reusing the investor portal UI.
- Document upload scoped to the selected fund's own investors, with an
explicit upload-target confirmation to prevent mis-attaching.
- GP/mgmt entities gain an Assets tab listing their stakes in the funds
they manage (new entity_stakes table + /api/entities/{id}/stakes).
- Edit-entity form (change type/status/etc.), so GP entities can be
categorized correctly.
Verified: 11/11 backend tests pass; alembic upgrades to head b8c9d0e1f2a3;
frontend tsc + vite build clean; s9pk packs at 0.2.22:0 (x86_64).
Also: ignore .DS_Store and *.s9pk artifacts.
This commit is contained in:
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import FileResponse
|
||||
|
||||
@@ -18,16 +19,26 @@ from ten31portal.routers.holding_router import router as holding_router
|
||||
from ten31portal.routers.position_router import router as position_router
|
||||
from ten31portal.routers.round_router import router as round_router
|
||||
from ten31portal.routers.import_router import router as import_router
|
||||
from ten31portal.routers.user_router import router as user_router
|
||||
from ten31portal.routers.document_router import router as document_router
|
||||
from ten31portal.routers.capital_account_router import router as capital_account_router
|
||||
from ten31portal.routers.capital_import_router import router as capital_import_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
run_migrations()
|
||||
from ten31portal.storage import ensure_docs_dir
|
||||
ensure_docs_dir()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="Ten31Portal", version="0.1.0", lifespan=lifespan)
|
||||
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
|
||||
# Compress HTML/JS/CSS/JSON over the wire (the 320KB JS bundle gzips to ~80KB). Added after
|
||||
# SessionMiddleware so it sits outermost and compresses the final response. minimum_size skips
|
||||
# tiny payloads (health checks, small JSON) where compression isn't worth it.
|
||||
app.add_middleware(GZipMiddleware, minimum_size=500)
|
||||
app.include_router(auth_router)
|
||||
app.include_router(audit_router)
|
||||
app.include_router(entity_router)
|
||||
@@ -35,6 +46,10 @@ app.include_router(holding_router)
|
||||
app.include_router(position_router)
|
||||
app.include_router(round_router)
|
||||
app.include_router(import_router)
|
||||
app.include_router(user_router)
|
||||
app.include_router(document_router)
|
||||
app.include_router(capital_account_router)
|
||||
app.include_router(capital_import_router)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
@@ -45,12 +60,29 @@ def health() -> dict[str, str]:
|
||||
# Serve built frontend in production (when static/ dir exists next to the app)
|
||||
_static_dir = Path(__file__).resolve().parent.parent / "static"
|
||||
if _static_dir.is_dir():
|
||||
@app.get("/{path:path}")
|
||||
# index.html must always be revalidated so a new build is picked up right after an
|
||||
# upgrade (its hashed asset references change). Hashed assets themselves are immutable.
|
||||
_NO_CACHE = {"Cache-Control": "no-cache, must-revalidate"}
|
||||
# Vite content-hashes asset filenames (index-UPbvqVP1.js), so they can cache forever.
|
||||
_IMMUTABLE = {"Cache-Control": "public, max-age=31536000, immutable"}
|
||||
_ONE_DAY = {"Cache-Control": "public, max-age=86400"}
|
||||
|
||||
def _index() -> FileResponse:
|
||||
return FileResponse(_static_dir / "index.html", headers=_NO_CACHE)
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "HEAD"])
|
||||
async def serve_spa(path: str):
|
||||
file = _static_dir / path
|
||||
if file.is_file():
|
||||
# Don't let the HTML entrypoint get cached; fingerprinted assets can cache.
|
||||
if file.name == "index.html":
|
||||
return _index()
|
||||
if path.startswith("assets/"):
|
||||
return FileResponse(file, headers=_IMMUTABLE)
|
||||
if path in ("ten31-logo.png", "favicon.svg", "favicon.ico"):
|
||||
return FileResponse(file, headers=_ONE_DAY)
|
||||
return FileResponse(file)
|
||||
return FileResponse(_static_dir / "index.html")
|
||||
return _index()
|
||||
|
||||
|
||||
def cli() -> None:
|
||||
|
||||
Reference in New Issue
Block a user