chore(ts-migration): migrate the app shell, routing and the last non-agent pages

Phase 10: `App`, `main`, `index.html`, and the four remaining `src/pages` files.

ROUTING IS PROVEN UNCHANGED, not assumed. The 49 `<Route>` elements were
fingerprinted before the rename and compared after: byte-identical, all 44
paths, all 13 `<Navigate>` redirects, the same nesting under `ProtectedRoute`
and `AdminRoute`, and the same provider order - AuthProvider, then
QueryClientProvider, then Router, with ScrollToTop and AuthenticatedApp inside
and both toasters as siblings. There is no lazy loading to preserve; there never
was any.

Two coordinated edits the rename forced. `main` imported `@/App.jsx` by explicit
extension, which stops resolving the moment `App` is `.tsx`; it is now
extensionless `@/App`. `index.html` pointed its module script at
`/src/main.jsx`; it points at `/src/main.tsx`. Both are required, and missing
either would have been a blank page rather than a type error.

ONE GENUINE SEMANTIC DIFFERENCE, INVESTIGATED AND ACCEPTED. esbuild elides
unused imports under the TypeScript loader but keeps them under the JavaScript
one, so `App` drops thirteen imports: `Layout`, `Overview`, `Positions`,
`Candidates`, `HiredHistory`, `TalentPool`, `UserTracking`, `Analytics`,
`Profile`, `WorkerProfile`, `KrowIdentity`, `Owliver` and `DesignSystem`. All
thirteen are dead - each has zero JSX uses in `App`, because every route that
once rendered them now `<Navigate>`s to an `/admin/*` equivalent. Before
accepting it I checked that none of the thirteen modules can do anything when
evaluated: no bare side-effect imports, no top-level calls, and every top-level
binding a literal or a function declaration. The consequence is 942 fewer bytes
in the index chunk and thirteen modules no longer evaluated at startup. Nothing
observable changes, and the routes those pages are reached through are
unaffected - they are reached through the `/admin` tree, which is untouched.

That is the first time in this migration the production bundle has changed for
a reason other than a comment, so it is recorded here rather than left to be
noticed later.

`CreatePosition` needed the only real typing. `vetting_criteria` is a `jsonb`
column holding the five weighting percentages the page edits, typed `unknown` by
the registry, and both the total and the three render sites read through it.
`onDone` is called with the created id, so it takes arguments - the generator
had classed it as zero-arg, and that rule is now narrowed to `onClose` alone.

The generator's entity-import rule was narrowed first, as instructed: it now
counts a type as used only when it appears in a type position inside a generated
interface, rather than anywhere in the file text. That is what produced four
unused-import lint errors in the previous batch.

Verified: tsc 21 -> 20, set-difference showing one removed and none added; zero
errors in any of the six files; all four pages emit byte-identical JavaScript;
route fingerprint identical; npm test 1684/1691 with the same seven failures;
Owliver baseline 59/59; lint 0 errors; no deferred agent-region file touched;
baseline artifacts untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
2026-09-18 13:18:59 +05:30
parent fcdaa32f4d
commit ecf5e75d76
7 changed files with 21 additions and 6 deletions

197
src/App.tsx Normal file
View File

