feat: complete release readiness work

- consolidate API ownership and remove dead vendor code

- add Stripe billing, learning paths, and public CV hardening

- add migration, recovery, security, audit, and browser gates
This commit is contained in:
cesnimda
2026-07-31 16:54:16 +02:00
parent a23c3dfc97
commit ce76046a29
1634 changed files with 6889 additions and 135429 deletions
@@ -27,21 +27,20 @@ import {
} from "../components/ApplicationAssets";
import { ApplicationInterviewPrep } from "../components/InterviewPrep";
import {
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi,
WORKSPACE_SECTIONS, WorkspaceOverview, WorkspaceSectionKey, applicationWorkspaceApi, workspaceSection,
} from "../applicationWorkspace";
// Phase 5 Milestone 1 — the dedicated Application Workspace.
//
// This is a surface, not a new data store: the overview is one aggregate read, and each section
// reuses the component that already owns that domain (Attachments, Correspondence, AiWorkspacePanel).
// Sections belonging to later milestones say so rather than pretending to work.
// docs/architecture/application-workspace.md.
export default function ApplicationWorkspacePage() {
const { id } = useParams();
const jobId = Number(id);
const navigate = useNavigate();
const [params, setParams] = useSearchParams();
const section = (params.get("section") as WorkspaceSectionKey) || "overview";
const section = workspaceSection(params.get("section"));
const [overview, setOverview] = useState<WorkspaceOverview | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -87,8 +86,7 @@ export default function ApplicationWorkspacePage() {
<ListItemButton key={s.key} selected={section === s.key} onClick={() => go(s.key)} sx={{ borderRadius: 2 }}>
<ListItemText
primary={s.label}
secondary={s.milestone ? `Milestone ${s.milestone}` : undefined}
slotProps={{ primary: { fontSize: 14, fontWeight: section === s.key ? 700 : 500 }, secondary: { fontSize: 11 } }}
slotProps={{ primary: { fontSize: 14, fontWeight: section === s.key ? 700 : 500 } }}
/>
</ListItemButton>
))}
@@ -126,14 +124,10 @@ export default function ApplicationWorkspacePage() {
<Paper sx={{ p: 2, borderRadius: 3 }}><AiWorkspacePanel jobId={jobId} /></Paper>
</>
)}
{["portfolio", "notes"].includes(section) && (
<ComingInMilestone section={section} />
)}
</Box>
</Box>
);
}
function WorkspaceHeader({ overview }: { overview: WorkspaceOverview | null }) {
if (!overview) return <Paper sx={{ p: 2.5, borderRadius: 3 }}><Skeleton width="45%" height={34} /><Skeleton width="30%" /></Paper>;
return (
@@ -265,15 +259,3 @@ function JobDetailsSection({ overview }: { overview: WorkspaceOverview | null })
</Paper>
);
}
function ComingInMilestone({ section }: { section: string }) {
const m = WORKSPACE_SECTIONS.find((s) => s.key === section);
return (
<Paper sx={{ p: 3, borderRadius: 3, textAlign: "center" }}>
<Typography sx={{ fontWeight: 700 }}>{m?.label}</Typography>
<Typography color="text.secondary">
Arriving in Phase 5 milestone {m?.milestone}. Existing tools for this remain available in the application dialog.
</Typography>
</Paper>
);
}
+7 -1
View File
@@ -48,7 +48,13 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const nextPath = (location?.state?.from as string | undefined) ?? "/dashboard";
const requestedNextPath = location?.state?.from;
const nextPath = typeof requestedNextPath === "string"
&& requestedNextPath.startsWith("/")
&& !requestedNextPath.startsWith("//")
&& !requestedNextPath.includes("\\")
? requestedNextPath
: "/dashboard";
useEffect(() => {
api
+5 -1
View File
@@ -7,6 +7,7 @@ import { api } from "../api";
// iframe. noindex is enforced server-side (X-Robots-Tag) and reinforced with a meta tag here.
export default function PublicCvPage() {
const { slug } = useParams();
const pdfUrl = `${api.defaults.baseURL}/public-cv/${slug}/pdf`;
const [html, setHtml] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -49,7 +50,10 @@ export default function PublicCvPage() {
}
return (
<div style={{ minHeight: "100vh", background: "#e9edf2", padding: "24px 0", display: "flex", justifyContent: "center" }}>
<div style={{ minHeight: "100vh", background: "#e9edf2", padding: "24px 0", display: "flex", flexDirection: "column", alignItems: "center", gap: 16 }}>
<a href={pdfUrl} download style={{ padding: "10px 16px", borderRadius: 8, background: "#1d4ed8", color: "#fff", font: "600 14px system-ui", textDecoration: "none" }}>
Download PDF
</a>
<iframe
title="Public CV"
srcDoc={html}