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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user