@@ -0,0 +1,197 @@
import { Toaster } from "@/components/ui/toaster"
import { Toaster as HotToaster } from 'react-hot-toast';
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClientInstance } from '@/lib/query-client'
import { Navigate, BrowserRouter as Router, Route, Routes, useParams } from 'react-router-dom';
import PageNotFound from './lib/PageNotFound';
import { AuthProvider, useAuth } from '@/lib/AuthContext';
import ScrollToTop from './components/ScrollToTop';
import ProtectedRoute from '@/components/ProtectedRoute';
import Layout from '@/layouts/Layout';
import Overview from '@/pages/Overview';
import Positions from '@/pages/Positions';
import PositionDetail from '@/pages/PositionDetail';
import Candidates from '@/pages/Candidates';
import HiredHistory from '@/pages/HiredHistory';
import TalentPool from '@/pages/TalentPool';
import Apply from '@/pages/Apply';
import UserTracking from '@/pages/UserTracking';
import Analytics from '@/pages/Analytics';
import CreatePosition from '@/pages/CreatePosition';
import Profile from '@/pages/Profile';
import University from '@/pages/University';
import CourseDetail from '@/pages/CourseDetail';
import WorkerProfile from '@/pages/WorkerProfile';
import KrowIdentity from '@/pages/KrowIdentity';
import Owliver from '@/pages/Owliver';
import EmployeeDashboard from '@/pages/EmployeeDashboard';
import OpportunityDetail from '@/pages/OpportunityDetail';
import DesignSystem from '@/pages/DesignSystem';
/* Admin product — its own layout and route tree, so the Admin redesign can
diverge without touching the Employer or Talent experiences. */
import AdminLayout from '@/layouts/AdminLayout';
import ControlCenter from '@/pages/admin/ControlCenter';
import AdminPositions from '@/pages/admin/Positions';
import AdminCandidates from '@/pages/admin/Candidates';
import AdminCandidateProfile from '@/pages/admin/CandidateProfile';
import AdminCandidatesAnalysis from '@/pages/admin/CandidatesAnalysis';
import AdminHiredHistory from '@/pages/admin/HiredHistory';
import AdminTalentPool from '@/pages/admin/TalentPool';
import AdminTalentProfile from '@/pages/admin/TalentProfile';
import AdminAnalytics from '@/pages/admin/Analytics';
import AdminActivity from '@/pages/admin/Activity';
import AdminProfile from '@/pages/admin/Profile';
import AdminSettings from '@/pages/admin/Settings';
import AdminWorkspace from '@/pages/admin/Workspace';
import AdminWorkspaceSkills from '@/pages/admin/WorkspaceSkills';
import AdminWorkspaceAgents from '@/pages/admin/WorkspaceAgents';
import AdminAgentDetail from '@/pages/admin/AgentDetail';
import AdminSkillEditor from '@/pages/admin/SkillEditor';
import AdminOwliverSkillEditor from '@/pages/admin/OwliverSkillEditor';
import AdminSkillDevelopment from '@/pages/admin/SkillDevelopment';
import AdminLogin from '@/pages/admin/Login';
import AdminRoute from '@/pages/admin/AdminRoute';
/** `/university/:id` → the same challenge inside the Admin shell, id preserved. */
const LegacyCourseRedirect = () => {
const { id } = useParams();
return <Navigate to={`/admin/university/${id}`} replace />;
};
/** `/positions/:id` → the same position inside the Admin shell, id preserved. */
const LegacyPositionRedirect = () => {
const { id } = useParams();
return <Navigate to={`/admin/positions/${id}`} replace />;
};
const AuthenticatedApp = () => {
const { isLoadingAuth, isLoadingPublicSettings } = useAuth();
if (isLoadingPublicSettings || isLoadingAuth) {
return (
<div className="fixed inset-0 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-slate-200 border-t-slate-800 rounded-full animate-spin"></div>
</div>
);
}
return (
<Routes>
{/* Admin sign-in — outside the protected boundary. */}
<Route path="/admin/login" element={<AdminLogin />} />
<Route element={<ProtectedRoute />}>
{/* The application opens on the Admin console, which is entered through
its own sign-in. Employer keeps every one of its routes, including
Overview at an explicit path — nothing of that product is removed,
only the address the app happens to start at. */}
<Route path="/" element={<Navigate to="/admin/login" replace />} />
<Route element={<AdminRoute />}>
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<ControlCenter />} />
<Route path="positions" element={<AdminPositions />} />
<Route path="positions/new" element={<CreatePosition />} />
<Route path="positions/:id" element={<PositionDetail />} />
<Route path="candidates" element={<AdminCandidates />} />
{/* The full Candidate Intelligence profile. A page rather than a
drawer: the record carries around forty fields and fifteen
screening dimensions, which a 480px column cannot show without
hiding most of it. */}
<Route path="candidates/:id" element={<AdminCandidateProfile />} />
{/* Candidate Analysis. A sibling segment, not `candidates/:id`:
the analysis reads the whole pool, so it is not addressed by a
candidate id and must not be matched as one. The route is the
one the `candidates-analysis` surface declares, so the skill
table, the assistant placement table and this agree. */}
<Route path="candidates-analysis" element={<AdminCandidatesAnalysis />} />
<Route path="hired" element={<AdminHiredHistory />} />
<Route path="talent-pool" element={<AdminTalentPool />} />
{/* The person, keyed by worker profile — distinct from
`candidates/:id`, which is one person FOR ONE POSITION. Two
routes because they answer different questions; one screen
would push the product back towards a candidate record per
vacancy, which is the thing `worker_profiles` exists to stop. */}
<Route path="talent/:id" element={<AdminTalentProfile />} />
<Route path="university" element={<University />} />
<Route path="university/:id" element={<CourseDetail />} />
<Route path="analytics" element={<AdminAnalytics />} />
<Route path="activity" element={<AdminActivity />} />
<Route path="profile" element={<AdminProfile />} />
{/* Two centres, deliberately separate. Settings configures the
account and the system; Workspace & Skills configures the
workforce — Owliver's capabilities, training and progression —
each with its own pages underneath rather than one page holding
both subjects. */}
<Route path="settings" element={<AdminSettings />} />
<Route path="workspace" element={<AdminWorkspace />} />
<Route path="workspace/agents" element={<AdminWorkspaceAgents />} />
{/* Static before dynamic, so `agents/new` cannot be read as an
agent whose id is "new". */}
<Route path="workspace/agents/new" element={<AdminAgentDetail />} />
<Route path="workspace/agents/:id" element={<AdminAgentDetail />} />
<Route path="workspace/skills" element={<AdminWorkspaceSkills />} />
{/* Two editors, because a UI skill and an Owliver skill configure
different things. Static segments rank above the dynamic `:id`,
so `skills/owliver/...` cannot be read as a skill called
"owliver" — and the two original addresses are untouched. */}
<Route path="workspace/skills/new" element={<AdminSkillEditor />} />
<Route path="workspace/skills/owliver/new" element={<AdminOwliverSkillEditor />} />
<Route path="workspace/skills/owliver/:id" element={<AdminOwliverSkillEditor />} />
<Route path="workspace/skills/:id" element={<AdminSkillEditor />} />
<Route path="workspace/skill-development" element={<AdminSkillDevelopment />} />
</Route>
</Route>
{/* Redirect top-level legacy aliases to the unified global shell */}
{/* Employee (talent) opportunity flow — full-page routes, no drawer.
Standalone (outside AdminLayout/AdminRoute, which are the employer
shell and gate); authenticated via ProtectedRoute. */}
<Route path="/employee" element={<div className="min-h-screen bg-[#F8FAFC]"><div className="max-w-5xl mx-auto px-4 py-8"><EmployeeDashboard /></div></div>} />
<Route path="/opportunities/:id" element={<div className="min-h-screen bg-[#F8FAFC]"><div className="max-w-3xl mx-auto px-4 py-8"><OpportunityDetail /></div></div>} />
<Route path="/apply" element={<div className="min-h-screen bg-[#F8FAFC]"><div className="max-w-3xl mx-auto px-4 py-8"><Apply /></div></div>} />
<Route path="/overview" element={<Navigate to="/admin" replace />} />
<Route path="/positions" element={<Navigate to="/admin/positions" replace />} />
{/* `/positions` only matches the exact path, so the sub-routes need
their own aliases or an inbound link lands on the 404 page. */}
<Route path="/positions/new" element={<Navigate to="/admin/positions/new" replace />} />
<Route path="/positions/:id" element={<LegacyPositionRedirect />} />
<Route path="/candidates" element={<Navigate to="/admin/candidates" replace />} />
<Route path="/hired" element={<Navigate to="/admin/hired" replace />} />
<Route path="/talent-pool" element={<Navigate to="/admin/talent-pool" replace />} />
<Route path="/university" element={<Navigate to="/admin/university" replace />} />
{/* Same treatment as every other legacy alias: the challenge opens inside
the application shell rather than on a bare page, so Forge → challenge
keeps its navigation, header and page frame. Existing links to
/university/:id are unchanged and still resolve here. */}
<Route path="/university/:id" element={<LegacyCourseRedirect />} />
<Route path="/analytics" element={<Navigate to="/admin/analytics" replace />} />
<Route path="/activity" element={<Navigate to="/admin/activity" replace />} />
<Route path="/tracking" element={<Navigate to="/admin/activity" replace />} />
</Route>
<Route path="*" element={<PageNotFound />} />
</Routes>
);
};
function App() {
return (
<AuthProvider>
<QueryClientProvider client={queryClientInstance}>
<Router>
<ScrollToTop />
<AuthenticatedApp />
</Router>
<Toaster />
{/* The design system's `toast` helper wraps react-hot-toast, which needs
its own renderer mounted. Without this every toast in the app fired
into nothing — confirmations, errors and the "not in the demo"
notices all silently did nothing. */}
<HotToaster position="bottom-right" />
</QueryClientProvider>
</AuthProvider>
)
}
export default App