From aa42d9ee8132f50ee167506f640c60b3793f26d3 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Mon, 24 Aug 2026 18:10:47 +0530 Subject: [PATCH] updates on the ui changes and changed into doormile --- .claude/launch.json | 2 +- .env.example | 12 + .gitignore | 2 + README.md | 285 +- fix_imports.cjs | 31 + index.html | 8 +- package-lock.json | 2312 +--- package.json | 23 +- patchImports.js | 31 + public/doormile-logo.png | Bin 0 -> 13651 bytes public/doormile-mark-cropped.png | Bin 0 -> 70275 bytes public/doormile-mark.png | Bin 0 -> 83551 bytes public/favicon.png | Bin 21146 -> 70275 bytes scripts/__baseline__/owliver-baseline.json | 1189 -- scripts/owliver-baseline.mjs | 53 - scripts/owliver-capture.mjs | 158 - scripts/skill-check.mjs | 4393 ------- skill-examples/board-invalid-context.md | 26 - skill-examples/board-page-level.md | 31 - skill-examples/board-position-card.md | 31 - skill-examples/board-ui-test.md | 40 - skill-examples/owliver-conversation-test.md | 63 - src/App.jsx | 236 +- src/agents/activity-agent.md | 42 - src/agents/analytics-agent.md | 42 - src/agents/candidates-agent.md | 41 - src/agents/control-center-agent.md | 47 - src/agents/hired-history-agent.md | 41 - src/agents/krow-forge-agent.md | 41 - src/agents/krow-workforce-agent.md | 100 - src/agents/positions-agent.md | 42 - src/agents/talent-pool-agent.md | 40 - src/api/aiEngine.js | 809 -- src/api/attendanceSeed.js | 233 - src/api/base44Client.js | 165 - src/api/doormile/client.js | 93 + src/api/doormile/endpoints.js | 655 + src/api/doormile/index.js | 29 + src/api/doormile/notify.js | 22 + src/api/doormile/queries.js | 1107 ++ src/api/seed.js | 1928 --- src/api/store.js | 173 - src/assets/brand.js | 17 +- src/assets/images/doormile-mark.png | Bin 0 -> 83551 bytes src/components/CircularLoader.jsx | 29 + src/components/ProtectedRoute.jsx | 35 +- src/components/UserNotRegisteredError.jsx | 31 - src/components/admin/PageShell.jsx | 97 - src/components/admin/RoleGlyph.jsx | 73 - src/components/agents/AddSkillsModal.jsx | 180 - src/components/agents/AgentCanvas.jsx | 516 - src/components/agents/AgentConfigure.jsx | 681 - src/components/agents/AgentInsightsPanel.jsx | 238 - src/components/agents/AgentTestPanel.jsx | 347 - src/components/agents/icons.js | 30 - src/components/ai-assistant/AgentContext.jsx | 196 - src/components/ai-assistant/AgentSwitcher.jsx | 234 - .../ai-assistant/AssistantMessage.jsx | 127 - .../ai-assistant/AssistantPanel.jsx | 199 - .../ai-assistant/AssistantPanelContext.jsx | 239 - .../ai-assistant/AssistantTrigger.jsx | 73 - src/components/ai-assistant/KrowAssistant.jsx | 839 -- src/components/ai-assistant/PageContext.jsx | 154 - src/components/ai-assistant/PromptChips.jsx | 83 - src/components/ai-assistant/PromptInput.jsx | 140 - src/components/ai-assistant/ResizeDivider.jsx | 110 - .../ai-assistant/ResponseBlocks.jsx | 543 - src/components/ai-assistant/blocks.js | 123 - .../ai-assistant/capabilities/admin.js | 2142 ---- .../ai-assistant/capabilities/employer.js | 592 - .../ai-assistant/capabilities/workspace.js | 552 - src/components/ai-assistant/contexts.js | 249 - src/components/ai-assistant/dynamic.js | 817 -- src/components/ai-assistant/history.js | 250 - src/components/ai-assistant/index.jsx | 32 - src/components/ai-assistant/insights.js | 369 - src/components/ai-assistant/placement.js | 172 - src/components/ai-assistant/provider.js | 207 - src/components/ai-assistant/routing.js | 930 -- src/components/ai-assistant/useAssistant.js | 812 -- src/components/assistant/DoormileAI.css | 1193 ++ .../assistant/DoormileAI/AIComposer.jsx | 158 + .../assistant/DoormileAI/AIFlowStep.jsx | 118 + .../assistant/DoormileAI/AIMessage.jsx | 245 + .../assistant/DoormileAI/AIPanel.jsx | 1591 +++ .../assistant/DoormileAI/AIParts.jsx | 149 + .../assistant/DoormileAI/AIRowsStep.jsx | 154 + .../assistant/DoormileAI/AIWelcome.jsx | 90 + src/components/assistant/DoormileAI/index.jsx | 39 + .../assistant/DoormileAI/pageContext.jsx | 413 + src/components/assistant/DoormileAI/shim.jsx | 139 + .../charts/DepartmentPerformance.jsx | 618 - src/components/charts/HiringFlow.jsx | 115 - src/components/charts/HiringTrendChart.jsx | 61 - src/components/charts/muiChartTheme.js | 86 - .../doormile/AddressAutocomplete.jsx | 197 + src/components/doormile/BatchAnalysis.jsx | 448 + src/components/doormile/BatchEfficiency.jsx | 102 + src/components/doormile/ComparePanel.jsx | 390 + src/components/doormile/DispatchMap.jsx | 323 + src/components/doormile/ListToolbar.jsx | 70 + src/components/doormile/RiderInfoPanel.jsx | 206 + .../doormile/assistant/AssistantPanel.jsx | 405 + src/components/ds/ActivityCard.jsx | 4 +- src/components/ds/Alert.jsx | 4 +- src/components/ds/Avatar.jsx | 6 +- src/components/ds/ChartContainer.jsx | 2 +- src/components/ds/DataTable.jsx | 6 +- src/components/ds/Drawer.jsx | 2 +- src/components/ds/EmptyState.jsx | 2 +- src/components/ds/FilterBar.jsx | 2 +- src/components/ds/InsightRow.jsx | 8 +- src/components/ds/KpiCard.jsx | 111 +- src/components/ds/Modal.jsx | 2 +- src/components/ds/MultiSelect.jsx | 12 +- src/components/ds/PageHeader.jsx | 4 +- src/components/ds/Progress.jsx | 12 +- src/components/ds/SectionHeader.jsx | 4 +- src/components/ds/StatusBadge.jsx | 115 +- src/components/ds/Tabs.jsx | 6 +- src/components/ds/Timeline.jsx | 2 +- src/components/ds/Toggle.jsx | 2 +- src/components/forge/AddSkillTraining.jsx | 523 - src/components/forge/ChallengeDirectory.jsx | 191 - src/components/forge/ForgeHeader.jsx | 49 - src/components/forge/ForgeInsight.jsx | 38 - src/components/forge/ForgeProgress.jsx | 142 - src/components/forge/ForgeRail.jsx | 118 - src/components/forge/NextSkill.jsx | 267 - src/components/forge/SkillCard.jsx | 210 - src/components/forge/SkillDetail.jsx | 185 - src/components/forge/SkillLibrary.jsx | 177 - src/components/forge/SkillProgression.jsx | 288 - src/components/forge/YourForge.jsx | 133 - src/components/forge/challengeMeta.js | 174 - src/components/forge/index.js | 49 - src/components/krow/AIInterviewModal.jsx | 452 - src/components/krow/ActivityCharts.jsx | 107 - src/components/krow/ActivityLogTable.jsx | 131 - src/components/krow/ActivityMetrics.jsx | 41 - src/components/krow/AnalyticsMetrics.jsx | 37 - src/components/krow/ApplyIdentityCard.jsx | 151 - src/components/krow/CandidateCard.jsx | 286 - .../krow/CandidateExpandedDetails.jsx | 147 - src/components/krow/CertificationManager.jsx | 99 - src/components/krow/HiringAnalytics.jsx | 93 - src/components/krow/ImpactMetrics.jsx | 131 - src/components/krow/InterviewAnalytics.jsx | 99 - src/components/krow/InterviewResults.jsx | 190 - src/components/krow/InviteApplicantsModal.jsx | 145 - src/components/krow/Journey.jsx | 183 - .../krow/KROWCandidateLeaderboard.jsx | 73 - src/components/krow/KROWJobPostingCard.jsx | 90 - src/components/krow/KrowIdPreview.jsx | 136 - src/components/krow/KrowIdentityCard.jsx | 82 - src/components/krow/MatchedCandidates.jsx | 359 - src/components/krow/MessageCandidateModal.jsx | 80 - src/components/krow/OwliverAvatar.jsx | 13 - src/components/krow/OwliverChatBubble.jsx | 76 - src/components/krow/PipelineFunnel.jsx | 68 - src/components/krow/PositionDetails.jsx | 156 - src/components/krow/RateEndorseModal.jsx | 154 - .../krow/ReferencesEndorsements.jsx | 120 - src/components/krow/RetentionMetrics.jsx | 102 - .../krow/ScheduleInterviewModal.jsx | 94 - src/components/krow/ScoreDistribution.jsx | 67 - .../krow/SkillRequirementsField.jsx | 132 - src/components/krow/SuggestedTalent.jsx | 154 - src/components/krow/TalentDetailModal.jsx | 286 - src/components/krow/TalentPoolCard.jsx | 246 - src/components/krow/UniversityCourseCard.jsx | 88 - src/components/krow/VerifiedSkills.jsx | 281 - src/components/krow/WorkforceReputation.jsx | 254 - .../krow/proving/ChallengeRunner.jsx | 310 - .../krow/proving/HazardIdentifier.jsx | 82 - .../krow/proving/MediaChallenge.jsx | 69 - .../krow/proving/RoleplayChallenge.jsx | 82 - src/components/krow/proving/VideoRecorder.jsx | 103 - src/components/krow/talent/CareerGrowth.jsx | 36 - src/components/krow/talent/CareerPassport.jsx | 70 - .../krow/talent/CareerScoreGauge.jsx | 63 - .../krow/talent/EliteTalentScore.jsx | 66 - .../krow/talent/EmployerInsights.jsx | 66 - .../krow/talent/ExperienceEndorsements.jsx | 72 - .../krow/talent/ExperienceNetwork.jsx | 48 - .../krow/talent/IdentityCareerCard.jsx | 262 - .../krow/talent/IncreaseMarketValue.jsx | 46 - src/components/krow/talent/MarketOffers.jsx | 94 - src/components/krow/talent/TalentHero.jsx | 124 - .../krow/talent/VerifiedExperienceList.jsx | 91 - .../nearle_components/AddressAutocomplete.jsx | 261 + src/components/nearle_components/CLAUDE.md | 178 + .../nearle_components/DateRangePicker.jsx | 113 + .../nearle_components/DebounceSearchBar.jsx | 63 + .../nearle_components/GlobalToast.jsx | 22 + .../nearle_components/LoaderWithImage.jsx | 56 + .../LocationAutocomplete.jsx | 106 + .../nearle_components/MobileCard.jsx | 132 + .../nearle_components/PageHeader.jsx | 88 + .../nearle_components/PageLayout.jsx | 238 + .../nearle_components/SegmentCard.jsx | 101 + src/components/nearle_components/StatCard.jsx | 104 + .../nearle_components/StatusBadge.jsx | 38 + .../nearle_components/StatusTabs.jsx | 109 + .../nearle_components/TableLoader.jsx | 28 + src/components/skills/SkillSections.jsx | 401 - src/components/skills/SkillSurface.jsx | 199 - src/components/third-party/Notistack.jsx | 61 + src/components/third-party/OpenToast.js | 12 + src/components/third-party/ReactTable.jsx | 42 + src/components/ui/badge.jsx | 8 +- src/components/ui/button.jsx | 12 +- src/components/ui/checkbox.jsx | 2 +- src/components/ui/input.jsx | 2 +- src/components/ui/radio-group.jsx | 2 +- src/components/ui/select.jsx | 2 +- src/components/ui/sheet.jsx | 14 +- src/components/ui/switch.jsx | 2 +- src/index.css | 139 +- src/layouts/AdminLayout.jsx | 576 +- src/layouts/Layout.jsx | 395 - src/lib/AuthContext.jsx | 133 +- src/lib/PageNotFound.jsx | 111 +- src/lib/activitySignals.js | 103 - src/lib/admin/permissions.js | 28 - src/lib/admin/positionInsights.js | 127 - src/lib/admin/session.js | 50 - src/lib/agents/agentConfig.js | 313 - src/lib/agents/agentFields.js | 209 - src/lib/agents/agentLifecycle.js | 83 - src/lib/agents/context.js | 114 - src/lib/agents/conversationInsights.js | 153 - src/lib/agents/customAgents.js | 114 - src/lib/agents/knowledge.js | 157 - src/lib/agents/registry.js | 276 - src/lib/agents/runtime.js | 388 - src/lib/agents/useAgents.js | 137 - src/lib/agents/vocabulary.js | 117 - src/lib/assistant/actions.js | 203 + src/lib/assistant/assignActions.js | 268 + src/lib/assistant/assignFlow.js | 200 + src/lib/assistant/bulkFile.js | 223 + src/lib/assistant/bulkFlow.js | 193 + src/lib/assistant/bulkOrderActions.js | 299 + src/lib/assistant/customerFlow.js | 142 + src/lib/assistant/flowEngine.js | 83 + src/lib/assistant/flows.js | 613 + src/lib/assistant/intents.js | 2319 ++++ src/lib/assistant/orderActions.js | 152 + src/lib/assistant/orderFlow.js | 268 + src/lib/assistant/pageContext.js | 103 + src/lib/assistant/ragRouter.js | 66 + src/lib/assistant/repeatFlow.js | 30 + src/lib/assistant/repeatRuns.js | 244 + src/lib/assistant/scan.js | 145 + src/lib/assistant/vocab.js | 342 + src/lib/attendance.js | 389 - src/lib/batchBucket.js | 73 + src/lib/bulkOrderColumns.js | 150 + src/lib/candidateIntelligence.js | 235 - src/lib/dispatchPreview.js | 270 + src/lib/dispatchShared.js | 442 + src/lib/distance.js | 78 + src/lib/doormileFormat.js | 102 + src/lib/doormileHooks.js | 771 ++ src/lib/doormileTimestamp.js | 33 + src/lib/hiringRecords.js | 315 - src/lib/krowAi.js | 463 - src/lib/krowHooks.js | 708 -- src/lib/krowScore.js | 126 - src/lib/logger.js | 35 + src/lib/orderStatusGroups.js | 64 + src/lib/positionModel.js | 264 - src/lib/preferences.js | 65 + src/lib/profitability.js | 133 + src/lib/provingGround.js | 78 - src/lib/roleCategories.js | 9 - src/lib/skillGraph.js | 399 - src/lib/skills/actions.js | 506 - src/lib/skills/customSkills.js | 182 - src/lib/skills/dataResolver.js | 1214 -- src/lib/skills/draftFlow.js | 361 - src/lib/skills/owliverConfig.js | 247 - src/lib/skills/owliverResolver.js | 416 - src/lib/skills/pageSkills.js | 127 - src/lib/skills/positionFlow.js | 512 - src/lib/skills/registry.js | 859 -- src/lib/skills/saveFeedback.js | 32 - src/lib/skills/skillFields.js | 595 - src/lib/skills/surfaces.js | 896 -- src/lib/skills/tools.js | 177 - src/lib/skills/uiConfig.js | 397 - src/lib/skills/usePageSkills.js | 84 - src/lib/skills/workforceFlow.js | 924 -- src/lib/skills/yaml.js | 174 - src/lib/talentHome.js | 73 - src/lib/talentInsights.js | 33 - src/lib/userTracking.js | 53 - src/lib/workforce.js | 574 - src/pages/Analytics.jsx | 62 - src/pages/Apply.jsx | 167 - src/pages/Candidates.jsx | 174 - src/pages/CourseDetail.jsx | 172 - src/pages/CreatePosition.jsx | 447 - src/pages/DesignSystem.jsx | 756 -- src/pages/EmployeeDashboard.jsx | 34 - src/pages/HiredHistory.jsx | 227 - src/pages/KrowIdentity.jsx | 193 - src/pages/Login.css | 500 + src/pages/Login.jsx | 222 + src/pages/Overview.jsx | 198 - src/pages/Owliver.jsx | 552 - src/pages/PositionDetail.jsx | 522 - src/pages/Positions.jsx | 95 - src/pages/Profile.jsx | 130 - src/pages/TalentPool.jsx | 129 - src/pages/University.jsx | 312 - src/pages/UserTracking.jsx | 36 - src/pages/WorkerProfile.jsx | 26 - src/pages/admin/Activity.jsx | 301 - src/pages/admin/AdminRoute.jsx | 25 - src/pages/admin/AgentDetail.jsx | 267 - src/pages/admin/Analytics.jsx | 364 - src/pages/admin/CandidateProfile.jsx | 1380 -- src/pages/admin/Candidates.jsx | 185 - src/pages/admin/CandidatesAnalysis.jsx | 422 - src/pages/admin/ControlCenter.jsx | 983 -- src/pages/admin/HiredHistory.jsx | 345 - src/pages/admin/Login.jsx | 223 - src/pages/admin/OwliverSkillEditor.jsx | 816 -- src/pages/admin/Positions.jsx | 1211 -- src/pages/admin/Profile.jsx | 496 - src/pages/admin/Settings.jsx | 305 - src/pages/admin/SkillDevelopment.jsx | 262 - src/pages/admin/SkillEditor.jsx | 768 -- src/pages/admin/TalentPool.jsx | 367 - src/pages/admin/Workspace.jsx | 276 - src/pages/admin/WorkspaceAgents.jsx | 564 - src/pages/admin/WorkspaceSkills.jsx | 745 -- src/pages/api/api.js | 2 + src/pages/api/doormileApi.js | 2 + src/pages/doormile/ViewProfile.jsx | 132 + src/pages/doormile/appUsers/AppUsers.jsx | 324 + src/pages/doormile/clients/CreateClient.jsx | 156 + src/pages/doormile/clients/CreateCustomer.jsx | 180 + src/pages/doormile/clients/Tenants.jsx | 582 + .../competitiveIntel/CompetitiveIntel.jsx | 324 + src/pages/doormile/customers/Customers.jsx | 270 + src/pages/doormile/deliveries/Deliveries.jsx | 693 + src/pages/doormile/dispatch/ActiveSection.jsx | 245 + src/pages/doormile/dispatch/CLAUDE.md | 165 + .../doormile/dispatch/CompareDataPanel.jsx | 1093 ++ src/pages/doormile/dispatch/Dispatch.css | 10606 ++++++++++++++++ src/pages/doormile/dispatch/Dispatch.js | 2 + src/pages/doormile/dispatch/Dispatch.jsx | 6703 ++++++++++ src/pages/doormile/dispatch/Preview.js | 2 + src/pages/doormile/dispatch/Preview.jsx | 1020 ++ .../dispatch/ProfitabilitySection.jsx | 627 + src/pages/doormile/dispatch/dispatchShared.js | 412 + src/pages/doormile/exceptions/Exceptions.jsx | 434 + src/pages/doormile/hubs/Hubs.jsx | 388 + src/pages/doormile/orders/CreateOrder.jsx | 394 + src/pages/doormile/orders/MultipleOrders.jsx | 353 + src/pages/doormile/orders/Orders.jsx | 382 + src/pages/doormile/orders/OrdersPreview.jsx | 237 + src/pages/doormile/pricing/Pricing.jsx | 583 + src/pages/doormile/reports/OrdersDetails.jsx | 258 + src/pages/doormile/reports/OrdersSummary.jsx | 313 + src/pages/doormile/reports/Profitability.jsx | 217 + src/pages/doormile/reports/RidersSummary.jsx | 220 + src/pages/doormile/riders/CreateRider.jsx | 234 + src/pages/doormile/riders/EditRider.jsx | 191 + src/pages/doormile/riders/Riders.jsx | 449 + src/pages/doormile/tripsheets/Tripsheets.jsx | 406 + src/pages/doormile/vehicles/Vehicles.jsx | 413 + src/skills/owliver/activity-analysis.md | 99 - src/skills/owliver/analytics-insights.md | 27 - src/skills/owliver/anomaly-detection.md | 96 - src/skills/owliver/attendance-analysis.md | 105 - src/skills/owliver/candidate-analysis.md | 98 - src/skills/owliver/candidate-search.md | 29 - src/skills/owliver/create-position.md | 69 - src/skills/owliver/executive-summary.md | 81 - src/skills/owliver/forge-skill-management.md | 64 - .../owliver/hiring-activity-assistant.md | 61 - src/skills/owliver/hiring-history-analysis.md | 88 - src/skills/owliver/hiring-pulse-analysis.md | 88 - src/skills/owliver/learning-analysis.md | 80 - src/skills/owliver/operational-risk.md | 94 - src/skills/owliver/overtime-analysis.md | 98 - src/skills/owliver/staffing-risk.md | 99 - src/skills/owliver/talent-pool-analysis.md | 93 - src/skills/owliver/workforce-analytics.md | 88 - src/skills/workforce/bartending-training.md | 37 - .../workforce/customer-service-training.md | 33 - src/skills/workforce/food-safety-training.md | 31 - src/skills/workforce/leadership-training.md | 37 - src/skills/workforce/server-training.md | 37 - src/themes/astryx.js | 100 + src/themes/dt/primitives.jsx | 133 + src/themes/dt/status.jsx | 112 + src/themes/dt/tokens.js | 109 + src/utils/batchBucket.js | 4 + src/utils/doormileAxios.js | 2 + src/utils/doormileTimestamp.js | 4 + src/utils/leafletPolylineOffset.js | 154 + src/utils/logger.js | 2 + tailwind.config.js | 14 +- vite.config.js | 14 +- 409 files changed, 49950 insertions(+), 66532 deletions(-) create mode 100644 .env.example create mode 100644 fix_imports.cjs create mode 100644 patchImports.js create mode 100644 public/doormile-logo.png create mode 100644 public/doormile-mark-cropped.png create mode 100644 public/doormile-mark.png delete mode 100644 scripts/__baseline__/owliver-baseline.json delete mode 100644 scripts/owliver-baseline.mjs delete mode 100644 scripts/owliver-capture.mjs delete mode 100644 scripts/skill-check.mjs delete mode 100644 skill-examples/board-invalid-context.md delete mode 100644 skill-examples/board-page-level.md delete mode 100644 skill-examples/board-position-card.md delete mode 100644 skill-examples/board-ui-test.md delete mode 100644 skill-examples/owliver-conversation-test.md delete mode 100644 src/agents/activity-agent.md delete mode 100644 src/agents/analytics-agent.md delete mode 100644 src/agents/candidates-agent.md delete mode 100644 src/agents/control-center-agent.md delete mode 100644 src/agents/hired-history-agent.md delete mode 100644 src/agents/krow-forge-agent.md delete mode 100644 src/agents/krow-workforce-agent.md delete mode 100644 src/agents/positions-agent.md delete mode 100644 src/agents/talent-pool-agent.md delete mode 100644 src/api/aiEngine.js delete mode 100644 src/api/attendanceSeed.js delete mode 100644 src/api/base44Client.js create mode 100644 src/api/doormile/client.js create mode 100644 src/api/doormile/endpoints.js create mode 100644 src/api/doormile/index.js create mode 100644 src/api/doormile/notify.js create mode 100644 src/api/doormile/queries.js delete mode 100644 src/api/seed.js delete mode 100644 src/api/store.js create mode 100644 src/assets/images/doormile-mark.png create mode 100644 src/components/CircularLoader.jsx delete mode 100644 src/components/UserNotRegisteredError.jsx delete mode 100644 src/components/admin/PageShell.jsx delete mode 100644 src/components/admin/RoleGlyph.jsx delete mode 100644 src/components/agents/AddSkillsModal.jsx delete mode 100644 src/components/agents/AgentCanvas.jsx delete mode 100644 src/components/agents/AgentConfigure.jsx delete mode 100644 src/components/agents/AgentInsightsPanel.jsx delete mode 100644 src/components/agents/AgentTestPanel.jsx delete mode 100644 src/components/agents/icons.js delete mode 100644 src/components/ai-assistant/AgentContext.jsx delete mode 100644 src/components/ai-assistant/AgentSwitcher.jsx delete mode 100644 src/components/ai-assistant/AssistantMessage.jsx delete mode 100644 src/components/ai-assistant/AssistantPanel.jsx delete mode 100644 src/components/ai-assistant/AssistantPanelContext.jsx delete mode 100644 src/components/ai-assistant/AssistantTrigger.jsx delete mode 100644 src/components/ai-assistant/KrowAssistant.jsx delete mode 100644 src/components/ai-assistant/PageContext.jsx delete mode 100644 src/components/ai-assistant/PromptChips.jsx delete mode 100644 src/components/ai-assistant/PromptInput.jsx delete mode 100644 src/components/ai-assistant/ResizeDivider.jsx delete mode 100644 src/components/ai-assistant/ResponseBlocks.jsx delete mode 100644 src/components/ai-assistant/blocks.js delete mode 100644 src/components/ai-assistant/capabilities/admin.js delete mode 100644 src/components/ai-assistant/capabilities/employer.js delete mode 100644 src/components/ai-assistant/capabilities/workspace.js delete mode 100644 src/components/ai-assistant/contexts.js delete mode 100644 src/components/ai-assistant/dynamic.js delete mode 100644 src/components/ai-assistant/history.js delete mode 100644 src/components/ai-assistant/index.jsx delete mode 100644 src/components/ai-assistant/insights.js delete mode 100644 src/components/ai-assistant/placement.js delete mode 100644 src/components/ai-assistant/provider.js delete mode 100644 src/components/ai-assistant/routing.js delete mode 100644 src/components/ai-assistant/useAssistant.js create mode 100644 src/components/assistant/DoormileAI.css create mode 100644 src/components/assistant/DoormileAI/AIComposer.jsx create mode 100644 src/components/assistant/DoormileAI/AIFlowStep.jsx create mode 100644 src/components/assistant/DoormileAI/AIMessage.jsx create mode 100644 src/components/assistant/DoormileAI/AIPanel.jsx create mode 100644 src/components/assistant/DoormileAI/AIParts.jsx create mode 100644 src/components/assistant/DoormileAI/AIRowsStep.jsx create mode 100644 src/components/assistant/DoormileAI/AIWelcome.jsx create mode 100644 src/components/assistant/DoormileAI/index.jsx create mode 100644 src/components/assistant/DoormileAI/pageContext.jsx create mode 100644 src/components/assistant/DoormileAI/shim.jsx delete mode 100644 src/components/charts/DepartmentPerformance.jsx delete mode 100644 src/components/charts/HiringFlow.jsx delete mode 100644 src/components/charts/HiringTrendChart.jsx delete mode 100644 src/components/charts/muiChartTheme.js create mode 100644 src/components/doormile/AddressAutocomplete.jsx create mode 100644 src/components/doormile/BatchAnalysis.jsx create mode 100644 src/components/doormile/BatchEfficiency.jsx create mode 100644 src/components/doormile/ComparePanel.jsx create mode 100644 src/components/doormile/DispatchMap.jsx create mode 100644 src/components/doormile/ListToolbar.jsx create mode 100644 src/components/doormile/RiderInfoPanel.jsx create mode 100644 src/components/doormile/assistant/AssistantPanel.jsx delete mode 100644 src/components/forge/AddSkillTraining.jsx delete mode 100644 src/components/forge/ChallengeDirectory.jsx delete mode 100644 src/components/forge/ForgeHeader.jsx delete mode 100644 src/components/forge/ForgeInsight.jsx delete mode 100644 src/components/forge/ForgeProgress.jsx delete mode 100644 src/components/forge/ForgeRail.jsx delete mode 100644 src/components/forge/NextSkill.jsx delete mode 100644 src/components/forge/SkillCard.jsx delete mode 100644 src/components/forge/SkillDetail.jsx delete mode 100644 src/components/forge/SkillLibrary.jsx delete mode 100644 src/components/forge/SkillProgression.jsx delete mode 100644 src/components/forge/YourForge.jsx delete mode 100644 src/components/forge/challengeMeta.js delete mode 100644 src/components/forge/index.js delete mode 100644 src/components/krow/AIInterviewModal.jsx delete mode 100644 src/components/krow/ActivityCharts.jsx delete mode 100644 src/components/krow/ActivityLogTable.jsx delete mode 100644 src/components/krow/ActivityMetrics.jsx delete mode 100644 src/components/krow/AnalyticsMetrics.jsx delete mode 100644 src/components/krow/ApplyIdentityCard.jsx delete mode 100644 src/components/krow/CandidateCard.jsx delete mode 100644 src/components/krow/CandidateExpandedDetails.jsx delete mode 100644 src/components/krow/CertificationManager.jsx delete mode 100644 src/components/krow/HiringAnalytics.jsx delete mode 100644 src/components/krow/ImpactMetrics.jsx delete mode 100644 src/components/krow/InterviewAnalytics.jsx delete mode 100644 src/components/krow/InterviewResults.jsx delete mode 100644 src/components/krow/InviteApplicantsModal.jsx delete mode 100644 src/components/krow/Journey.jsx delete mode 100644 src/components/krow/KROWCandidateLeaderboard.jsx delete mode 100644 src/components/krow/KROWJobPostingCard.jsx delete mode 100644 src/components/krow/KrowIdPreview.jsx delete mode 100644 src/components/krow/KrowIdentityCard.jsx delete mode 100644 src/components/krow/MatchedCandidates.jsx delete mode 100644 src/components/krow/MessageCandidateModal.jsx delete mode 100644 src/components/krow/OwliverAvatar.jsx delete mode 100644 src/components/krow/OwliverChatBubble.jsx delete mode 100644 src/components/krow/PipelineFunnel.jsx delete mode 100644 src/components/krow/PositionDetails.jsx delete mode 100644 src/components/krow/RateEndorseModal.jsx delete mode 100644 src/components/krow/ReferencesEndorsements.jsx delete mode 100644 src/components/krow/RetentionMetrics.jsx delete mode 100644 src/components/krow/ScheduleInterviewModal.jsx delete mode 100644 src/components/krow/ScoreDistribution.jsx delete mode 100644 src/components/krow/SkillRequirementsField.jsx delete mode 100644 src/components/krow/SuggestedTalent.jsx delete mode 100644 src/components/krow/TalentDetailModal.jsx delete mode 100644 src/components/krow/TalentPoolCard.jsx delete mode 100644 src/components/krow/UniversityCourseCard.jsx delete mode 100644 src/components/krow/VerifiedSkills.jsx delete mode 100644 src/components/krow/WorkforceReputation.jsx delete mode 100644 src/components/krow/proving/ChallengeRunner.jsx delete mode 100644 src/components/krow/proving/HazardIdentifier.jsx delete mode 100644 src/components/krow/proving/MediaChallenge.jsx delete mode 100644 src/components/krow/proving/RoleplayChallenge.jsx delete mode 100644 src/components/krow/proving/VideoRecorder.jsx delete mode 100644 src/components/krow/talent/CareerGrowth.jsx delete mode 100644 src/components/krow/talent/CareerPassport.jsx delete mode 100644 src/components/krow/talent/CareerScoreGauge.jsx delete mode 100644 src/components/krow/talent/EliteTalentScore.jsx delete mode 100644 src/components/krow/talent/EmployerInsights.jsx delete mode 100644 src/components/krow/talent/ExperienceEndorsements.jsx delete mode 100644 src/components/krow/talent/ExperienceNetwork.jsx delete mode 100644 src/components/krow/talent/IdentityCareerCard.jsx delete mode 100644 src/components/krow/talent/IncreaseMarketValue.jsx delete mode 100644 src/components/krow/talent/MarketOffers.jsx delete mode 100644 src/components/krow/talent/TalentHero.jsx delete mode 100644 src/components/krow/talent/VerifiedExperienceList.jsx create mode 100644 src/components/nearle_components/AddressAutocomplete.jsx create mode 100644 src/components/nearle_components/CLAUDE.md create mode 100644 src/components/nearle_components/DateRangePicker.jsx create mode 100644 src/components/nearle_components/DebounceSearchBar.jsx create mode 100644 src/components/nearle_components/GlobalToast.jsx create mode 100644 src/components/nearle_components/LoaderWithImage.jsx create mode 100644 src/components/nearle_components/LocationAutocomplete.jsx create mode 100644 src/components/nearle_components/MobileCard.jsx create mode 100644 src/components/nearle_components/PageHeader.jsx create mode 100644 src/components/nearle_components/PageLayout.jsx create mode 100644 src/components/nearle_components/SegmentCard.jsx create mode 100644 src/components/nearle_components/StatCard.jsx create mode 100644 src/components/nearle_components/StatusBadge.jsx create mode 100644 src/components/nearle_components/StatusTabs.jsx create mode 100644 src/components/nearle_components/TableLoader.jsx delete mode 100644 src/components/skills/SkillSections.jsx delete mode 100644 src/components/skills/SkillSurface.jsx create mode 100644 src/components/third-party/Notistack.jsx create mode 100644 src/components/third-party/OpenToast.js create mode 100644 src/components/third-party/ReactTable.jsx delete mode 100644 src/layouts/Layout.jsx delete mode 100644 src/lib/activitySignals.js delete mode 100644 src/lib/admin/permissions.js delete mode 100644 src/lib/admin/positionInsights.js delete mode 100644 src/lib/admin/session.js delete mode 100644 src/lib/agents/agentConfig.js delete mode 100644 src/lib/agents/agentFields.js delete mode 100644 src/lib/agents/agentLifecycle.js delete mode 100644 src/lib/agents/context.js delete mode 100644 src/lib/agents/conversationInsights.js delete mode 100644 src/lib/agents/customAgents.js delete mode 100644 src/lib/agents/knowledge.js delete mode 100644 src/lib/agents/registry.js delete mode 100644 src/lib/agents/runtime.js delete mode 100644 src/lib/agents/useAgents.js delete mode 100644 src/lib/agents/vocabulary.js create mode 100644 src/lib/assistant/actions.js create mode 100644 src/lib/assistant/assignActions.js create mode 100644 src/lib/assistant/assignFlow.js create mode 100644 src/lib/assistant/bulkFile.js create mode 100644 src/lib/assistant/bulkFlow.js create mode 100644 src/lib/assistant/bulkOrderActions.js create mode 100644 src/lib/assistant/customerFlow.js create mode 100644 src/lib/assistant/flowEngine.js create mode 100644 src/lib/assistant/flows.js create mode 100644 src/lib/assistant/intents.js create mode 100644 src/lib/assistant/orderActions.js create mode 100644 src/lib/assistant/orderFlow.js create mode 100644 src/lib/assistant/pageContext.js create mode 100644 src/lib/assistant/ragRouter.js create mode 100644 src/lib/assistant/repeatFlow.js create mode 100644 src/lib/assistant/repeatRuns.js create mode 100644 src/lib/assistant/scan.js create mode 100644 src/lib/assistant/vocab.js delete mode 100644 src/lib/attendance.js create mode 100644 src/lib/batchBucket.js create mode 100644 src/lib/bulkOrderColumns.js delete mode 100644 src/lib/candidateIntelligence.js create mode 100644 src/lib/dispatchPreview.js create mode 100644 src/lib/dispatchShared.js create mode 100644 src/lib/distance.js create mode 100644 src/lib/doormileFormat.js create mode 100644 src/lib/doormileHooks.js create mode 100644 src/lib/doormileTimestamp.js delete mode 100644 src/lib/hiringRecords.js delete mode 100644 src/lib/krowAi.js delete mode 100644 src/lib/krowHooks.js delete mode 100644 src/lib/krowScore.js create mode 100644 src/lib/logger.js create mode 100644 src/lib/orderStatusGroups.js delete mode 100644 src/lib/positionModel.js create mode 100644 src/lib/preferences.js create mode 100644 src/lib/profitability.js delete mode 100644 src/lib/provingGround.js delete mode 100644 src/lib/roleCategories.js delete mode 100644 src/lib/skillGraph.js delete mode 100644 src/lib/skills/actions.js delete mode 100644 src/lib/skills/customSkills.js delete mode 100644 src/lib/skills/dataResolver.js delete mode 100644 src/lib/skills/draftFlow.js delete mode 100644 src/lib/skills/owliverConfig.js delete mode 100644 src/lib/skills/owliverResolver.js delete mode 100644 src/lib/skills/pageSkills.js delete mode 100644 src/lib/skills/positionFlow.js delete mode 100644 src/lib/skills/registry.js delete mode 100644 src/lib/skills/saveFeedback.js delete mode 100644 src/lib/skills/skillFields.js delete mode 100644 src/lib/skills/surfaces.js delete mode 100644 src/lib/skills/tools.js delete mode 100644 src/lib/skills/uiConfig.js delete mode 100644 src/lib/skills/usePageSkills.js delete mode 100644 src/lib/skills/workforceFlow.js delete mode 100644 src/lib/skills/yaml.js delete mode 100644 src/lib/talentHome.js delete mode 100644 src/lib/talentInsights.js delete mode 100644 src/lib/userTracking.js delete mode 100644 src/lib/workforce.js delete mode 100644 src/pages/Analytics.jsx delete mode 100644 src/pages/Apply.jsx delete mode 100644 src/pages/Candidates.jsx delete mode 100644 src/pages/CourseDetail.jsx delete mode 100644 src/pages/CreatePosition.jsx delete mode 100644 src/pages/DesignSystem.jsx delete mode 100644 src/pages/EmployeeDashboard.jsx delete mode 100644 src/pages/HiredHistory.jsx delete mode 100644 src/pages/KrowIdentity.jsx create mode 100644 src/pages/Login.css create mode 100644 src/pages/Login.jsx delete mode 100644 src/pages/Overview.jsx delete mode 100644 src/pages/Owliver.jsx delete mode 100644 src/pages/PositionDetail.jsx delete mode 100644 src/pages/Positions.jsx delete mode 100644 src/pages/Profile.jsx delete mode 100644 src/pages/TalentPool.jsx delete mode 100644 src/pages/University.jsx delete mode 100644 src/pages/UserTracking.jsx delete mode 100644 src/pages/WorkerProfile.jsx delete mode 100644 src/pages/admin/Activity.jsx delete mode 100644 src/pages/admin/AdminRoute.jsx delete mode 100644 src/pages/admin/AgentDetail.jsx delete mode 100644 src/pages/admin/Analytics.jsx delete mode 100644 src/pages/admin/CandidateProfile.jsx delete mode 100644 src/pages/admin/Candidates.jsx delete mode 100644 src/pages/admin/CandidatesAnalysis.jsx delete mode 100644 src/pages/admin/ControlCenter.jsx delete mode 100644 src/pages/admin/HiredHistory.jsx delete mode 100644 src/pages/admin/Login.jsx delete mode 100644 src/pages/admin/OwliverSkillEditor.jsx delete mode 100644 src/pages/admin/Positions.jsx delete mode 100644 src/pages/admin/Profile.jsx delete mode 100644 src/pages/admin/Settings.jsx delete mode 100644 src/pages/admin/SkillDevelopment.jsx delete mode 100644 src/pages/admin/SkillEditor.jsx delete mode 100644 src/pages/admin/TalentPool.jsx delete mode 100644 src/pages/admin/Workspace.jsx delete mode 100644 src/pages/admin/WorkspaceAgents.jsx delete mode 100644 src/pages/admin/WorkspaceSkills.jsx create mode 100644 src/pages/api/api.js create mode 100644 src/pages/api/doormileApi.js create mode 100644 src/pages/doormile/ViewProfile.jsx create mode 100644 src/pages/doormile/appUsers/AppUsers.jsx create mode 100644 src/pages/doormile/clients/CreateClient.jsx create mode 100644 src/pages/doormile/clients/CreateCustomer.jsx create mode 100644 src/pages/doormile/clients/Tenants.jsx create mode 100644 src/pages/doormile/competitiveIntel/CompetitiveIntel.jsx create mode 100644 src/pages/doormile/customers/Customers.jsx create mode 100644 src/pages/doormile/deliveries/Deliveries.jsx create mode 100644 src/pages/doormile/dispatch/ActiveSection.jsx create mode 100644 src/pages/doormile/dispatch/CLAUDE.md create mode 100644 src/pages/doormile/dispatch/CompareDataPanel.jsx create mode 100644 src/pages/doormile/dispatch/Dispatch.css create mode 100644 src/pages/doormile/dispatch/Dispatch.js create mode 100644 src/pages/doormile/dispatch/Dispatch.jsx create mode 100644 src/pages/doormile/dispatch/Preview.js create mode 100644 src/pages/doormile/dispatch/Preview.jsx create mode 100644 src/pages/doormile/dispatch/ProfitabilitySection.jsx create mode 100644 src/pages/doormile/dispatch/dispatchShared.js create mode 100644 src/pages/doormile/exceptions/Exceptions.jsx create mode 100644 src/pages/doormile/hubs/Hubs.jsx create mode 100644 src/pages/doormile/orders/CreateOrder.jsx create mode 100644 src/pages/doormile/orders/MultipleOrders.jsx create mode 100644 src/pages/doormile/orders/Orders.jsx create mode 100644 src/pages/doormile/orders/OrdersPreview.jsx create mode 100644 src/pages/doormile/pricing/Pricing.jsx create mode 100644 src/pages/doormile/reports/OrdersDetails.jsx create mode 100644 src/pages/doormile/reports/OrdersSummary.jsx create mode 100644 src/pages/doormile/reports/Profitability.jsx create mode 100644 src/pages/doormile/reports/RidersSummary.jsx create mode 100644 src/pages/doormile/riders/CreateRider.jsx create mode 100644 src/pages/doormile/riders/EditRider.jsx create mode 100644 src/pages/doormile/riders/Riders.jsx create mode 100644 src/pages/doormile/tripsheets/Tripsheets.jsx create mode 100644 src/pages/doormile/vehicles/Vehicles.jsx delete mode 100644 src/skills/owliver/activity-analysis.md delete mode 100644 src/skills/owliver/analytics-insights.md delete mode 100644 src/skills/owliver/anomaly-detection.md delete mode 100644 src/skills/owliver/attendance-analysis.md delete mode 100644 src/skills/owliver/candidate-analysis.md delete mode 100644 src/skills/owliver/candidate-search.md delete mode 100644 src/skills/owliver/create-position.md delete mode 100644 src/skills/owliver/executive-summary.md delete mode 100644 src/skills/owliver/forge-skill-management.md delete mode 100644 src/skills/owliver/hiring-activity-assistant.md delete mode 100644 src/skills/owliver/hiring-history-analysis.md delete mode 100644 src/skills/owliver/hiring-pulse-analysis.md delete mode 100644 src/skills/owliver/learning-analysis.md delete mode 100644 src/skills/owliver/operational-risk.md delete mode 100644 src/skills/owliver/overtime-analysis.md delete mode 100644 src/skills/owliver/staffing-risk.md delete mode 100644 src/skills/owliver/talent-pool-analysis.md delete mode 100644 src/skills/owliver/workforce-analytics.md delete mode 100644 src/skills/workforce/bartending-training.md delete mode 100644 src/skills/workforce/customer-service-training.md delete mode 100644 src/skills/workforce/food-safety-training.md delete mode 100644 src/skills/workforce/leadership-training.md delete mode 100644 src/skills/workforce/server-training.md create mode 100644 src/themes/astryx.js create mode 100644 src/themes/dt/primitives.jsx create mode 100644 src/themes/dt/status.jsx create mode 100644 src/themes/dt/tokens.js create mode 100644 src/utils/batchBucket.js create mode 100644 src/utils/doormileAxios.js create mode 100644 src/utils/doormileTimestamp.js create mode 100644 src/utils/leafletPolylineOffset.js create mode 100644 src/utils/logger.js diff --git a/.claude/launch.json b/.claude/launch.json index 798ab2a..0575c72 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -2,7 +2,7 @@ "version": "0.0.1", "configurations": [ { - "name": "krow-demo", + "name": "doormile-console", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"], "port": 5173 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..06b71c7 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# The Doormile Express admin API. +# +# Leave this unset in production — the console defaults to the real backend, +# https://api.doormile.com/api/v1, and every /admin/* call goes through it. +# Set it only to point a local build at a staging or mock backend; a build +# started with an override logs a warning saying so. +# +# VITE_DOORMILE_URL=https://api.doormile.com/api/v1 + +# OSRM routing, used to price an order by real driving distance. Falls back to +# the public demo server, which is rate-limited and not for production traffic. +VITE_OSRM_URL=https://router.project-osrm.org diff --git a/.gitignore b/.gitignore index bb8b6de..fb2c8ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ #env .env .env.* +# The template is meant to be committed — it documents the variables, never their values. +!.env.example # Logs /logs diff --git a/README.md b/README.md index 5179187..fe216c9 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ -# KROW Demo +# Doormile Express Console -AI-powered hiring for event and hospitality staffing — a standalone demo build of KROW Forge. - -Runs with no backend, no API keys and no real login. `npm install && npm run dev` and the app is fully -populated. +The operator console for the Doormile Express dispatch platform — orders, deliveries, riders, +dispatch optimisation and reporting, talking to the live `api.doormile.com` admin API. ```bash npm install -npm run dev # http://localhost:5173 — opens on /admin/login +cp .env.example .env.local # point it at the API you want +npm run dev # http://localhost:5173 — opens on /login ``` +Production users are warehouse and dispatch staff, not end customers. + ## Stack -Vite 6 · React 19 · React Router 6 · React Query 5 · Tailwind + shadcn/ui · Framer Motion · -Recharts + MUI X Charts · react-hot-toast. +Vite 6 · React 19 · React Router 6 · React Query 5 · axios · Tailwind + shadcn/ui · +Framer Motion · Leaflet · dayjs · SheetJS · react-hot-toast. ## Structure @@ -23,129 +24,165 @@ src/ App.jsx providers + the entire route table index.css design tokens (HSL CSS variables) + utility layers - api/ the backend seam — see below - base44Client.js the contract the production app talks to - store.js in-memory entity store, mirrored to localStorage - aiEngine.js local, deterministic InvokeLLM - seed.js demo dataset - attendanceSeed.js shift/attendance dataset + api/doormile/ the backend seam — see below + client.js axios instance, bearer token, 401 handling + endpoints.js one function per /admin/* route + queries.js the page-facing layer: joins, status mapping, solver calls + notify.js the data layer's toast seam + index.js the single import surface lib/ domain logic and data hooks - krowHooks.js React Query hooks over the entity store - krowAi.js AI workflows - krowScore.js score engine - AuthContext.jsx auth provider - admin/ admin session, permissions, position insights - skills/ the Owliver skill system (registry, resolver, flows) - agents/ the agent system (registry, runtime, lifecycle) - - skills/ *.md skill definitions — loaded by import.meta.glob - agents/ *.md agent definitions — loaded by import.meta.glob + doormileHooks.js React Query hooks over the API — every page reads through here + doormileFormat.js currency/number/distance formatting, search, xlsx export + doormileTimestamp.js the IST wall-clock parse every date must go through + batchBucket.js the Morning/Afternoon/Evening wave windows + orderStatusGroups.js which raw booking enums make up each Orders tab + dispatchPreview.js the optimiser preview's tree/flat-list model + profitability.js revenue, cost and margin for a delivery day + distance.js OSRM driving distance, Haversine fallback + bulkOrderColumns.js the bulk-upload sheet's column map + preferences.js per-browser display settings + AuthContext.jsx the Doormile session components/ - ui/ 23 shadcn primitives - ds/ 30 design-system components (the app's real vocabulary) - admin/ admin page shell + role glyphs - krow/ domain components (+ talent/, proving/) - charts/ MUI X Charts wrappers and theme - agents/ agent configure / canvas / test panel - forge/ KROW Forge skill + challenge components - skills/ skill authoring surfaces - ai-assistant/ Owliver — panel, routing, capabilities, history + ui/ shadcn primitives + ds/ the design system — the vocabulary pages are written in + doormile/ console-specific pieces (ListToolbar, DispatchMap) - layouts/ AdminLayout — header nav, ⌘K search, assistant panel - pages/ route components (admin/ holds the live product) - hooks/ shared hooks - assets/ brand asset references + lib/assistant/ Doormile AI — the deterministic operations copilot + intents.js the intent catalog: match → run → answer + scan.js booking scans with honest truncation reporting + vocab.js dates, statuses, batches, typo tolerance + flows.js the three conversational writes + the confirm gate + pageContext.js route → context label + suggested questions + + layouts/ AdminLayout — header nav, ⌘K search, exception notifications, AI trigger + pages/ Login + doormile/ (the console) ``` ## Routes -The app opens on **`/admin/login`**. Everything else lives under `/admin/*` behind two guards: -`ProtectedRoute` (auth) and `AdminRoute` (admin session), rendered inside `AdminLayout`. +The app opens on **`/login`**. Everything else lives under `/doormile/*` behind `ProtectedRoute`, +rendered inside `AdminLayout`. | Path | Page | | --- | --- | -| `/admin/login` | Admin sign-in — outside the protected boundary | -| `/admin` | Control Center | -| `/admin/positions`, `positions/new`, `positions/:id` | Positions, Create, Detail | -| `/admin/candidates`, `candidates/:id` | Candidates, Candidate Intelligence profile | -| `/admin/hired` · `/admin/talent-pool` | Hired History, Talent Pool | -| `/admin/university`, `university/:id` | KROW Forge, challenge detail | -| `/admin/analytics` · `/admin/activity` | Analytics, Activity | -| `/admin/profile` · `/admin/settings` | Profile, Settings | -| `/admin/workspace` | Workspace hub | -| `/admin/workspace/agents`, `agents/new`, `agents/:id` | Agent list, agent editor | -| `/admin/workspace/skills`, `skills/new`, `skills/:id` | UI skill list and editor | -| `/admin/workspace/skills/owliver/new`, `owliver/:id` | Owliver skill editor | -| `/admin/workspace/skill-development` | Skill Development | +| `/login` | Sign-in — outside the protected boundary | +| `/doormile/dispatch` | The dispatch board — live map, rider detail, wave economics | +| `/doormile/dispatch/preview` | The optimiser's plan — reconcile, then commit | +| `/doormile/orders`, `orders/create`, `orders/createorders`, `orders/preview` | Orders, create one, bulk upload, assignment preview | +| `/doormile/deliveries` | Deliveries | +| `/doormile/riders`, `riders/create`, `riders/edit` | Riders, create, edit | +| `/doormile/tenants` · `clients/create` | Clients, create | +| `/doormile/customers` · `customer/create` | Customers, create | +| `/doormile/pricing` | Per-client rate cards + quote simulator | +| `/doormile/hubs` · `vehicles` · `tripsheets` · `exceptions` · `competitive-intel` · `app-users` | Fleet Ops | +| `/doormile/reports/orderssummary`, `ordersdetails`, `riderssummary`, `profitability` | Reports | +| `/doormile/profile` | Account and password | | `*` | `lib/PageNotFound.jsx` | -Legacy top-level paths (`/overview`, `/positions`, `/candidates`, `/hired`, `/talent-pool`, -`/university`, `/analytics`, `/activity`, `/tracking`) all `Navigate` into their `/admin/*` -equivalent, id preserved. Static route segments are declared before dynamic ones so -`workspace/agents/new` cannot be read as an agent whose id is `"new"`. - -> **Note.** `App.jsx` still imports a set of pre-redesign Employer/Talent pages -> (`Overview`, `Apply`, `WorkerProfile`, `KrowIdentity`, `EmployeeDashboard`, `DesignSystem`, …) -> and `layouts/Layout.jsx`. None of them are mounted on a route — they are unreachable at runtime -> and retained pending a decision on whether to re-route or remove them. +Static route segments are declared before dynamic ones, so `orders/create` cannot be read as an +order whose id is `"create"`. ## The backend seam -The production app talks to a Base44 backend through `src/api/base44Client.js`. This demo keeps that -module path, export name and full method contract, and swaps only the transport: +Everything goes through `src/api/doormile/`, which talks to `api.doormile.com/api/v1/admin/*`. -| Contract | Demo implementation | +| File | Role | | --- | --- | -| `base44.entities..list/filter/get/create/update/delete` | `api/store.js` — in-memory records, mirrored to `localStorage` | -| `base44.integrations.Core.InvokeLLM` | `api/aiEngine.js` — local, deterministic | -| `base44.integrations.Core.UploadFile` | blob URLs, session-scoped | -| `base44.auth.*` | seeded employer/admin user, always signed in | +| `client.js` | One axios instance. Attaches `Bearer localStorage.doormileToken` to every request; a 401 clears the session and returns to `/login`. Rejects with the server's own body, keeping the status on `err.httpStatus`. | +| `endpoints.js` | One named export per route. Thin: build the URL, call, unwrap the envelope. GET/list return `response.data.data`; mutations return the whole `{ success, data \| message }`. | +| `queries.js` | The page-facing layer — the joins, the status mapping, the AI-solver calls, and safe empty defaults for the resources this API has no equivalent for. | -Because the seam did not move, everything above it — `lib/krowHooks.js`, `lib/krowAi.js`, -`lib/krowScore.js`, every page and component — is unchanged from the reference implementation. +Pages do not import these directly. They use the React Query hooks in `lib/doormileHooks.js`, so a +resource has exactly one cache key across the app and a write on one page invalidates the list +another page is showing. -## The AI engine +### What this API cannot do -Every AI workflow funnels through one call, `InvokeLLM({ prompt, response_json_schema })`. -`api/aiEngine.js` reimplements it locally: it recognises each workflow by the phrase its prompt opens -with, reads the structured fields the prompt already carries (`Years Experience: 6`, -`Required Certifications: …`, the talent-pool JSON block) and scores them deterministically. +Several things the previous backend served have no equivalent here. The functions covering them +return safe empty defaults rather than throwing, so a page renders an empty state instead of +crashing: -Nine workflows are implemented: candidate screening, job-description generation, résumé building, -interview questions, interview evaluation, talent matching, Owliver's conversation, Career DNA -building and Proving Ground evaluation. Results respond to the data rather than being canned — the -same input always returns the same output. +- **Zones** are not a resource. `applocationid` exists only as a field on a hub, so every zone + picker is derived from the distinct cities across `GET /admin/hubs`. +- **Rider substitution** has no endpoint at all, so the console has no substitution surface. +- **Invoices and expense approvals** have no endpoint. +- **Payment-mode types, item subcategories, rider shifts and vehicle types** are not enumerable. -## Skills and agents +The AI dispatch optimiser (`routes.workolik.com` / `routemate.workolik.com`) is a separate solver +service with no equivalent in the admin API, and is called directly. -Two registries discover their definitions from markdown at build time: +## Doormile AI -| Registry | Glob | Files | -| --- | --- | --- | -| `lib/skills/registry.js` | `import.meta.glob('/src/skills/**/*.md')` | 18 Owliver skills, 5 workforce training skills | -| `lib/agents/registry.js` | `import.meta.glob('/src/agents/**/*.md')` | 9 agent definitions | +An in-console copilot, opened from the header — not a route. It answers questions about live data +by calling **the same endpoints the pages call**, so its numbers and the screen's cannot disagree. -**These globs are absolute paths.** Moving or renaming `src/skills/` or `src/agents/` makes the -registry silently return nothing — no build error, no import failure, just an empty registry. Add -definitions by dropping a new `.md` file into the right folder; nothing else needs to change. +**It is deterministic, not an LLM.** This app has no backend of its own, so there is nowhere to hold +a model key that would not ship to the browser. The trade was taken deliberately: a rich matcher +over typed API calls, rather than a generated answer nobody can verify. -## Demo data +- **Never fabricate a number.** A question no API function covers is left unanswered. A wrong number + from this bot is worse than no answer. +- **Every answer carries its sources** behind a disclosure — which endpoint ran, and what came back. + That is the verifiability contract; do not remove it. +- **A truncated scan is a floor, not a total.** `/admin/bookings` caps `pagesize` at 1000, so scans + drain pages up to a budget and report `truncated`. Counts built on one say "at least", and the + source entry reports an error rather than a green tick. +- **Writes are gated.** The three creates — customer, order, bulk — are conversations, one question + per turn. The bot gathers, shows exactly what will be sent, and the mutation fires only when the + operator presses the button. `executeFlow` is the only mutating function and no `match` calls it. +- **A flow reply never reaches the router.** The panel intercepts it first: the router matches text, + and a bare answer like a phone number matches no intent. +- **Ordering is load-bearing.** Domain intents (rider, tenant, fleet) sit ahead of the generic + order/status ones, and the generic ones additionally guard against another domain's keywords. A + bug shipped once where "how many riders are active" was swallowed by the order-status intent, + because "active" is a valid order status. -Seeded in `src/api/seed.js` — positions, applicants, AI-scored candidates, interviews, hires, -talent-pool profiles, Proving Ground courses and an activity log; shift data in -`src/api/attendanceSeed.js`. +## Dispatch is a monitoring board -Edits persist to `localStorage`. To restore the shipped dataset: +It reads the deliveries feed, which by construction returns only bookings that **already carry a +rider or a consignment** — so an unassigned order cannot appear on it. Assignment therefore lives on +Orders, which reads raw bookings and can see the pending pile. Running the optimiser from Dispatch +would re-send work that is already assigned; the source console never did either. -```js -// in the browser console -localStorage.removeItem('krow_demo_db'); location.reload(); -``` +Selecting a rider opens their day: live position with a reverse-geocoded area and ping history, the +plan against their GPS trail, and what the day earned. Per-stop actual distance is apportioned from +the trail by each stop's share of the plan — the feed carries no per-stop odometer, so it is an +estimate and the panel says so. -Bumping `STORAGE_VERSION` in `src/api/store.js` (currently `8`) invalidates saved snapshots -automatically, so changing the seed does not require a manual clear. +## Things that look wrong but are not + +- **A delivery can read "Active" while the booking says `Converted_To_Consignment`.** A booking's + status freezes the moment it is picked up; the lifecycle continues on the consignment. The + Deliveries table lets the consignment's status win and labels the row so you can tell where the + value came from. +- **The Picked tab is empty on hyperlocal traffic.** A matching 3-digit pickup/delivery pincode + prefix goes straight to `Out_for_Delivery` at pickup, so those parcels never rest in "picked". +- **Update Status refuses half its own options.** The write is consignment-scoped, and Pending, + Accepted, Arrived, Picked and Skipped describe states a consignment cannot be in. Those are + refused with the reason rather than guessed at. +- **Assigned means different things on Orders and Deliveries.** Orders tracks the *operator's* + workflow, so assigning a rider counts as Assigned. Deliveries tracks the *rider's* engagement, so + it waits for the rider to accept. This is deliberate, and the two taxonomies live in + `orderStatusGroups.js` and `queries.js` respectively. + +## Batches, dates and the one rule about time + +Waves bucket on a booking's **creation time** (`orderdate`), never on `assigntime` — which is mapped +to the backend's last-modified column and is re-stamped by any status change — and never on +`expecteddeliverytime`, which is the promised slot rather than the wave the order was placed in. + +`GET /admin/bookings` takes no date parameter, so the range is applied client-side on that same +field. **Filtering on one timestamp while bucketing on another cannot produce an honest count**, and +both have been tried. + +The windows in `lib/batchBucket.js` cover the whole clock, and that is load-bearing: earlier windows +left 40% of the day in a gap where an order belonged to no wave and vanished from every filter. + +Every timestamp goes through `parseDoormileTimestamp`. Doormile stores IST wall-clock in naive +columns and some responses carry a false trailing `Z`; parsing that as UTC shifts a row by five and +a half hours, and therefore into the wrong day and the wrong wave. ## Design system @@ -153,56 +190,40 @@ Tokens live in `src/index.css` as HSL CSS variables consumed by `tailwind.config | Token | Value | | --- | --- | -| Primary — KROW Blue | `#0A39DF` | -| Accent — KROW Yellow | `#F9E547` | -| Pale Yellow | `#F8E08E` | +| Primary — Doormile red | `#C8102E` | +| Accent | `#F9E547` | | Dark Navy | `#333F48` | -| Mint | `#D1E0D7` | | Radius | `0.75rem` | | Headings | Sora | | Body | Inter | -`src/components/ds/` is the vocabulary the app actually writes in — `Surface`, `KpiCard`, -`DataTable`, `PageHeader`, `Modal`, `toast` and the rest, exported through `ds/index.js`. -`src/components/ui/` holds the shadcn primitives those are built on. Surfaces use `glass` and -`glass-card`; the page background is the `gradient-mesh` utility. +`src/components/ds/` is the vocabulary the app writes in — `Surface`, `KpiCard`, `DataTable`, +`PageHeader`, `Modal`, `StatusBadge`, `toast` and the rest, exported through `ds/index.js`. +`src/components/ui/` holds the shadcn primitives those are built on. -Toasts go through `ds/toast` (a wrapper over `react-hot-toast`), rendered by `` in -`App.jsx`. +`StatusBadge`'s `STATUS_MAP` is the only place a lifecycle status maps to a colour and a label. +Statuses are looked up case-insensitively, and an unmapped value renders as a neutral badge showing +the humanised raw string — so a missing entry is visible rather than silent. Add states there, never +in a page-local table. + +## Environment + +| Variable | Purpose | +| --- | --- | +| `VITE_DOORMILE_URL` | The admin API. Defaults to `https://api.doormile.com/api/v1`. | +| `VITE_OSRM_URL` | Routing for order pricing. Defaults to the public OSRM demo server, which is rate-limited and not for production. | ## Scripts | Command | Purpose | | --- | --- | -| `npm run dev` | Dev server (honors `PORT`, default 5173) | +| `npm run dev` | Dev server (honours `PORT`, default 5173) | | `npm run build` | Production build | | `npm run preview` | Serve the build | | `npm run lint` | ESLint — currently clean | | `npm run lint:fix` | ESLint with `--fix` | -| `npm test` | `scripts/skill-check.mjs` — 835 assertions over the skill and agent systems | | `npm run typecheck` | `tsc -p ./jsconfig.json` with `checkJs` | -### The test harness - -`npm test` boots a real Vite dev server in middleware mode and `ssrLoadModule`s the actual -application modules, so it exercises the real `@/` alias and `import.meta.glob` pipeline rather than -mocks. It also diffs Owliver's behaviour across eleven page contexts against a committed snapshot, -`scripts/__baseline__/owliver-baseline.json`; regenerate that deliberately with -`node scripts/owliver-baseline.mjs --write`, never to turn a red check green. - -Because it loads modules by absolute path, the harness pins the locations of `src/api/`, `src/lib/`, -`src/lib/skills/`, `src/lib/agents/`, `src/components/ai-assistant/`, `src/App.jsx`, -`src/layouts/AdminLayout.jsx` and a handful of agent files. Move any of them and update -`scripts/owliver-capture.mjs` and `scripts/skill-check.mjs` in the same change. - -### Known-failing checks - -Two checks fail on a clean checkout and are tracked as separate work, not regressions: - -- `npm test` — 834/835 pass; `the seeded overtime climb is found` fails. -- `npm run typecheck` — 59 errors, all pre-existing JSDoc/inference gaps in `lib/krowAi.js`, - `lib/skills/*`, `lib/positionModel.js` and a few components. - ## Deployment `Dockerfile` builds with `node:22-alpine` (capped at 1 GB heap for small hosts) and serves the diff --git a/fix_imports.cjs b/fix_imports.cjs new file mode 100644 index 0000000..bbc844c --- /dev/null +++ b/fix_imports.cjs @@ -0,0 +1,31 @@ +const fs = require('fs'); +const path = require('path'); +const dir = 'src/lib/assistant'; +fs.readdirSync(dir).filter(f => f.endsWith('.js')).forEach(f => { + const p = path.join(dir, f); + let content = fs.readFileSync(p, 'utf8'); + let changed = false; + content = content.replace(/from\s+['"]utils\/([^'"]+)['"]/g, (m, p1) => { + changed = true; + return `from '@/lib/${p1}'`; + }); + content = content.replace(/from\s+['"]pages\/([^'"]+)['"]/g, (m, p1) => { + changed = true; + return `from '@/${p1}'`; + }); + content = content.replace(/from\s+['"]components\/nearle_components\/([^'"]+)['"]/g, (m, p1) => { + changed = true; + return `from '@/components/doormile/${p1}'`; + }); + content = content.replace(/from\s+['"]components\/([^'"]+)['"]/g, (m, p1) => { + changed = true; + return `from '@/components/${p1}'`; + }); + content = content.replace(/from\s+['"]hooks\/([^'"]+)['"]/g, (m, p1) => { + changed = true; + return `from '@/hooks/${p1}'`; + }); + if (changed) { + fs.writeFileSync(p, content); + } +}); diff --git a/index.html b/index.html index 5caebd7..c2f6106 100644 --- a/index.html +++ b/index.html @@ -2,13 +2,15 @@ - + + + - KROW Forge — AI-Powered Hiring - + Doormile Express Console +
diff --git a/package-lock.json b/package-lock.json index 87fab90..9186748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,15 @@ { - "name": "krow-demo", + "name": "doormile-console", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "krow-demo", + "name": "doormile-console", "version": "0.0.0", "dependencies": { - "@astryxdesign/core": "^0.3.0", - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/material": "^9.3.1", - "@mui/x-charts": "^9.11.1", + "@astryxdesign/core": "^0.4.7", + "@astryxdesign/theme-neutral": "^0.4.7", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", @@ -27,19 +24,26 @@ "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", "@tanstack/react-query": "^5.84.1", + "axios": "^1.19.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.23", "framer-motion": "^11.16.4", - "html2canvas": "^1.4.1", + "leaflet": "^1.9.4", "lucide-react": "^0.475.0", + "papaparse": "^5.6.0", + "prop-types": "^15.8.1", "react": "^19.2.8", + "react-csv": "^2.2.2", "react-dom": "^19.2.8", "react-hot-toast": "^2.6.0", - "react-markdown": "^9.0.1", + "react-icons": "^5.7.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^6.26.0", "recharts": "^3.10.1", "tailwind-merge": "^3.0.2", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^9.19.0", @@ -72,9 +76,9 @@ } }, "node_modules/@astryxdesign/core": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.3.0.tgz", - "integrity": "sha512-t2hHvR7AcwW0oKrnxCwbdEEE5Ytgy0Ug61DcYAl9BxAuV5z4XL0/kMo5DiPvX9jbDxzysmOa8ClnXqzxouRUVQ==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.4.7.tgz", + "integrity": "sha512-vlPnZFtyqip8R6t7iJk5W6TOB+ssd72jqHVW1g651dx+OwOqADcBZQo2TfDypxeMSU7roRIAMeV/01f+yXpbvQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -86,10 +90,33 @@ "react-dom": ">=19.0.0" } }, + "node_modules/@astryxdesign/theme-neutral": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-neutral/-/theme-neutral-0.4.7.tgz", + "integrity": "sha512-mGxR7ck/1AeA4YkO+sYr+FogxhgmBtPGlEuSyOr2o/1M0X0YMQYjV4ND2ePLBak3JNEyH2/ETMXCSsK6bSjRGA==", + "license": "MIT", + "dependencies": { + "lucide-react": "^1.18.0" + }, + "peerDependencies": { + "@astryxdesign/core": "0.4.7", + "react": ">=19" + } + }, + "node_modules/@astryxdesign/theme-neutral/node_modules/lucide-react": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.33.0.tgz", + "integrity": "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -145,6 +172,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.8", @@ -178,6 +206,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -187,6 +216,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -228,6 +258,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -237,6 +268,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -270,6 +302,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.8" @@ -313,19 +346,11 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -340,6 +365,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -358,6 +384,7 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -367,180 +394,6 @@ "node": ">=6.9.0" } }, - "node_modules/@base-ui/utils": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz", - "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@floating-ui/utils": "^0.2.12", - "reselect": "^5.2.0", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "@types/react": "^17 || ^18 || ^19", - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1185,9 +1038,9 @@ "license": "MIT" }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.16.tgz", - "integrity": "sha512-kl6b/4D56gjGZi4ZewSmvXbalHwjOUI5ogEHPZqw42goeXTTrL7/yuPzvdrvr0QigDtvaOeb+UeMf62jks43Yg==", + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.17.tgz", + "integrity": "sha512-cN9jhVqT7u0K9tix43fhjoUwL0nazyW6zsNIXs2QdPADr+nurPfYyssUiMqcSCGlPcCiqnYVxgSn7zBSuI+5Bg==", "license": "MIT", "dependencies": { "@formatjs/icu-skeleton-parser": "2.1.11" @@ -1311,328 +1164,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mui/core-downloads-tracker": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.3.1.tgz", - "integrity": "sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/material": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.3.1.tgz", - "integrity": "sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@mui/core-downloads-tracker": "^9.3.1", - "@mui/system": "^9.3.0", - "@mui/types": "^9.3.0", - "@mui/utils": "^9.3.0", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.2.3", - "prop-types": "^15.8.1", - "react-is": "^19.2.8", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^9.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/react-is": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "license": "MIT" - }, - "node_modules/@mui/private-theming": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.3.0.tgz", - "integrity": "sha512-ERvqk5pejf9aRnQcDILSWGtFmsEMiVxlQ4+xsVCjsEvmK0fV9BiVP/cQwAF5dwyDFbve4lTrlcgeFEecVzTNiA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@mui/utils": "^9.3.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.3.0.tgz", - "integrity": "sha512-x9+KYxhjoHYZ4nioxdKnvQWdw0RScbhoZfQ4tv3Db742683U3wlYSp6zV6Us78dMFnKnyTrPTkQKyutp14gnKA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.3.0.tgz", - "integrity": "sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@mui/private-theming": "^9.3.0", - "@mui/styled-engine": "^9.3.0", - "@mui/types": "^9.3.0", - "@mui/utils": "^9.3.0", - "clsx": "^2.1.1", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.3.0.tgz", - "integrity": "sha512-2JSxyfpEFWNUB2vKs/T1BvkfyNisMHWph8bLMj8T0uHwmLl/0qfAwQkfwMT6kxLXN9uIum9AEbECXU8er3amIg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@mui/types": "^9.3.0", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.8" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils/node_modules/react-is": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "license": "MIT" - }, - "node_modules/@mui/x-charts": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-9.11.1.tgz", - "integrity": "sha512-5Z6fzVm0NvD4auoyGZopp1wwg1mMVDXjbPEi4jahdxn14EzIAB99kKw4NUkKtmSaZJ6DiBU7zJXQAec1IZTaLA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@mui/utils": "^9.3.0", - "@mui/x-charts-vendor": "^9.11.1", - "@mui/x-internal-gestures": "^9.10.0", - "@mui/x-internals": "^9.11.0", - "bezier-easing": "^3.0.1", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "reselect": "^5.2.0", - "use-sync-external-store": "^1.6.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@mui/material": "^7.3.0 || ^9.0.0", - "@mui/system": "^7.3.0 || ^9.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/x-charts-vendor": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-9.11.1.tgz", - "integrity": "sha512-Q7cuPaKZLLsyvtEiP+ENG7xLlPatXyNQ7u5WkbAcAIZwyMK8MJw4MPgJiyWyfy9eVQO/IkRLIupabvu8cA2jPA==", - "license": "MIT AND ISC", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@types/d3-array": "^3.2.2", - "@types/d3-color": "^3.1.3", - "@types/d3-format": "^3.0.4", - "@types/d3-geo": "^3.1.0", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-path": "^3.1.1", - "@types/d3-scale": "^4.0.9", - "@types/d3-shape": "^3.1.8", - "@types/d3-time": "^3.0.4", - "@types/d3-time-format": "^4.0.3", - "@types/d3-timer": "^3.0.2", - "d3-array": "^3.2.4", - "d3-color": "^3.1.0", - "d3-format": "^3.1.2", - "d3-geo": "^3.1.1", - "d3-interpolate": "^3.0.1", - "d3-path": "^3.1.0", - "d3-scale": "^4.0.2", - "d3-shape": "^3.2.0", - "d3-time": "^3.1.0", - "d3-time-format": "^4.1.0", - "d3-timer": "^3.0.1", - "flatqueue": "^3.1.0", - "internmap": "^2.0.3" - } - }, - "node_modules/@mui/x-internal-gestures": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-9.10.0.tgz", - "integrity": "sha512-Fgmh+vJlOjLst238/J5SlEOZLdnSe0yWjrYctYVWJr3pKbtmYSbDkjYvWjG97PeDvq5Wa1+W1qagiBoJgkUIng==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@base-ui/utils": "^0.3.1" - } - }, - "node_modules/@mui/x-internals": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.11.0.tgz", - "integrity": "sha512-JjKe9k1+gVWNPwMTZLNSc92eVmoZqP5Xq3Ui6mJkstotUrbWIZC0o9+4AfTR1lYqWwnKmqm+VtLLVCa7MEBXWQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.7", - "@base-ui/utils": "^0.3.1", - "@mui/utils": "^9.3.0", - "reselect": "^5.2.0", - "use-sync-external-store": "^1.6.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -1685,16 +1216,6 @@ "node": ">= 8" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@radix-ui/number": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", @@ -2629,6 +2150,17 @@ "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", "license": "MIT" }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", @@ -3134,21 +2666,6 @@ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", @@ -3188,57 +2705,19 @@ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, "license": "MIT" }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", - "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3246,21 +2725,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -3271,22 +2735,11 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "license": "MIT" - }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3302,33 +2755,12 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", - "license": "ISC" - }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -3373,6 +2805,27 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -3598,6 +3051,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.5.4", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", @@ -3651,50 +3110,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/babel-plugin-macros/node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { @@ -3704,15 +3129,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.11.12", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", @@ -3726,12 +3142,6 @@ "node": ">=6.0.0" } }, - "node_modules/bezier-easing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-3.0.1.tgz", - "integrity": "sha512-vvvg10Anq7py0+KkIqlwJpUVdde9Z8kSzeUzAkQNYAJdA3mZnLG9r9ExdwtjcASCYER6UkNHdBNHHMnVimnU7A==", - "license": "MIT" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3824,7 +3234,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3855,6 +3264,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3890,14 +3300,17 @@ ], "license": "CC-BY-4.0" }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" } }, "node_modules/chalk": { @@ -3917,46 +3330,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4014,6 +3387,15 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4034,14 +3416,16 @@ "dev": true, "license": "MIT" }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/commander": { @@ -4067,29 +3451,16 @@ "dev": true, "license": "MIT" }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" }, "engines": { - "node": ">=10" - } - }, - "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" + "node": ">=0.8" } }, "node_modules/cross-spawn": { @@ -4107,15 +3478,6 @@ "node": ">= 8" } }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/css-mediaquery": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", @@ -4180,18 +3542,6 @@ "node": ">=12" } }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -4328,6 +3678,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4351,19 +3707,6 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4407,13 +3750,13 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.4.0" } }, "node_modules/detect-node-es": { @@ -4422,19 +3765,6 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4460,21 +3790,10 @@ "node": ">=0.10.0" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4492,15 +3811,6 @@ "dev": true, "license": "ISC" }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -4593,7 +3903,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4640,7 +3949,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4653,7 +3961,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4766,6 +4073,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4980,16 +4288,6 @@ "node": ">=4.0" } }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5006,12 +4304,6 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5095,12 +4387,6 @@ "node": ">=8" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5132,12 +4418,6 @@ "node": ">=16" } }, - "node_modules/flatqueue": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-3.1.0.tgz", - "integrity": "sha512-Ia4qIYrrsEqIRx3c3XhkT+QDLQuUV5ovsr6ah1rIgKT5wclhoGK3lAMS1bWRAWxlx7wtlTBpV7QXB5d9fOSRxA==", - "license": "ISC" - }, "node_modules/flatted": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", @@ -5145,6 +4425,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5161,6 +4461,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -5283,7 +4608,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5317,7 +4641,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5400,7 +4723,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5465,7 +4787,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5478,7 +4799,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5502,76 +4822,17 @@ "node": ">= 0.4" } }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "license": "MIT", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" + "agent-base": "6", + "debug": "4" }, "engines": { - "node": ">=8.0.0" + "node": ">= 6" } }, "node_modules/ignore": { @@ -5598,6 +4859,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -5620,12 +4882,6 @@ "node": ">=0.8.19" } }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", - "license": "MIT" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5651,13 +4907,13 @@ } }, "node_modules/intl-messageformat": { - "version": "11.2.13", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.13.tgz", - "integrity": "sha512-JaPaE6TIX+TAS5XLhDUh41geLw4QfBHX4s5pW8Km+L9fVC8HzB9yOuhbh4EMR/F1+8C6b9qk4763Cv+LdOG1kg==", + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.14.tgz", + "integrity": "sha512-9f2VD1HFuxUvMw0RxsaP8WmMns6JRTnsNB/zghTFrp11ZktiXWwVDeZBPQchBKEmo+Gx/ZhxI7Qht7YglFD4PA==", "license": "BSD-3-Clause", "dependencies": { "@formatjs/fast-memoize": "3.1.7", - "@formatjs/icu-messageformat-parser": "3.5.16" + "@formatjs/icu-messageformat-parser": "3.5.17" } }, "node_modules/invariant": { @@ -5670,30 +4926,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5712,12 +4944,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -5846,16 +5072,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-document.all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", @@ -5929,16 +5145,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -5991,18 +5197,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6222,6 +5416,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -6237,12 +5432,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6296,6 +5485,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6351,16 +5546,6 @@ "dev": true, "license": "MIT" }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6396,165 +5581,11 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6564,448 +5595,6 @@ "node": ">= 8" } }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7019,6 +5608,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -7312,10 +5922,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/papaparse": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.6.0.tgz", + "integrity": "sha512-N2vuNQAYGK1/4vs6HJX86+VYU6OkiSTgdJz3JQfTk1y51cFCO/U8gnaeTF4iNE4r57Tt0sV47dUua1/19pxO6Q==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -7324,49 +5941,6 @@ "node": ">=6" } }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7393,15 +5967,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7646,14 +6211,13 @@ "react-is": "^16.13.1" } }, - "node_modules/property-information": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", - "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=10" } }, "node_modules/punycode": { @@ -7695,6 +6259,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-csv": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", + "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==", + "license": "MIT" + }, "node_modules/react-dom": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", @@ -7724,37 +6294,33 @@ "react-dom": ">=16" } }, + "node_modules/react-icons": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", - "license": "MIT", + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "@react-leaflet/core": "^3.0.0" }, "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" } }, "node_modules/react-redux": { @@ -7891,22 +6457,6 @@ } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -8017,39 +6567,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/reselect": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", @@ -8084,6 +6601,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8387,15 +6905,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8405,14 +6914,16 @@ "node": ">=0.10.0" } }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" } }, "node_modules/stop-iteration-iterator": { @@ -8528,20 +7039,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8555,24 +7052,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, "node_modules/styleq": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.2.1.tgz", @@ -8580,12 +7059,6 @@ "license": "MIT", "peer": true }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", - "license": "MIT" - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -8710,15 +7183,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -8803,26 +7267,6 @@ "node": ">=8.0" } }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -8966,93 +7410,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/update-browserslist-db": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", @@ -9152,43 +7509,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "license": "MIT", - "dependencies": { - "base64-arraybuffer": "^1.0.2" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -9422,6 +7742,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -9432,6 +7770,27 @@ "node": ">=0.10.0" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -9439,23 +7798,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -9468,16 +7810,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } } } } diff --git a/package.json b/package.json index 64f5f36..f8f5635 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "krow-demo", + "name": "doormile-console", "private": true, "version": "0.0.0", "type": "module", @@ -8,16 +8,12 @@ "build": "vite build", "lint": "eslint . --quiet", "lint:fix": "eslint . --fix", - "test": "node scripts/skill-check.mjs", "typecheck": "tsc -p ./jsconfig.json", "preview": "vite preview" }, "dependencies": { - "@astryxdesign/core": "^0.3.0", - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/material": "^9.3.1", - "@mui/x-charts": "^9.11.1", + "@astryxdesign/core": "^0.4.7", + "@astryxdesign/theme-neutral": "^0.4.7", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", @@ -32,19 +28,26 @@ "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", "@tanstack/react-query": "^5.84.1", + "axios": "^1.19.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.23", "framer-motion": "^11.16.4", - "html2canvas": "^1.4.1", + "leaflet": "^1.9.4", "lucide-react": "^0.475.0", + "papaparse": "^5.6.0", + "prop-types": "^15.8.1", "react": "^19.2.8", + "react-csv": "^2.2.2", "react-dom": "^19.2.8", "react-hot-toast": "^2.6.0", - "react-markdown": "^9.0.1", + "react-icons": "^5.7.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^6.26.0", "recharts": "^3.10.1", "tailwind-merge": "^3.0.2", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^9.19.0", diff --git a/patchImports.js b/patchImports.js new file mode 100644 index 0000000..87dba56 --- /dev/null +++ b/patchImports.js @@ -0,0 +1,31 @@ +import fs from 'fs'; +import path from 'path'; + +const dir = path.join(process.cwd(), 'src/components/assistant/DoormileAI'); +const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsx') && f !== 'shim.jsx'); + +for (const file of files) { + const filepath = path.join(dir, file); + let content = fs.readFileSync(filepath, 'utf8'); + + // Replace @astryxdesign/core imports with shim + content = content.replace(/import\s+{\s*([^}]+)\s*}\s+from\s+'@astryxdesign\/core[^']*';/g, "import { $1 } from './shim';"); + + // Fix intents, flows, etc imports to point to src/lib/assistant/ + // The original were: import { something } from '../intents'; + // Now it should be: import { something } from '@/lib/assistant/intents'; + content = content.replace(/from\s+'\.\.\/(intents|actions|assignActions|bulkOrderActions|customerFlow|orderFlow|bulkFlow|assignFlow|repeatFlow|repeatRuns)'/g, "from '@/lib/assistant/$1'"); + + // Replace OpenToast with toast + content = content.replace(/import\s+{\s*OpenToast\s*}\s*from\s*'components\/third-party\/OpenToast';/, "import { toast } from '@/components/ui/use-toast';\nconst OpenToast = ({message, tone}) => toast({description: message, variant: tone === 'error' ? 'destructive' : 'default'});"); + + // STATUS is from themes/dt/tokens + content = content.replace(/import\s+{\s*STATUS\s*}\s*from\s*'themes\/dt\/tokens';/, "const STATUS = { error: 'error', success: 'success', warning: 'warning', info: 'info', muted: 'muted' };"); + + // A few icons from @ant-design/icons to lucide-react + content = content.replace(/import\s+{\s*CloseOutlined,\s*MoreOutlined\s*}\s*from\s*'@ant-design\/icons';/, "import { X as CloseOutlined, MoreHorizontal as MoreOutlined } from 'lucide-react';"); + content = content.replace(/import\s+{\s*([A-Za-z]+Outlined)\s*}\s*from\s*'@ant-design\/icons';/g, "import { $1 } from 'lucide-react';"); // close enough for now + + fs.writeFileSync(filepath, content, 'utf8'); +} +console.log('Patch complete'); diff --git a/public/doormile-logo.png b/public/doormile-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1cbe324359d358f60159f03d6dcd1ed4531b38b4 GIT binary patch literal 13651 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 literal 0 HcmV?d00001 diff --git a/public/doormile-mark-cropped.png b/public/doormile-mark-cropped.png new file mode 100644 index 0000000000000000000000000000000000000000..fc80e35611ad8f6aa1ad13bae0fefd4f7270af15 GIT binary patch literal 70275 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%0A@sCVj9Q15thrCmiN#Ki7v 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ℑLH^vV6!ltU%*7=%^zH&!eaFX1 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 V!zj{wQHMGNsHWkAiu;al{txy&3PS(@ literal 0 HcmV?d00001 diff --git a/public/favicon.png b/public/favicon.png index 4aad6c5f3d00d7dfadc9d342d4ddde688ac8e928..fc80e35611ad8f6aa1ad13bae0fefd4f7270af15 100644 GIT binary patch literal 70275 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 diff --git a/scripts/__baseline__/owliver-baseline.json b/scripts/__baseline__/owliver-baseline.json deleted file mode 100644 index add7667..0000000 --- a/scripts/__baseline__/owliver-baseline.json +++ /dev/null @@ -1,1189 +0,0 @@ -{ - "schema": 1, - "today": "2026-08-20T09:00:00.000Z", - "skillIds": [ - "analytics-insights", - "bartending-training", - "candidate-search", - "create-position", - "customer-service-training", - "food-safety-training", - "forge-skill-management", - "hiring-activity-assistant", - "leadership-training", - "server-training" - ], - "diagnostics": [], - "routes": { - "/admin": "admin.controlCenter", - "/admin/activity": "admin.activity", - "/admin/analytics": "admin.analytics", - "/admin/candidates": "admin.candidatesList", - "/admin/candidates-analysis": "admin.candidates", - "/admin/hired": "admin.hiredHistory", - "/admin/positions": "admin.positions", - "/admin/positions/new": "admin.createPosition", - "/admin/profile": "admin.profile", - "/admin/talent-pool": "admin.talentPool", - "/admin/university": "admin.forge" - }, - "contexts": { - "admin.controlCenter": { - "pageKey": "control-center", - "route": "/admin", - "skills": [], - "suggestions": [], - "prompts": [ - "What needs attention?", - "Platform health", - "Bottleneck at Hired", - "Summarize platform usage", - "Hiring operations", - "Identify operational risks" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.positions": { - "pageKey": "positions", - "route": "/admin/positions", - "skills": [ - "create-position", - "hiring-activity-assistant" - ], - "suggestions": [ - "Show hiring activity as a flow", - "Summarize hiring activity for this position" - ], - "prompts": [ - "What needs attention?", - "Which position has the strongest pipeline?", - "Show the 10 applications waiting for review", - "Summarize hiring activity across all positions" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Show hiring activity", - "matchedSkill": "hiring-activity-assistant", - "kind": "skill", - "skill": "hiring-activity-assistant", - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.createPosition": { - "pageKey": "create-position", - "route": "/admin/positions/new", - "skills": [], - "suggestions": [], - "prompts": [ - "Explain the vetting weights", - "Compare with similar roles", - "Credentials in use", - "How this form works" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.candidatesList": { - "pageKey": "candidates", - "route": "/admin/candidates", - "skills": [ - "candidate-search" - ], - "suggestions": [], - "prompts": [ - "1 waiting on a decision", - "Who are the strongest candidates?", - "Which candidates are ready for interview?", - "10 have no score", - "Summarize the candidate pipeline", - "Identify candidate risks" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.candidates": { - "pageKey": "candidates-analysis", - "route": "/admin/candidates-analysis", - "skills": [ - "candidate-search" - ], - "suggestions": [], - "prompts": [ - "Compare Chef & Marcus", - "1 missing credentials", - "10 unscored applicants", - "Recommend next actions", - "Recruitment insights" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.positions", - "doc": [ - "text", - "note" - ] - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.hiredHistory": { - "pageKey": "hired-history", - "route": "/admin/hired", - "skills": [], - "suggestions": [], - "prompts": [ - "3 hires unrated", - "Bartender leads on hires", - "Which positions hired the strongest talent?", - "Summarize recent hires", - "What hiring outcomes should I review?" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.positions", - "doc": [ - "text", - "note" - ] - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.talentPool": { - "pageKey": "talent-pool", - "route": "/admin/talent-pool", - "skills": [], - "suggestions": [], - "prompts": [ - "Why does Maria rank first?", - "4 profiles unscored", - "7 need verification", - "Who is currently available?", - "Who has the strongest score?" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.positions", - "doc": [ - "text", - "note" - ] - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.forge": { - "pageKey": "krow-forge", - "route": "/admin/university", - "skills": [ - "forge-skill-management" - ], - "suggestions": [], - "prompts": [ - "Create a skill training", - "40 published", - "What does Owliver evaluate?", - "What skills do we currently have?", - "4 skills verified" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.positions", - "doc": [ - "text", - "note" - ] - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.analytics": { - "pageKey": "analytics", - "route": "/admin/analytics", - "skills": [ - "analytics-insights" - ], - "suggestions": [], - "prompts": [ - "What is the hiring trend?", - "Which department is performing best?", - "Biggest drop at Hired", - "Which positions are converting best?", - "Summarize hiring performance", - "10 unscored applications" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.activity": { - "pageKey": "activity", - "route": "/admin/activity", - "skills": [], - "suggestions": [], - "prompts": [ - "3 unusual patterns", - "Summarize today's activity", - "What changed recently?", - "Identify security concerns", - "Who is most active?" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.positions", - "doc": [ - "text", - "note" - ] - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.profile", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - }, - "admin.profile": { - "pageKey": "profile", - "route": "/admin/profile", - "skills": [], - "suggestions": [], - "prompts": [ - "What are my permissions?", - "3 scopes are restricted", - "How is my account secured?", - "What are my preferences set to?", - "My last 8 actions" - ], - "intents": [ - { - "question": "What needs my attention?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Summarize this page", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - }, - { - "question": "Show hiring activity", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "Which positions need attention?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.positions", - "doc": [ - "text", - "note" - ] - }, - { - "question": "Where are candidates dropping off?", - "matchedSkill": null, - "kind": "navigate", - "skill": null, - "capability": null, - "action": null, - "destination": "admin.candidatesList", - "doc": [ - "text", - "note" - ] - }, - { - "question": "What are my permissions?", - "matchedSkill": null, - "kind": "answer", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": null - }, - { - "question": "What is the weather today?", - "matchedSkill": null, - "kind": "outOfScope", - "skill": null, - "capability": null, - "action": null, - "destination": null, - "doc": [ - "text", - "list", - "note" - ] - } - ] - } - } -} diff --git a/scripts/owliver-baseline.mjs b/scripts/owliver-baseline.mjs deleted file mode 100644 index f3ccc22..0000000 --- a/scripts/owliver-baseline.mjs +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Writes the Owliver behaviour baseline. - * - * node scripts/owliver-baseline.mjs # report drift, write nothing - * node scripts/owliver-baseline.mjs --write # (re)generate the snapshot - * - * The snapshot is the existing product's behaviour, recorded before the Agent - * layer was built and asserted by `skill-check.mjs` on every run afterwards. - * - * Regenerating is a deliberate act with a reason attached. A baseline rewritten - * to make a red check go green records the regression instead of catching it, - * which is worse than having no baseline at all — it converts a caught bug into - * a documented one. - */ -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; -import { dirname } from 'node:path'; -import { createServer } from 'vite'; -import { BASELINE_PATH, captureBaseline } from './owliver-capture.mjs'; - -const ROOT = process.cwd(); - -const server = await createServer({ - root: ROOT, - server: { middlewareMode: true }, - appType: 'custom', - logLevel: 'error', -}); - -const captured = await captureBaseline(server); -await server.close(); - -const serialized = `${JSON.stringify(captured, null, 2)}\n`; - -if (process.argv.includes('--write')) { - mkdirSync(dirname(BASELINE_PATH), { recursive: true }); - writeFileSync(BASELINE_PATH, serialized); - const pages = Object.keys(captured.contexts).length; - console.log(`Baseline written — ${pages} contexts, ${captured.skillIds.length} skills, ${Object.keys(captured.routes).length} routes.`); - process.exit(0); -} - -if (!existsSync(BASELINE_PATH)) { - console.error('No baseline recorded. Run `node scripts/owliver-baseline.mjs --write`.'); - process.exit(1); -} - -if (readFileSync(BASELINE_PATH, 'utf8') === serialized) { - console.log('Owliver behaviour matches the baseline.'); - process.exit(0); -} - -console.error('Owliver behaviour has DRIFTED from the baseline. Run `npm test` for the per-page detail.'); -process.exit(1); diff --git a/scripts/owliver-capture.mjs b/scripts/owliver-capture.mjs deleted file mode 100644 index ed49c6c..0000000 --- a/scripts/owliver-capture.mjs +++ /dev/null @@ -1,158 +0,0 @@ -/** - * Today's Owliver behaviour, captured as data. - * - * The Agent layer is additive: opening a Krow page must produce exactly the - * Owliver it produces now. That is a claim about eleven page contexts, their - * skills, their suggestions, their prompts and their intent routing — far more - * than anyone can hold in their head while editing, and all of it silent when - * it breaks. So it is recorded here as a value, committed, and compared on - * every run. - * - * One module, two callers: `owliver-baseline.mjs` writes the snapshot and - * `skill-check.mjs` asserts against it. A second implementation of "what we - * measure" would be a second definition of the contract. - * - * Everything below is pinned. `TODAY` is fixed rather than `new Date()`, - * because `buildFacts` windows records by date and a baseline that drifts - * daily is not a baseline. Lists are sorted; nothing depends on registry - * ordering or on the clock. - */ - -import { join } from 'node:path'; - -/** Where the committed snapshot lives. Named here rather than in the CLI so a - * reader can import the path without also running a Vite server. */ -export const BASELINE_PATH = join(process.cwd(), 'scripts/__baseline__/owliver-baseline.json'); - -/** Fixed so the snapshot means the same thing tomorrow. */ -export const TODAY = new Date('2026-08-20T09:00:00.000Z'); - -/** - * The contexts under contract. - * - * The eight Krow pages the brief protects, plus the three further Admin - * contexts the placement table carries — Create Position, Candidates Analysis - * and Profile. They are page-scoped by exactly the same mechanism, so leaving - * them out would protect most of the boundary and quietly not the rest. - */ -export const CONTEXTS = [ - 'admin.controlCenter', - 'admin.positions', - 'admin.createPosition', - 'admin.candidatesList', - 'admin.candidates', - 'admin.hiredHistory', - 'admin.talentPool', - 'admin.forge', - 'admin.analytics', - 'admin.activity', - 'admin.profile', -]; - -/** - * What gets asked, per context. - * - * Three kinds on purpose, because they exercise three different branches of - * `resolveIntent` and a change to any one of them is a regression a reader - * would notice: a question the page owns, a question another page owns (which - * must route, not answer), and a question nothing owns (which must decline). - */ -export const QUESTIONS = [ - 'What needs my attention?', - 'Summarize this page', - 'Show hiring activity', - 'Which positions need attention?', - 'Where are candidates dropping off?', - 'What are my permissions?', - 'What is the weather today?', -]; - -const sorted = (list) => [...list].sort(); - -/** Block types only — structure is the contract, wording is not. */ -const docShape = (document) => - Array.isArray(document?.blocks) ? document.blocks.map((b) => b?.type ?? null) : null; - -/** - * Loads the real module graph and reads today's behaviour off it. - * - * `ssrLoadModule` rather than a mock, for the reason `skill-check.mjs` gives: - * the pipeline is what is under test, and a mock of it would reproduce none of - * the failures this exists to catch. - */ -export async function captureBaseline(server) { - const registry = await server.ssrLoadModule('/src/lib/skills/registry.js'); - const placement = await server.ssrLoadModule('/src/components/ai-assistant/placement.js'); - const resolver = await server.ssrLoadModule('/src/lib/skills/owliverResolver.js'); - const dynamic = await server.ssrLoadModule('/src/components/ai-assistant/dynamic.js'); - const routing = await server.ssrLoadModule('/src/components/ai-assistant/routing.js'); - const insights = await server.ssrLoadModule('/src/components/ai-assistant/insights.js'); - const seed = await server.ssrLoadModule('/src/api/seed.js'); - - /* The fact sheet the panel builds, from the shipped seed at a fixed instant. */ - const facts = insights.buildFacts({ - applications: seed.seedData.JobApplication, - postings: seed.seedData.JobPosting, - interviews: seed.seedData.AIInterview, - staff: seed.seedData.Staff, - profiles: seed.seedData.WorkerProfile, - activity: seed.seedData.UserActivity, - courses: seed.seedData.Course, - profile: null, - user: seed.DEMO_USER, - today: TODAY, - }); - - const contexts = {}; - for (const contextId of CONTEXTS) { - const pageKey = registry.pageKeyForContext(contextId); - - contexts[contextId] = { - pageKey, - route: pageKey ? registry.routeForPageKey(pageKey) : null, - - /* The page boundary itself: which skills this page carries with no - agent, no account customization and nothing disabled. */ - skills: sorted(registry.skillsForContext(contextId, [], []).map((s) => s.id)), - - suggestions: sorted( - resolver.owliverSuggestions(contextId, [], [], {}).map((s) => s.label) - ), - - prompts: (dynamic.buildPrompts(contextId, facts, null) || []).map((p) => p.label), - - intents: QUESTIONS.map((question) => { - const matched = registry.matchSkill(question, contextId, [], []); - const intent = routing.resolveIntent({ question, contextId }); - return { - question, - matchedSkill: matched?.id ?? null, - kind: intent?.kind ?? null, - skill: intent?.skill?.id ?? null, - capability: intent?.capability ?? null, - action: intent?.action?.name ?? null, - destination: intent?.destination?.contextId ?? null, - doc: docShape(intent?.doc), - }; - }), - }; - } - - /* Route → context, for every placement the product declares. An agent must - never become an input to this. */ - const routes = {}; - for (const route of Object.keys(placement.PLACEMENT_ROUTES).sort()) { - routes[route] = placement.resolveAssistantContext('admin', route)?.id ?? null; - } - - return { - /* Bumped only when the shape of what we measure changes — never to make a - failing comparison pass. */ - schema: 1, - today: TODAY.toISOString(), - skillIds: sorted(registry.SKILLS.map((s) => s.id)), - diagnostics: registry.readSkillRegistry([]).diagnostics, - routes, - contexts, - }; -} diff --git a/scripts/skill-check.mjs b/scripts/skill-check.mjs deleted file mode 100644 index e0d076a..0000000 --- a/scripts/skill-check.mjs +++ /dev/null @@ -1,4393 +0,0 @@ -/** - * Skill pipeline check — Owliver skills and Board (UI) skills. - * - * Runs the *real* module graph through Vite, so `import.meta.glob`, the `@/` - * alias and the Markdown loading all behave exactly as they do in the app. That - * matters: the reliability problem this script exists for was never in the - * Markdown, it was in what the pipeline did with a definition it could only - * partly read, and a mock of that pipeline would have reproduced none of it. - * - * node scripts/skill-check.mjs # against source, as the dev server sees it - * node scripts/skill-check.mjs --dist # also assert the built bundle carries every definition - * - * Exits non-zero on failure, so it can gate a build. - */ -import { readFileSync, readdirSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; -import { createServer } from 'vite'; -import { BASELINE_PATH, captureBaseline } from './owliver-capture.mjs'; - -const ROOT = process.cwd(); -const results = []; -const record = (name, pass, detail = '') => { - results.push({ name, pass, detail }); - const mark = pass ? ' ok ' : ' FAIL '; - console.log(`[${mark}] ${name}${detail ? ` — ${detail}` : ''}`); -}; - -const server = await createServer({ - root: ROOT, - server: { middlewareMode: true }, - appType: 'custom', - logLevel: 'error', -}); - -const reg = await server.ssrLoadModule('/src/lib/skills/registry.js'); -const placement = await server.ssrLoadModule('/src/components/ai-assistant/placement.js'); - -const contextFor = (pageKey) => Object.entries(placement.PLACEMENT_ROUTES) - .find(([route]) => reg.pageKeyForRoute(route) === pageKey)?.[1] ?? null; - -/* ── 1. Discovery ─────────────────────────────────────────────────────────── */ -console.log('\n── Discovery ──'); - -const onDisk = readdirSync(join(ROOT, 'src/skills')) - .flatMap((dir) => readdirSync(join(ROOT, 'src/skills', dir)).map((f) => `${dir}/${f}`)) - .filter((f) => f.endsWith('.md')); - -record( - 'every .md under src/skills registers', - reg.SKILLS.length === onDisk.length, - `${reg.SKILLS.length} registered / ${onDisk.length} files` -); - -record( - 'no duplicate skill ids', - new Set(reg.SKILLS.map((s) => s.id)).size === reg.SKILLS.length, - `${new Set(reg.SKILLS.map((s) => s.id)).size} unique ids` -); - -record( - 'every skill declares at least one page', - reg.SKILLS.every((s) => s.pages.length > 0) -); - -/* Both managed lists are non-empty — the Workspace → Skills tabs. */ -const uiFacet = reg.skillsWithFacet(reg.SKILLS, 'ui'); -const owliverFacet = reg.skillsWithFacet(reg.SKILLS, 'owliver'); -record('Owliver Skills list is populated', owliverFacet.length > 0, `${owliverFacet.length} skills`); -record( - 'Board Skills list resolves without error', - Array.isArray(uiFacet), - `${uiFacet.length} skills declare a \`ui:\` block` -); - -/* ── 2. Registration integrity: nothing half-loads in silence ─────────────── */ -console.log('\n── Registration integrity ──'); - -const clean = reg.readSkillRegistry([]); -record( - 'shipped registry reports no diagnostics', - clean.diagnostics.length === 0, - clean.diagnostics.map((d) => d.message).join(' | ') || 'none' -); - -record( - 'every shipped skill registered every capability it declared', - reg.SKILLS.every((s) => !(s.owliverErrors?.length || s.uiErrors?.length)) -); - -/* An unreadable stored definition must be REPORTED, not swallowed. */ -const brokenYaml = `---\nid: broken\n name: bad indent\n---\n# Broken\n`; -const withBroken = reg.readSkillRegistry([{ path: 'custom/broken.md', raw: brokenYaml }]); -record( - 'unreadable stored skill is reported, not silently dropped', - withBroken.diagnostics.some((d) => d.kind === 'unreadable'), - withBroken.diagnostics.find((d) => d.kind === 'unreadable')?.message ?? 'NO DIAGNOSTIC' -); - -/* A definition with one bad capability keeps the good one AND says so. */ -const partial = `--- -id: partial-check -name: Partial Check -description: One resolvable capability and one that names no source. -pages: - - positions -status: active -owliver: - enabled: true - capabilities: - - summary - - flow - responses: - summary: - source: position.activity - periods: - - today ---- - -# Partial Check -`; -const withPartial = reg.readSkillRegistry([{ path: 'custom/partial.md', raw: partial }]); -const partialSkill = withPartial.skills.find((s) => s.id === 'partial-check'); -record( - 'partly-resolvable skill still registers its good capability', - partialSkill?.owliver.capabilities.includes('summary'), - `capabilities = ${JSON.stringify(partialSkill?.owliver.capabilities)}` -); -record( - 'and the capability it LOST is reported', - withPartial.diagnostics.some((d) => d.kind === 'incomplete' && d.skillId === 'partial-check'), - withPartial.diagnostics.find((d) => d.kind === 'incomplete')?.message ?? 'NO DIAGNOSTIC' -); - -/* Shadowing a built-in is allowed, but must be visible. */ -const shadow = `---\nid: create-position\nname: Shadow\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n# Shadow\n`; -const withShadow = reg.readSkillRegistry([{ path: 'custom/shadow.md', raw: shadow }]); -record( - 'a custom skill overriding a built-in is reported', - withShadow.diagnostics.some((d) => d.kind === 'shadowed' && d.skillId === 'create-position'), - withShadow.diagnostics.find((d) => d.kind === 'shadowed')?.message ?? 'NO DIAGNOSTIC' -); - -/* ── 3. Markdown parsing: no silent content loss ──────────────────────────── */ -console.log('\n── Markdown parsing ──'); - -const cp = reg.SKILLS.find((s) => s.id === 'create-position'); -record( - 'wrapped bullets are read whole', - cp.capabilities.some((c) => c.endsWith('out of a single sentence.')), - cp.capabilities.find((c) => c.includes('single sentence')) ?? 'TRUNCATED' -); - -record( - 'conversation steps all parse', - cp.conversation.length === 7 && cp.conversation.every((s) => s.field && s.question), - `${cp.conversation.length} steps` -); - -const numbered = reg.parseSkill( - `---\nid: numbered\nname: Numbered\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n` - + `## Capabilities\n\n1. First instruction.\n2. Second instruction.\n3. Third instruction.\n`, - { custom: true } -); -record( - 'numbered instructions are preserved', - numbered.capabilities.length === 3, - JSON.stringify(numbered.capabilities) -); - -/* A section ends at the next `##`, and a heading that merely starts with the - same word is a different heading. Both halves matter: the first is what stops - `Capabilities` swallowing the rest of the file, the second is what stops a - near-miss heading being read as the real one. */ -const bounded = reg.parseSkill( - `---\nid: bounded\nname: Bounded\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n` - + `## Capabilities\n\n- one\n- two\n\n## Capabilities (v2)\n\n- three\n\n## Actions\n\n- four\n`, - { custom: true } -); -record( - 'a section stops at the next heading', - bounded.capabilities.length === 2, - JSON.stringify(bounded.capabilities) -); - -/* Sub-headings (`###`) belong to the section they sit under, and must not end - it — `\n##\s` would match `### ` if the space were not required. */ -const nested = reg.parseSkill( - `---\nid: nested\nname: Nested\ndescription: d\npages:\n - positions\nstatus: active\n---\n\n` - + `## Capabilities\n\n- one\n\n### Detail\n\n- two\n\n## Actions\n\n- three\n`, - { custom: true } -); -record( - 'a `###` sub-heading does not truncate its section', - nested.capabilities.length === 2, - JSON.stringify(nested.capabilities) -); - -/* Frontmatter of every shipped file parses and carries its required fields. */ -let frontmatterOk = true; -const frontmatterDetail = []; -for (const s of reg.SKILLS) { - if (!s.id || !s.name || !s.pages.length) { - frontmatterOk = false; - frontmatterDetail.push(s.path); - } -} -record('every shipped skill has id, name and pages', frontmatterOk, frontmatterDetail.join(', ')); - -/* ── 4. Trigger / matching ────────────────────────────────────────────────── */ -console.log('\n── Trigger matching ──'); - -const positions = contextFor('positions'); -record('positions context resolves', Boolean(positions), String(positions)); - -const MATRIX = [ - ['create a position', 'create-position'], - ['i want to hire', 'create-position'], - ['post a job', 'create-position'], - ['add a client', 'create-position'], - ['show hiring activity', 'hiring-activity-assistant'], - ['hiring flow', 'hiring-activity-assistant'], - ['applications over time', 'hiring-activity-assistant'], -]; -for (const [question, expected] of MATRIX) { - const got = reg.matchSkill(question, positions)?.id ?? null; - record(`trigger "${question}" -> ${expected}`, got === expected, got ?? 'no match'); -} - -/* Every claimant is enumerable, so a contested phrase is not invisible. */ -const contender = `--- -id: zz-contender -name: ZZ Contender -description: Also claims a phrase create-position owns. -pages: - - positions -status: active -triggers: - - create a position -owliver: - enabled: true - capabilities: - - summary - responses: - summary: - source: position.activity - periods: - - today ---- - -# ZZ Contender -`; -const contested = [{ path: 'custom/zz.md', raw: contender }]; -const claimants = reg.matchSkills('create a position', positions, [], contested); -record( - 'all trigger claimants are enumerable', - claimants.length === 2, - claimants.map((s) => s.id).join(', ') -); -record( - 'a contested trigger is reported as a collision', - reg.readSkillRegistry(contested).diagnostics.some((d) => d.kind === 'trigger-collision'), - reg.readSkillRegistry(contested).diagnostics.find((d) => d.kind === 'trigger-collision')?.message ?? 'NO DIAGNOSTIC' -); - -/* ── 5. Owliver suggestions ───────────────────────────────────────────────── */ -console.log('\n── Suggestions ──'); - -const resolver = await server.ssrLoadModule('/src/lib/skills/owliverResolver.js'); -const declared = resolver.owliverSuggestions(positions, [], [], {}); - -record( - 'every declared suggestion names a capability the skill offers', - declared.every((c) => !c.skillCapability - || reg.SKILLS.find((s) => s.id === c.skillId)?.owliver.capabilities.includes(c.skillCapability)) -); - -const byIntent = declared.map((c) => `${c.skillId}:${c.skillCapability}`); -record( - 'no two declared suggestions resolve to one capability', - new Set(byIntent).size === byIntent.length, - byIntent.join(' | ') -); - -const dynamic = await server.ssrLoadModule('/src/components/ai-assistant/dynamic.js'); - -/* The generator is exercised at three different data shapes: the labels must - track the data, and must never appear when the count behind them is zero. */ -const factsFor = (n) => ({ - postings: [], starvedPositions: new Array(n).fill({}), unscreened: new Array(n).fill({}), - scored: [], stalled: [], ranked: [], activity: [], missingCredentials: [], -}); -const shapes = [0, 1, 7, 12]; -let labelsOk = true; -const offenders = []; -for (const n of shapes) { - for (const chip of dynamic.buildPrompts('admin.positions', factsFor(n), null)) { - /* A complete intent asks or instructs. A raw fragment — "4 ready for - interview" — does neither, and is what this check exists to catch. */ - const isIntent = /\?$/.test(chip.label) || /^(show|summari[sz]e|create|continue|compare|find|explain|what|which|who)\b/i.test(chip.label); - if (!isIntent) { labelsOk = false; offenders.push(`n=${n}: "${chip.label}"`); } - if (n === 0 && /\b0\b/.test(chip.label)) { labelsOk = false; offenders.push(`n=0 leaked a zero count: "${chip.label}"`); } - } -} -record('every generated label is a complete intent', labelsOk, offenders.join(' ; ') || 'all labels are intents'); - -/* Counts must move with the data rather than being baked in. */ -const at7 = dynamic.buildPrompts('admin.positions', factsFor(7), null).map((c) => c.label); -const at12 = dynamic.buildPrompts('admin.positions', factsFor(12), null).map((c) => c.label); -record( - 'labels are dynamic, not hardcoded', - at7.some((l) => l.includes('7')) && at12.some((l) => l.includes('12')) && !at7.some((l) => l.includes('12')), - `7 -> ${at7.find((l) => l.includes('7'))} | 12 -> ${at12.find((l) => l.includes('12'))}` -); - -const atZero = dynamic.buildPrompts('admin.positions', factsFor(0), null).map((c) => c.label); -record( - 'no count-bearing suggestion when the count is zero', - !atZero.some((l) => /\d/.test(l)), - atZero.join(' | ') -); - -/** - * The workforce branch — the one the fragments came from. - * - * "4 ready for interview" and "2 strong for Event Server" were generated here, - * so a suggestion check that passes `null` for workforce proves nothing about - * them. Applications are dated today so the "applied today" branch is live too. - */ -const workforceAt = (readyCount) => { - const position = { id: 'p1', title: 'Event Server – Fine Dining', status: 'active', headcount: 5 }; - const today = new Date(); - const applications = Array.from({ length: readyCount }, (_, i) => ({ - id: `a${i}`, - job_posting_id: 'p1', - email: `c${i}@example.com`, - applicant_name: `Candidate ${i}`, - status: i % 2 ? 'ai_screened' : 'shortlisted', - ai_score: 90, - created_date: today.toISOString(), - updated_date: today.toISOString(), - })); - return { - positions: [position], - currentPositionId: null, - context: { applications, today, profiles: [], staff: [], assignments: [] }, - }; -}; - -for (const n of [1, 4, 9]) { - const labels = dynamic - .buildPrompts('admin.positions', factsFor(2), workforceAt(n)) - .map((c) => c.label); - - const ready = labels.find((l) => /ready for interview/i.test(l)); - record( - `workforce branch at ${n}: "ready for interview" is a complete question`, - Boolean(ready) && /\?$/.test(ready) && ready.includes(String(n)) && /^Which\b/.test(ready), - ready ?? 'NOT GENERATED' - ); - - /* Singular/plural has to track the count, or the fix trades one awkward - label for another. */ - if (n === 1) { - record( - 'a single candidate reads as one candidate', - ready === 'Which 1 candidate is ready for interview?', - ready ?? 'NOT GENERATED' - ); - } - - const fragment = labels.find((l) => /^\d+\s/.test(l)); - record( - `workforce branch at ${n}: no bare status fragments`, - !fragment, - fragment ?? 'none' - ); -} - -/* ── 6. Authoring: what the editors save, and what they refuse ────────────── */ -console.log('\n── Authoring ──'); - -const fields = await server.ssrLoadModule('/src/lib/skills/skillFields.js'); -const templates = await server.ssrLoadModule('/src/lib/skills/customSkills.js'); -const surfaces = await server.ssrLoadModule('/src/lib/skills/surfaces.js'); - -/* A template must produce something that works before it is edited. The Board - template used to default to a source its own default placement cannot read. */ -record( - 'the Board template validates as written', - reg.validateSkillSource(templates.uiSkillTemplate({ - id: 'template-check', name: 'Template Check', pages: ['positions'], - })) === null, - reg.validateSkillSource(templates.uiSkillTemplate({ - id: 'template-check', name: 'Template Check', pages: ['positions'], - })) || 'valid' -); - -/* An Owliver block that can answer nothing is refused rather than saved and - then reported as a skill that does not work. */ -record( - 'an Owliver skill with no capabilities is refused', - Boolean(reg.validateSkillSource(templates.owliverSkillTemplate({ - id: 'empty-owliver', name: 'Empty Owliver', pages: ['positions'], - }))), - reg.validateSkillSource(templates.owliverSkillTemplate({ - id: 'empty-owliver', name: 'Empty Owliver', pages: ['positions'], - })) || 'ACCEPTED — should have been refused' -); - -/* The section-can-never-resolve check, in both directions. */ -const boardOn = (page, placement) => `--- -id: context-check -name: Context Check -description: Reads position activity. -pages: - - ${page} -status: active -ui: - type: flow - placement: ${placement} - source: position.activity ---- - -# Context Check -`; - -record( - 'a position source on a page with no position is refused', - Boolean(reg.validateSkillSource(boardOn('analytics', 'after-header'))), - reg.validateSkillSource(boardOn('analytics', 'after-header')) || 'ACCEPTED — should have been refused' -); -record( - 'the same source is refused above the Positions grid', - Boolean(reg.validateSkillSource(boardOn('positions', 'after-position-list'))), - reg.validateSkillSource(boardOn('positions', 'after-position-list')) || 'ACCEPTED — should have been refused' -); -record( - 'and accepted inside a position card', - reg.validateSkillSource(boardOn('positions', 'after-position-card')) === null, - reg.validateSkillSource(boardOn('positions', 'after-position-card')) || 'accepted' -); - -/* Owliver responses are deliberately NOT subject to that rule: an unmet need is - a question back, not a dead card. `hiring-activity-assistant` depends on it. */ -record( - 'a shipped Owliver skill reading one position still validates', - reg.validateSkillSource( - reg.SKILLS.find((s) => s.id === 'hiring-activity-assistant').markdown - ) === null -); - -record( - 'every shipped definition still validates', - reg.SKILLS.every((s) => reg.validateSkillSource(s.markdown) === null), - reg.SKILLS.filter((s) => reg.validateSkillSource(s.markdown)).map((s) => s.id).join(', ') || 'all valid' -); - -/* Reading a definition into the editor fields — the upload path. */ -for (const skill of reg.SKILLS.filter((s) => s.kind === 'assistant')) { - const read = fields.owliverFieldsFromSource(skill.markdown); - record( - `\`${skill.id}\` reads back into the Owliver fields`, - read.id === skill.id && read.name === skill.name - && read.pages.join(',') === skill.pages.join(','), - `${read.name} / ${read.pages.join(', ')}` - ); -} - -record( - 'an unparseable file leaves the fields empty rather than throwing', - fields.owliverFieldsFromSource('not a definition').id === '' -); - -/* Writing a field back — the half that used to do nothing. */ -const original = reg.SKILLS.find((s) => s.id === 'hiring-activity-assistant').markdown; -const renamed = fields.patchFrontmatter(original, { name: 'Renamed Assistant' }); - -record( - 'patching a field changes what the registry reads', - reg.parseSkill(renamed, { custom: true }).name === 'Renamed Assistant', - reg.parseSkill(renamed, { custom: true }).name -); -record( - 'patching a field leaves the body untouched', - reg.parseSkill(renamed, { custom: true }).body === reg.parseSkill(original, { custom: true }).body -); -record( - 'patching a field preserves frontmatter comments', - renamed.includes('# One suggestion per capability.') -); -record( - 'patching a field changes nothing else', - original.split('\n').filter((l) => !l.startsWith('name:')).join('\n') - === renamed.split('\n').filter((l) => !l.startsWith('name:')).join('\n') -); -record( - 'a patched definition still validates', - reg.validateSkillSource(renamed) === null, - reg.validateSkillSource(renamed) || 'valid' -); - -const repaged = fields.patchFrontmatter(original, { pages: ['analytics', 'activity'] }); -record( - 'patching a list replaces the whole block', - reg.parseSkill(repaged, { custom: true }).pages.join(',') === 'analytics,activity', - reg.parseSkill(repaged, { custom: true }).pages.join(',') -); - -const restatused = fields.patchFrontmatter(original, { status: 'inactive' }); -record( - 'patching a key the file never declared adds it', - reg.parseSkill(restatused, { custom: true }).status === 'inactive' -); - -/* Both keys, because that is what the editor's own field handler writes: the - registry reads capabilities as the union of the declared list and the keys of - `responses:`, so patching one without the other changes nothing. */ -const recapped = fields.patchFrontmatter(original, { - 'owliver.capabilities': ['summary'], - 'owliver.responses': { summary: { source: 'position.activity', periods: ['today'] } }, -}); -record( - 'patching a nested block rewrites only that block', - reg.parseSkill(recapped, { custom: true }).owliver.capabilities.join(',') === 'summary', - reg.parseSkill(recapped, { custom: true }).owliver.capabilities.join(',') -); -/* Asserted on the text, not on the parse: with `flow` no longer offered, the - registry correctly drops the suggestion that names it — which is the rule - working, not the patch reaching a sibling key it should not have. */ -record( - 'and leaves its siblings in the same block alone', - recapped.includes('Summarize hiring activity for this position') - && recapped.includes('Show hiring activity as a flow') - && recapped.includes(' enabled: true') -); - -/* The two `ui:` shapes, told apart — the per-page form must not be overwritten - from four single-valued fields. */ -record( - 'the shorthand `ui:` form is recognised', - fields.uiShape(boardOn('positions', 'after-position-card')) === 'shorthand' -); -record( - 'a definition with no `ui:` block reports none', - fields.uiShape(original) === 'none' -); - -/* The picker and the validator read the same table. */ -record( - 'a position card supplies a position', - surfaces.contextSuppliedBy(['positions'], 'after-position-card').includes('positionId') -); -record( - 'the Positions list supplies nothing', - surfaces.contextSuppliedBy(['positions'], 'after-position-list').length === 0 -); -record( - 'Analytics supplies nothing', - surfaces.contextSuppliedBy(['analytics'], 'after-header').length === 0 -); -record( - 'every surface declares what its placements provide', - surfaces.SKILL_SURFACES.every((s) => s.provides && typeof s.provides === 'object'), - surfaces.SKILL_SURFACES.filter((s) => !s.provides).map((s) => s.id).join(', ') || 'all declared' -); - -/* ── 7. Upload hydration: a file, into the fields ─────────────────────────── */ -console.log('\n── Upload hydration ──'); - -/** - * The shapes people actually upload. - * - * Every case here is a definition that arrived from outside the editors, which - * is the only way most definitions arrive. What is being asserted is not that - * the parser is lenient — it is that the three identity fields, the pages and - * the section a reader can see in the file are the ones the form shows. - */ -const COMPLETE = `--- -id: hiring-activity-assistant -name: Hiring Activity Assistant -description: Answer questions about recent hiring activity on a position. -type: board -ui: - - page: Positions - placement: grid-card - source: position.activity ---- - -# Hiring Activity Assistant -`; - -const NO_ID = `--- -name: Hiring Activity Assistant -description: Answer questions about recent hiring activity. -pages: - - positions ---- - -# Hiring Activity Assistant -`; - -const MULTI_PAGE = `--- -id: hiring-activity -name: Hiring Activity -description: Hiring activity across the workspace. -ui: - - page: Positions - placement: grid-card - source: candidates.activity - - page: Analytics - placement: panel - source: hires.performance ---- - -# Hiring Activity -`; - -/* Case 1 — a complete definition hydrates every field it declares. */ -{ - const f = fields.boardFieldsFromSource(COMPLETE); - record('upload: name hydrates', f.name === 'Hiring Activity Assistant', f.name || 'EMPTY'); - record('upload: id hydrates', f.id === 'hiring-activity-assistant', f.id || 'EMPTY'); - record( - 'upload: description hydrates', - f.description === 'Answer questions about recent hiring activity on a position.', - f.description || 'EMPTY' - ); - record('upload: pages come from the `ui:` entries', f.pages.join(',') === 'positions', f.pages.join(',') || 'EMPTY'); - record('upload: `grid-card` resolves to a real placement', f.placement === 'after-position-card', f.placement || 'EMPTY'); - record('upload: source hydrates', f.source === 'position.activity', f.source || 'EMPTY'); - record('upload: an undeclared type is inferred from the source', f.type === 'flow', f.type || 'EMPTY'); - record('upload: the definition validates as written', reg.validateSkillSource(COMPLETE) === null, - reg.validateSkillSource(COMPLETE) || 'valid'); - record('upload: it is classified as a Board skill', - fields.facetsFromSource(COMPLETE).join(',') === 'ui', fields.facetsFromSource(COMPLETE).join(',')); - - /* The same file read by the other editor's reader — one pipeline, two views. */ - const o = fields.owliverFieldsFromSource(COMPLETE); - record('upload: the Owliver reader hydrates the same identity', - o.id === f.id && o.name === f.name && o.description === f.description); -} - -/* Case 2 — no `id:`, so it is slugged from the name and never invented. */ -{ - const f = fields.boardFieldsFromSource(NO_ID); - record('upload: a missing id slugs the name', f.id === 'hiring-activity-assistant', f.id || 'EMPTY'); - record('upload: a missing id does not become the placeholder path', f.id !== 'custom'); - record('upload: an explicit id is never replaced by a generated one', - fields.boardFieldsFromSource(COMPLETE).id === 'hiring-activity-assistant'); - record('upload: a file with no frontmatter hydrates nothing', - fields.boardFieldsFromSource('# Just a heading\n').id === ''); -} - -/* Case 3 — several pages, and every entry survives a field edit. */ -{ - const f = fields.boardFieldsFromSource(MULTI_PAGE); - record('upload: every page in the list is reported', f.pages.join(',') === 'positions,analytics', f.pages.join(',')); - - const parsed = reg.parseSkill(MULTI_PAGE, { custom: true }); - record('upload: every entry becomes a section', - parsed.ui.positions.sections.length === 1 && parsed.ui.analytics.sections.length === 1); - record('upload: each entry keeps its own source', - parsed.ui.positions.sections[0].source === 'candidates.activity' - && parsed.ui.analytics.sections[0].source === 'hires.performance'); - record('upload: a multi-page definition validates', reg.validateSkillSource(MULTI_PAGE) === null, - reg.validateSkillSource(MULTI_PAGE) || 'valid'); - - /* The fields must not be able to flatten it. */ - record('upload: the section fields are read-only against a list', !fields.uiIsEditableFromFields(MULTI_PAGE)); - record('upload: a single-section definition stays field-editable', - fields.uiIsEditableFromFields(templates.uiSkillTemplate({ id: 'x', name: 'X', pages: ['positions'] }))); - - /* Renaming is identity, not structure: it must still work, and must not - touch either entry. */ - const renamedMulti = fields.patchFrontmatter(MULTI_PAGE, { name: 'Renamed Multi' }); - const after = reg.parseSkill(renamedMulti, { custom: true }); - record('upload: renaming a multi-page definition keeps both entries', - after.ui.positions?.sections.length === 1 && after.ui.analytics?.sections.length === 1, - Object.keys(after.ui).join(',')); - record('upload: ...and actually renames it', after.name === 'Renamed Multi', after.name); - record('upload: ...and leaves the `ui:` text byte-identical', - renamedMulti.slice(renamedMulti.indexOf('ui:')) === MULTI_PAGE.slice(MULTI_PAGE.indexOf('ui:'))); -} - -/* Files as they actually arrive: from Windows, from a download, from paste. */ -{ - const dirty = { - 'a byte-order mark': `${COMPLETE}`, - 'CRLF line endings': COMPLETE.replace(/\n/g, '\r\n'), - 'a BOM and CRLF': `${COMPLETE.replace(/\n/g, '\r\n')}`, - 'a blank line above the fence': `\n\n${COMPLETE}`, - 'trailing spaces on the fence': COMPLETE.replace(/^---$/gm, '--- '), - }; - for (const [what, md] of Object.entries(dirty)) { - const f = fields.boardFieldsFromSource(md); - record( - `upload: a file with ${what} still hydrates`, - f.name === 'Hiring Activity Assistant' && f.id === 'hiring-activity-assistant' - && f.pages.join(',') === 'positions', - `${f.name || 'EMPTY'} / ${f.id || 'EMPTY'} / ${f.pages.join(',') || 'EMPTY'}` - ); - record( - `upload: ...and the registry reads it the same way`, - reg.parseSkill(md, { custom: true }).name === 'Hiring Activity Assistant', - reg.parseSkill(md, { custom: true }).name - ); - record(`upload: ...and it validates`, reg.validateSkillSource(md) === null, - reg.validateSkillSource(md) || 'valid'); - } - - /* Normalising on the way in is what keeps `patchFrontmatter` safe: an - unrecognised fence would have it write a second one above the first. */ - const patchedDirty = fields.patchFrontmatter(fields.normalizeUpload(`${COMPLETE}`), { name: 'Clean' }); - record('upload: patching a normalised file writes one frontmatter block', - (patchedDirty.match(/^---$/gm) || []).length === 2, - `${(patchedDirty.match(/^---$/gm) || []).length} fences`); - record('upload: ...and it still parses', reg.parseSkill(patchedDirty, { custom: true }).name === 'Clean'); - - /* A file with nothing to read must be refused, not read as a blank skill. */ - record('upload: a file with no fence is not readable', !fields.isReadableDefinition('# Just prose\n')); - record('upload: a real definition is readable', fields.isReadableDefinition(COMPLETE)); -} - -/* Page names and placements as they are written in the product, not as the - vocabulary spells them internally. */ -record('upload: `Positions` resolves to the positions surface', surfaces.canonicalPage('Positions') === 'positions'); -record('upload: `Talent Pool` resolves to the talent-pool surface', surfaces.canonicalPage('Talent Pool') === 'talent-pool'); -record('upload: `panel` on Analytics resolves to a real placement', - surfaces.placementFor('analytics', 'panel') === 'after-header'); -record('upload: an alias never resolves onto a surface that lacks it', - surfaces.placementFor('analytics', 'grid-card') === null); -record('upload: a canonical placement still resolves to itself', - surfaces.placementFor('positions', 'after-position-card') === 'after-position-card'); - -/* Case 4 — an existing definition reopened for editing. */ -for (const skill of reg.SKILLS) { - const f = skill.facets?.includes('ui') - ? fields.boardFieldsFromSource(skill.markdown) - : fields.owliverFieldsFromSource(skill.markdown); - record( - `reopening \`${skill.id}\` loads its identity unchanged`, - f.id === skill.id && f.name === skill.name && f.description === skill.description - && f.pages.join(',') === skill.pages.join(','), - `${f.id} / ${f.name} / ${f.pages.join(', ')}` - ); -} - -/* Case 5 — a manual edit is not reverted by later synchronisation. */ -{ - /* The editor's own handler, in miniature: hydrate from the file, edit one - field, then edit an unrelated one. The first edit must survive the second. */ - let draft = fields.owliverFieldsFromSource(COMPLETE); - let src = COMPLETE; - const edit = (patch) => { - draft = { ...draft, ...patch }; - src = fields.patchFrontmatter(src, { - id: draft.id || undefined, - name: draft.name || undefined, - description: draft.description || undefined, - pages: draft.pages?.length && !fields.pagesAreDerived(src) ? draft.pages : undefined, - }); - }; - - edit({ name: 'My Own Name' }); - record('edit: a manual name reaches the artefact', - reg.parseSkill(src, { custom: true }).name === 'My Own Name'); - - edit({ description: 'My own description.' }); - record('edit: a later edit does not revert the earlier one', - reg.parseSkill(src, { custom: true }).name === 'My Own Name', - reg.parseSkill(src, { custom: true }).name); - record('edit: ...and applies itself', - reg.parseSkill(src, { custom: true }).description === 'My own description.'); - record('edit: ...and the `ui:` block is untouched throughout', - src.slice(src.indexOf('ui:')) === COMPLETE.slice(COMPLETE.indexOf('ui:'))); - - /* A second upload replaces the draft outright — it is a new source. */ - const rehydrated = fields.owliverFieldsFromSource(MULTI_PAGE); - record('edit: a derived `pages:` is never written back', - !src.includes('\npages:'), src.includes('\npages:') ? 'pages: was inserted' : 'not written'); - - record('edit: a declared `pages:` still patches normally', - reg.parseSkill( - fields.patchFrontmatter(NO_ID, { pages: ['analytics'] }), { custom: true } - ).pages.join(',') === 'analytics'); - - record('edit: an inherited trigger is not written into the file', - !src.includes('triggers:'), src.includes('triggers:') ? 'triggers: was materialised' : 'not written'); - record('edit: a declared trigger is still read into the fields', - fields.owliverFieldsFromSource( - reg.SKILLS.find((x) => x.id === 'hiring-activity-assistant').markdown - ).triggers.includes('hiring activity')); - record('edit: a definition with no triggers reads none', - fields.owliverFieldsFromSource(COMPLETE).triggers.length === 0, - JSON.stringify(fields.owliverFieldsFromSource(COMPLETE).triggers)); - - record('edit: a second upload hydrates from the new file', - rehydrated.id === 'hiring-activity' && rehydrated.name === 'Hiring Activity', - `${rehydrated.id} / ${rehydrated.name}`); -} - -/* ── 8. Markdown ↔ manual equivalence ───────────────────────────────────── - The two authoring paths, asserted to produce one artefact. - - A form that cannot express what the format can is not a shortcut to it: the - manual Owliver editor carried a single `source` shared by every selected - capability, so a capability chosen before a source was picked composed no - `responses:` block at all — and a definition whose capabilities all drop - registers, shows as active on its page, and contributes no chip. These check - that both doors compose the same definition, and that the one that cannot be - configured is refused rather than saved empty. */ -console.log('\n── Markdown ↔ manual equivalence ──'); - -const dataResolver = await server.ssrLoadModule('/src/lib/skills/dataResolver.js'); -const positionsContext = Object.entries(placement.PLACEMENT_ROUTES) - .find(([route]) => reg.pageKeyForRoute(route) === 'positions')?.[1]; - -/* The canonical model is what `parseSkill` makes of a definition. Comparing the - whole record would compare its Markdown too, which is the one thing the two - paths are allowed to differ on — so the runtime configuration is compared, - which is exactly what every consumer reads. */ -const canonical = (skill) => JSON.parse(JSON.stringify({ - id: skill.id, - name: skill.name, - description: skill.description, - status: skill.status, - pages: skill.pages, - kind: skill.kind, - triggers: skill.triggers, - ui: skill.ui, - owliver: skill.owliver, -})); - -/* TEST A / B — the Board card, written both ways. */ -const BOARD_MD = `--- -id: board -name: Board -description: Helps Owliver understand, analyze, and act on the current task board. -pages: - - positions -status: active -ui: - type: card - placement: after-position-list-summary - title: Board - source: candidates.activity - periods: - - today - - last-7-days - - previous-month ---- - -# Board -`; - -const boardManual = templates.uiSkillTemplate({ - id: 'board', - name: 'Board', - description: 'Helps Owliver understand, analyze, and act on the current task board.', - pages: ['positions'], - type: 'card', - placement: 'after-position-list-summary', - title: 'Board', - source: 'candidates.activity', - periods: ['today', 'last-7-days', 'previous-month'], -}); - -record('TEST A: Markdown Board saves', reg.validateSkillSource(BOARD_MD) === null, - reg.validateSkillSource(BOARD_MD) || 'accepted'); -record('TEST B: manual Board saves', reg.validateSkillSource(boardManual) === null, - reg.validateSkillSource(boardManual) || 'accepted'); - -const boardFromMd = reg.parseSkill(BOARD_MD, { custom: true }); -const boardFromForm = reg.parseSkill(boardManual, { custom: true }); -const boardSection = (skill) => Object.values(skill.ui || {}).flatMap((pg) => pg.sections || [])[0]; - -record('TEST A: one section, above the position list', - Object.values(boardFromMd.ui).flatMap((pg) => pg.sections).length === 1 - && boardSection(boardFromMd).placement === 'after-position-list-summary', - `${boardSection(boardFromMd).type}@${boardSection(boardFromMd).placement}`); -record('TEST A: the Board card contributes no Owliver capability', - boardFromMd.owliver.enabled === false && boardFromMd.owliver.capabilities.length === 0); -record('TEST A: no unresolvable section', reg.unresolvableSections(boardFromMd).length === 0, - reg.unresolvableSections(boardFromMd)[0]?.message || 'none'); -record('TEST B: manual Board renders the identical section', - JSON.stringify(boardSection(boardFromForm)) === JSON.stringify(boardSection(boardFromMd)), - JSON.stringify(boardSection(boardFromForm))); -record('TEST B: manual Board keeps its title', boardSection(boardFromForm).title === 'Board', - String(boardSection(boardFromForm).title)); - -/* TEST C / D — the conversational skill, written both ways. */ -const OWLIVER_MD = `--- -id: owliver-conversation-test -name: Hiring Activity Test -description: Shows hiring activity from the Positions page. -pages: - - positions -status: active -triggers: - - hiring activity test -owliver: - enabled: true - suggestions: - - Show hiring activity - - Summarize hiring activity - capabilities: - - summary - responses: - summary: - source: candidates.activity - periods: - - today - - last-7-days - - previous-month ---- - -# Hiring Activity Test -`; - -const owliverManual = templates.owliverSkillTemplate({ - id: 'owliver-conversation-test', - name: 'Hiring Activity Test', - description: 'Shows hiring activity from the Positions page.', - pages: ['positions'], - suggestions: ['Show hiring activity', 'Summarize hiring activity'], - capabilities: ['summary'], - responses: { - summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'] }, - }, -}); - -record('TEST C: Markdown Owliver skill saves', reg.validateSkillSource(OWLIVER_MD) === null, - reg.validateSkillSource(OWLIVER_MD) || 'accepted'); -record('TEST D: manual Owliver skill saves', reg.validateSkillSource(owliverManual) === null, - reg.validateSkillSource(owliverManual) || 'accepted'); - -for (const [label, source] of [['TEST C', OWLIVER_MD], ['TEST D', owliverManual]]) { - const custom = [{ path: 'custom/owliver-conversation-test.md', raw: source }]; - const skill = reg.parseSkill(source, { custom: true }); - - record(`${label}: the capability keeps its source`, - skill.owliver.responses.summary?.source === 'candidates.activity', - skill.owliver.responses.summary?.source || 'dropped'); - record(`${label}: the capability keeps its periods`, - JSON.stringify(skill.owliver.responses.summary?.periods) - === JSON.stringify(['today', 'last-7-days', 'previous-month']), - JSON.stringify(skill.owliver.responses.summary?.periods)); - record(`${label}: it draws no page card`, - Object.keys(skill.ui).length === 0, `${Object.keys(skill.ui).length} pages`); - - const chips = resolver.owliverSuggestions(positionsContext, [], custom, {}); - const mine = chips.filter((c) => c.skillId === 'owliver-conversation-test'); - record(`${label}: its suggestion reaches the panel`, - mine.some((c) => c.label === 'Show hiring activity'), - JSON.stringify(chips.map((c) => c.label))); - record(`${label}: the suggestion answers without asking for a record`, - mine.every((c) => !c.deferred)); - - const owlSkills = resolver.owliverSkillsForContext(positionsContext, [], custom); - const matched = resolver.matchOwliverSkill('Show hiring activity', owlSkills); - record(`${label}: clicking it routes to this skill's summary`, - matched?.skill.id === 'owliver-conversation-test' && matched?.capability === 'summary' && matched?.exact, - `${matched?.skill.id}:${matched?.capability} exact=${matched?.exact}`); - - const answered = resolver.resolveOwliverResponse({ - skill: matched.skill, - capability: matched.capability, - question: 'Show hiring activity', - context: { - positions: [{ id: 'p1', title: 'Line Cook', status: 'active' }], - applications: [{ id: 'a1', job_posting_id: 'p1', created_date: new Date().toISOString() }], - }, - now: new Date(), - }); - record(`${label}: it answers with the configured reading`, - answered?.missing === null && resolver.summaryLines(answered.data).length === 3, - JSON.stringify(resolver.summaryLines(answered?.data || {}))); -} - -/* TEST H — the same skill through both doors, normalized, compared. */ -record('TEST H: Markdown and manual normalize identically (Board)', - JSON.stringify(canonical(boardFromMd)) === JSON.stringify(canonical(boardFromForm)), - JSON.stringify(canonical(boardFromForm)).slice(0, 120)); -record('TEST H: Markdown and manual normalize identically (Owliver)', - JSON.stringify(canonical(reg.parseSkill(OWLIVER_MD, { custom: true }))) - === JSON.stringify(canonical(reg.parseSkill(owliverManual, { custom: true }))), - JSON.stringify(canonical(reg.parseSkill(owliverManual, { custom: true }))).slice(0, 120)); - -/* TEST E — a capability with no source is refused, never saved empty. */ -const noSourceFields = { - id: 'no-source-test', - name: 'No Source Test', - description: 'A capability with nothing to read.', - pages: ['positions'], - suggestions: ['Show hiring activity'], - capabilities: ['summary'], - responses: {}, -}; -const noSource = templates.owliverSkillTemplate(noSourceFields); -record('TEST E: an unconfigured capability is named by the form', - JSON.stringify(fields.unconfiguredCapabilities(noSourceFields)) === '["summary"]', - JSON.stringify(fields.unconfiguredCapabilities(noSourceFields))); -record('TEST E: and the save is refused', - /owliver\.responses\.summary/.test(reg.validateSkillSource(noSource) || ''), - reg.validateSkillSource(noSource) || 'ACCEPTED — a skill with no capability would register'); -record('TEST E: the refused definition would have had zero capabilities', - reg.parseSkill(noSource, { custom: true }).owliver.capabilities.length === 0, - 'which is why it is refused rather than stored'); - -/* TEST F — two capabilities, two different sources, neither inheriting. */ -const twoSources = templates.owliverSkillTemplate({ - id: 'two-source-test', - name: 'Two Source Test', - description: 'Two capabilities reading two sources.', - pages: ['positions'], - suggestions: ['Summarize workspace applications', 'List the open roles'], - capabilities: ['summary', 'list'], - responses: { - summary: { source: 'candidates.activity', periods: ['today'] }, - list: { source: 'positions.demand', limit: 5 }, - }, -}); -const twoSkill = reg.parseSkill(twoSources, { custom: true }); -record('TEST F: manual save accepted', reg.validateSkillSource(twoSources) === null, - reg.validateSkillSource(twoSources) || 'accepted'); -record('TEST F: each capability keeps its own source', - twoSkill.owliver.responses.summary?.source === 'candidates.activity' - && twoSkill.owliver.responses.list?.source === 'positions.demand', - `summary→${twoSkill.owliver.responses.summary?.source}, list→${twoSkill.owliver.responses.list?.source}`); -record('TEST F: neither inherits the other\'s options', - JSON.stringify(twoSkill.owliver.responses.summary.periods) === '["today"]' - && twoSkill.owliver.responses.list.limit === 5 - && twoSkill.owliver.responses.list.periods.length === 0, - `summary periods=${JSON.stringify(twoSkill.owliver.responses.summary.periods)}, list limit=${twoSkill.owliver.responses.list.limit}`); -record('TEST F: both are offered as suggestions', - resolver.owliverSuggestions(positionsContext, [], [{ path: 'custom/two.md', raw: twoSources }], {}) - .filter((c) => c.skillId === 'two-source-test').length === 2); - -/* TEST G — editing one field leaves every other configuration alone. */ -const edited = fields.patchFrontmatter( - twoSources, - fields.owliverPatch( - { ...fields.owliverFieldsFromSource(twoSources), description: 'A new description.' }, - { existing: twoSources } - ) -); -const editedSkill = reg.parseSkill(edited, { custom: true }); -record('TEST G: the edit lands', editedSkill.description === 'A new description.', editedSkill.description); -record('TEST G: every capability source survives the edit', - JSON.stringify(canonical(editedSkill).owliver.responses) - === JSON.stringify(canonical(twoSkill).owliver.responses), - `summary→${editedSkill.owliver.responses.summary?.source}, list→${editedSkill.owliver.responses.list?.source}`); -record('TEST G: suggestions survive the edit', - JSON.stringify(editedSkill.owliver.suggestions) === JSON.stringify(twoSkill.owliver.suggestions)); - -const boardEdited = fields.patchFrontmatter( - boardManual, - fields.boardPatch( - { ...fields.boardFieldsFromSource(boardManual), description: 'A new description.' }, - { existing: boardManual } - ) -); -record('TEST G: Board UI configuration survives the edit', - JSON.stringify(boardSection(reg.parseSkill(boardEdited, { custom: true }))) - === JSON.stringify(boardSection(boardFromForm)), - JSON.stringify(boardSection(reg.parseSkill(boardEdited, { custom: true })))); - -/* TEST 6 — one source/shape resolver, shared by the pickers and the validator. */ -record('shape compatibility: the resolver refuses what the normalizer refuses', - surfaces.sourcesForShape('list').every((src) => surfaces.sourceSupportsShape(src.id, 'list')) - && !surfaces.sourcesForShape('list').some((src) => src.id === 'candidates.activity'), - `list sources: ${surfaces.sourcesForShape('list').map((s) => s.id).join(', ')}`); -record('shape compatibility: a list against candidates.activity is refused on save', - /cannot be shown as/.test(reg.validateSkillSource(templates.owliverSkillTemplate({ - id: 'bad-shape-test', - name: 'Bad Shape Test', - description: 'A list of something with no list in it.', - pages: ['positions'], - capabilities: ['list'], - responses: { list: { source: 'candidates.activity' } }, - })) || ''), - 'the picker cannot offer it, and the save refuses it'); -record('summary is prose, so every source suits it', - surfaces.shapeForCapability('summary') === null - && surfaces.sourcesForShape(null).length === surfaces.DATA_SOURCES.length); - -/* ── 9. The reported failure, reproduced end to end ───────────────────────── */ -console.log('\n── Regression: manual Owliver skill on Positions ──'); - -/* Exactly the configuration from the report: Pages = Positions, one suggestion, - Summary, `candidates.activity`, three periods — composed the way the form - composes it, then read the way the panel reads it. */ -const REGRESSION = templates.owliverSkillTemplate({ - id: 'owliver-conversation-test', - name: 'Hiring Activity Test', - description: 'Shows hiring activity from the Positions page.', - pages: ['positions'], - suggestions: ['Show hiring activity'], - capabilities: ['summary'], - responses: { - summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'] }, - }, -}); -const stored = [{ path: 'custom/owliver-conversation-test.md', raw: REGRESSION }]; - -record('the manual definition saves', reg.validateSkillSource(REGRESSION) === null, - reg.validateSkillSource(REGRESSION) || 'accepted'); -record('it registers', reg.allSkills(stored).some((s) => s.id === 'owliver-conversation-test')); -record('it is attached to Positions', - reg.skillsForContext(positionsContext, [], stored).some((s) => s.id === 'owliver-conversation-test')); -record('it keeps a capability — the step that used to empty', - reg.parseSkill(REGRESSION, { custom: true }).owliver.capabilities.length === 1, - JSON.stringify(reg.parseSkill(REGRESSION, { custom: true }).owliver.capabilities)); -record('it reaches owliverSkillsForContext', - resolver.owliverSkillsForContext(positionsContext, [], stored) - .some((s) => s.id === 'owliver-conversation-test')); - -const regressionChips = resolver.owliverSuggestions(positionsContext, [], stored, {}); -record('"Show hiring activity" is offered as a chip', - regressionChips.some((c) => c.label === 'Show hiring activity'), - JSON.stringify(regressionChips.map((c) => c.label))); - -const clicked = resolver.matchOwliverSkill( - 'Show hiring activity', - resolver.owliverSkillsForContext(positionsContext, [], stored) -); -record('clicking it invokes this skill\'s summary', - clicked?.skill.id === 'owliver-conversation-test' && clicked?.capability === 'summary', - `${clicked?.skill.id}:${clicked?.capability}`); - -const now = new Date(); -const answer = resolver.resolveOwliverResponse({ - skill: clicked.skill, - capability: clicked.capability, - question: 'Show hiring activity', - context: { - positions: [{ id: 'p1', title: 'Line Cook', status: 'active' }], - applications: [ - { id: 'a1', job_posting_id: 'p1', created_date: now.toISOString() }, - { id: 'a2', job_posting_id: 'p1', created_date: new Date(now - 3 * 86400000).toISOString() }, - ], - }, - now, -}); -record('it answers with the configured source and periods', - answer?.missing === null && answer.section.source === 'candidates.activity' - && answer.section.periods.length === 3, - `${answer?.section.source} over ${JSON.stringify(answer?.section.periods)}`); -record('the answer carries real figures', - resolver.summaryLines(answer.data).length === 3, - JSON.stringify(resolver.summaryLines(answer.data))); -record('and no `position.activity needs a position` is possible', - dataResolver.resolveSkillData(answer.section, { applications: [], positions: [] }, now).unavailable !== true, - 'the source declares no context requirement'); - -/* Reload: what is stored is the Markdown, so the round trip through storage is - the round trip through the parser. */ -const reloaded = reg.parseSkill( - fields.patchFrontmatter(REGRESSION, {}), - { custom: true } -); -record('the source survives a reload', - reloaded.owliver.responses.summary?.source === 'candidates.activity', - reloaded.owliver.responses.summary?.source || 'lost'); -record('the form reads it back unchanged', - JSON.stringify(fields.owliverFieldsFromSource(REGRESSION).responses) - === JSON.stringify({ summary: { source: 'candidates.activity', periods: ['today', 'last-7-days', 'previous-month'], limit: null } }), - JSON.stringify(fields.owliverFieldsFromSource(REGRESSION).responses)); - -/* ── 10. Storage round trip ─────────────────────────────────────────────── - The editors do not hand the registry a record; they write Markdown into - `preferences.customSkills`, which `base44Client` persists to localStorage and - reads back on the next load. "The source disappeared after reload" is a claim - about *that* path, so it is exercised here rather than assumed: the same - module the app runs, against a localStorage the same shape the browser's is. */ -console.log('\n── Storage round trip ──'); - -const store = new Map(); -globalThis.localStorage = { - getItem: (k) => (store.has(k) ? store.get(k) : null), - setItem: (k, v) => store.set(k, String(v)), - removeItem: (k) => store.delete(k), -}; - -const { base44 } = await server.ssrLoadModule('/src/api/base44Client.js'); -const custom = await server.ssrLoadModule('/src/lib/skills/customSkills.js'); - -/* Saving is what the editor's Save button does: compose, validate, upsert, - write the preference. */ -const saved = custom.upsertCustomSkill(base44.auth.preferences().customSkills || [], REGRESSION); -const written = await base44.auth.updatePreferences({ customSkills: saved.next }); -record('the definition is persisted', written.persisted, written.error?.message || 'written to storage'); - -/* Reload: a fresh read of the same key, exactly as a page load does. */ -const rehydratedSkills = base44.auth.preferences().customSkills || []; -record('it survives the reload', rehydratedSkills.length === 1 - && String(rehydratedSkills[0].raw).includes('candidates.activity'), - `${rehydratedSkills.length} stored`); - -const afterReload = reg.parseSkill(rehydratedSkills[0].raw, { custom: true }); -record('its capability survives the reload', - afterReload.owliver.responses.summary?.source === 'candidates.activity', - afterReload.owliver.responses.summary?.source || 'lost'); -record('and it still offers its chip after the reload', - resolver.owliverSuggestions(positionsContext, [], rehydratedSkills, {}) - .some((c) => c.label === 'Show hiring activity'), - JSON.stringify(resolver.owliverSuggestions(positionsContext, [], rehydratedSkills, {}).map((c) => c.label))); -record('the editor reopens it with its source intact', - fields.owliverFieldsFromSource(rehydratedSkills[0].raw).responses.summary?.source === 'candidates.activity', - JSON.stringify(fields.owliverFieldsFromSource(rehydratedSkills[0].raw).responses)); - -/* The Board skill through the same path, stored beside it — the two subsystems - are independent, so both must survive one write. */ -const bothStored = custom.upsertCustomSkill(rehydratedSkills, boardManual).next; -await base44.auth.updatePreferences({ customSkills: bothStored }); -const finalStored = base44.auth.preferences().customSkills || []; -const finalRegistry = reg.readSkillRegistry(finalStored); -record('Board and Owliver skills coexist in storage', finalStored.length === 2, - `${finalStored.length} stored`); -record('neither produces a registry diagnostic', - !finalRegistry.diagnostics.some((d) => d.level === 'error'), - finalRegistry.diagnostics.filter((d) => d.level === 'error').map((d) => d.message).join(' | ') || 'clean'); -record('the Board card draws once on Positions, and offers no chip', - reg.getSkillsForPage('positions', { customSources: finalStored }) - .filter((sk) => sk.id === 'board') - .every((sk) => Object.values(sk.ui).flatMap((pg) => pg.sections).length === 1 - && sk.owliver.capabilities.length === 0)); -record('the Owliver skill offers its chip, and draws no card', - resolver.owliverSuggestions(positionsContext, [], finalStored, {}) - .some((c) => c.skillId === 'owliver-conversation-test') - && Object.keys(reg.parseSkill(REGRESSION, { custom: true }).ui).length === 0); - -/* ── 11. A fresh Add Owliver Skill screen ───────────────────────────────── - What the form starts as, asserted at the state that drives the checkbox - rather than at the checkbox. `checked` is - `draft.capabilities.includes(capability.id)` and the per-capability - configuration is rendered under `{checked && …}`, so the array below is the - whole of "what is ticked" and "what is shown". */ -console.log('\n── Fresh Add Owliver Skill ──'); - -const CAPS = surfaces.OWLIVER_CAPABILITIES.map((c) => c.id); - -/* 1. NEW skill. `emptyDraft` is `EMPTY_OWLIVER_FIELDS`, which is what the - editor initializes to when there is no `:id` and nothing handed over. */ -record('new skill: initial capabilities are []', - Array.isArray(fields.EMPTY_OWLIVER_FIELDS.capabilities) - && fields.EMPTY_OWLIVER_FIELDS.capabilities.length === 0, - JSON.stringify(fields.EMPTY_OWLIVER_FIELDS.capabilities)); -record('new skill: no response is pre-configured either', - Object.keys(fields.EMPTY_OWLIVER_FIELDS.responses).length === 0, - JSON.stringify(fields.EMPTY_OWLIVER_FIELDS.responses)); -record(`new skill: all ${CAPS.length} capability checkboxes start unchecked`, - CAPS.every((id) => !fields.EMPTY_OWLIVER_FIELDS.capabilities.includes(id)), - CAPS.filter((id) => fields.EMPTY_OWLIVER_FIELDS.capabilities.includes(id)).join(', ') || 'none ticked'); - -/* The template the fresh screen composes must not declare capabilities either: - an author who types a name before ticking anything patches *that* file, and a - `capabilities:` key in it would tick boxes nobody chose. */ -const freshTemplate = templates.owliverSkillTemplate({}); -record('new skill: the composed template declares no capabilities', - !/^\s*capabilities:/m.test(freshTemplate), - freshTemplate.split('\n').filter((l) => /capabilit/i.test(l)).join(' / ') || 'no capabilities key'); -record('new skill: reading that template back still yields []', - fields.owliverFieldsFromSource(freshTemplate).capabilities.length === 0, - JSON.stringify(fields.owliverFieldsFromSource(freshTemplate).capabilities)); - -/* 2. One capability, chosen. The editor's `update({ capabilities })` is a plain - state merge, so the assertion is on what that array then means. */ -const pickedSummary = { ...fields.EMPTY_OWLIVER_FIELDS, capabilities: ['summary'] }; -record('selecting Summary ticks exactly Summary', - JSON.stringify(pickedSummary.capabilities) === '["summary"]'); -record('and only Summary renders a configuration block', - CAPS.filter((id) => pickedSummary.capabilities.includes(id)).length === 1, - CAPS.filter((id) => pickedSummary.capabilities.includes(id)).join(', ')); -record('an unsourced Summary is invalid, and only Summary is reported', - JSON.stringify(fields.unconfiguredCapabilities(pickedSummary)) === '["summary"]', - JSON.stringify(fields.unconfiguredCapabilities(pickedSummary))); - -/* 3. Choosing its source clears the error, and the definition saves. */ -const sourcedSummary = { - ...pickedSummary, - id: 'fresh-summary-test', - name: 'Fresh Summary Test', - description: 'One capability, configured.', - pages: ['positions'], - suggestions: ['Show hiring activity'], - responses: { summary: { source: 'candidates.activity', periods: ['today'] } }, -}; -record('choosing a source clears the validation error', - fields.unconfiguredCapabilities(sourcedSummary).length === 0, - JSON.stringify(fields.unconfiguredCapabilities(sourcedSummary))); -const freshSaved = templates.owliverSkillTemplate(sourcedSummary); -record('and the save succeeds', reg.validateSkillSource(freshSaved) === null, - reg.validateSkillSource(freshSaved) || 'accepted'); -record('the saved definition declares only the chosen capability', - JSON.stringify(reg.parseSkill(freshSaved, { custom: true }).owliver.capabilities) === '["summary"]', - JSON.stringify(reg.parseSkill(freshSaved, { custom: true }).owliver.capabilities)); -record('and its suggestion reaches the Positions panel', - resolver.owliverSuggestions(positionsContext, [], [{ path: 'custom/fresh.md', raw: freshSaved }], {}) - .some((c) => c.skillId === 'fresh-summary-test')); - -/* 4. Several, chosen by hand — only those, each validated on its own. */ -const pickedTwo = { - ...fields.EMPTY_OWLIVER_FIELDS, - capabilities: ['summary', 'list'], - responses: { summary: { source: 'candidates.activity' } }, -}; -record('selecting two capabilities renders exactly those two', - CAPS.filter((id) => pickedTwo.capabilities.includes(id)).join(',') === 'summary,list'); -record('and only the unconfigured one is reported', - JSON.stringify(fields.unconfiguredCapabilities(pickedTwo)) === '["list"]', - JSON.stringify(fields.unconfiguredCapabilities(pickedTwo))); - -/* 5. An EXISTING skill hydrates from its own definition, and from nothing else. - Including the capability that did *not* resolve: it is the one the author - opened the editor to fix, and dropping it from the form would delete it - from the file on the next save. */ -const DECLARED_ONE = `--- -id: declared-one -name: Declared One -description: Declares a single capability. -pages: - - positions -status: active -owliver: - enabled: true - suggestions: - - Show hiring activity - capabilities: - - summary - responses: - summary: - source: candidates.activity ---- - -# Declared One -`; -record('existing skill: hydrates exactly its declared capability', - JSON.stringify(fields.owliverFieldsFromSource(DECLARED_ONE).capabilities) === '["summary"]', - JSON.stringify(fields.owliverFieldsFromSource(DECLARED_ONE).capabilities)); -record('existing skill: no new-skill default is applied on top', - CAPS.filter((id) => fields.owliverFieldsFromSource(DECLARED_ONE).capabilities.includes(id)).length === 1); - -const HALF_BROKEN = DECLARED_ONE - .replace(' - summary\n', ' - summary\n - list\n') - .replace('id: declared-one', 'id: half-broken'); -const halfFields = fields.owliverFieldsFromSource(HALF_BROKEN); -record('existing skill: a declared capability that lost its source still shows', - JSON.stringify(halfFields.capabilities) === '["summary","list"]', - JSON.stringify(halfFields.capabilities)); -record('...unsourced, so it stays invalid rather than silently dropped', - JSON.stringify(fields.unconfiguredCapabilities(halfFields)) === '["list"]', - JSON.stringify(fields.unconfiguredCapabilities(halfFields))); -record('...and the configured one keeps its source through the round trip', - halfFields.responses.summary.source === 'candidates.activity', - halfFields.responses.summary.source); -record('...while the runtime still registers only what can answer', - JSON.stringify(reg.parseSkill(HALF_BROKEN, { custom: true }).owliver.capabilities) === '["summary"]', - JSON.stringify(reg.parseSkill(HALF_BROKEN, { custom: true }).owliver.capabilities)); - -/* A definition inheriting its reading from a `ui:` section still hydrates the - capability it actually offers — the union is declaration-first, not - declaration-only. */ -const INHERITED = `--- -id: inherited-cap -name: Inherited Cap -description: A capability reading the page section. -pages: - - positions -status: active -ui: - type: card - placement: after-position-list-summary - source: candidates.activity -owliver: - enabled: true - capabilities: - - card ---- - -# Inherited Cap -`; -record('a capability inheriting from `ui:` still hydrates with its source', - fields.owliverFieldsFromSource(INHERITED).responses.card?.source === 'candidates.activity', - fields.owliverFieldsFromSource(INHERITED).responses.card?.source || 'lost'); - -/* ── 12. Attendance and overtime data ───────────────────────────────────── - * - * Attendance is the one collection whose dates are anchored to now rather than - * written as calendar dates, and the one whose foreign keys have to line up - * with a narrative written months earlier. Both are easy to get wrong in ways - * that look fine: a shift pointing at a staff member who was never hired reads - * as a working feature until someone asks whose shift it was. - * - * These checks are about the data being *true* — that it joins, that it is - * windowable by the existing period machinery, and that the analyses over it - * find what is there and stay quiet about what is not. - */ -console.log('\n── Attendance and overtime data ──'); - -const attendance = await server.ssrLoadModule('/src/lib/attendance.js'); -const shiftSeed = await server.ssrLoadModule('/src/api/attendanceSeed.js'); -const seedModule = await server.ssrLoadModule('/src/api/seed.js'); -const resolverModule = await server.ssrLoadModule('/src/lib/skills/dataResolver.js'); - -const SHIFTS = shiftSeed.SHIFT_RECORDS; -const SHIFT_STATUSES = ['present', 'late', 'absent', 'no_show', 'excused']; - -record('shift records are seeded', SHIFTS.length > 0, `${SHIFTS.length} shifts`); -record('the seed registers them as `ShiftRecord`', - Array.isArray(seedModule.seedData.ShiftRecord) && seedModule.seedData.ShiftRecord.length === SHIFTS.length, - `${seedModule.seedData.ShiftRecord?.length ?? 0} in seedData`); - -record('every shift has a unique id', - new Set(SHIFTS.map((s) => s.id)).size === SHIFTS.length); - -record('every shift status is one the product recognises', - SHIFTS.every((s) => SHIFT_STATUSES.includes(s.status)), - [...new Set(SHIFTS.map((s) => s.status))].join(', ')); - -record('every shift was scheduled for real hours', - SHIFTS.every((s) => s.scheduled_hours > 0)); - -record('overtime is never negative', - SHIFTS.every((s) => s.overtime_hours >= 0)); - -/* ── The joins ───────────────────────────────────────────────────────────── */ - -const staffIds = new Set(seedModule.seedData.Staff.map((s) => s.id)); -const postingsById = new Map(seedModule.seedData.JobPosting.map((p) => [p.id, p])); - -record('every shift belongs to someone who was actually hired', - SHIFTS.every((s) => staffIds.has(s.staff_id)), - [...new Set(SHIFTS.map((s) => s.staff_id).filter((id) => !staffIds.has(id)))].join(', ') || 'all resolve'); - -record('every shift belongs to a real position', - SHIFTS.every((s) => postingsById.has(s.job_posting_id)), - [...new Set(SHIFTS.map((s) => s.job_posting_id).filter((id) => !postingsById.has(id)))].join(', ') || 'all resolve'); - -/* "Department" has to mean the same thing here as it does on Hired History and - Analytics, both of which read it off the posting — see the join in - `lib/hiringRecords.js`. A shift carrying its own category is a denormalized - copy, and a copy that disagrees is worse than no copy. */ -record('a shift\'s department matches its position\'s', - SHIFTS.every((s) => postingsById.get(s.job_posting_id)?.role_category === s.role_category), - SHIFTS.filter((s) => postingsById.get(s.job_posting_id)?.role_category !== s.role_category) - .map((s) => s.id).join(', ') || 'all agree'); - -/** - * `created_date` is the instant the shift was worked. - * - * Load-bearing rather than incidental: `inPeriod` windows every collection on - * `created_date`, so if these drifted apart every period reading of attendance - * would come back empty and nothing would say why. - */ -record('a shift\'s created date is the instant it was worked', - SHIFTS.every((s) => s.created_date === s.scheduled_start)); - -/* ── Hours add up ────────────────────────────────────────────────────────── */ - -record('a missed shift is zero hours worked, not a short one', - SHIFTS.filter((s) => s.status === 'absent' || s.status === 'no_show') - .every((s) => s.actual_hours === 0 && s.overtime_hours === 0 && s.actual_start === null)); - -record('a worked shift\'s hours are its schedule, less lateness, plus overtime', - SHIFTS.filter((s) => s.status === 'present' || s.status === 'late').every((s) => { - const expected = s.scheduled_hours - s.minutes_late / 60 + s.overtime_hours; - return Math.abs(s.actual_hours - expected) < 0.02; - })); - -record('a late shift is one that was turned up for', - SHIFTS.filter((s) => s.status === 'late').every((s) => s.minutes_late > 0 && s.actual_start !== null)); - -/* ── Reachable the way a page reaches it ─────────────────────────────────── */ - -const client = await server.ssrLoadModule('/src/api/base44Client.js'); -const listed = await client.base44.entities.ShiftRecord.list('-created_date', 500); - -record('shifts are queryable through the entity API', - listed.length === SHIFTS.length, `${listed.length} returned`); - -record('...newest first, like every other collection', - listed.every((s, i) => i === 0 - || new Date(listed[i - 1].created_date).getTime() >= new Date(s.created_date).getTime())); - -const filtered = await client.base44.entities.ShiftRecord.filter({ status: 'absent' }); -record('...and filterable by field', - filtered.length > 0 && filtered.every((s) => s.status === 'absent'), - `${filtered.length} absences`); - -/* ── The sources future skills will declare ──────────────────────────────── */ - -for (const id of ['workforce.attendance', 'workforce.overtime']) { - const source = surfaces.dataSourceFor(id); - record(`\`${id}\` is a declarable source`, Boolean(source)); - record(`\`${id}\` needs no record in context`, source?.context === null); - record(`\`${id}\` supports every shape it declares`, - source.shapes.every((shape) => surfaces.sourceSupportsShape(id, shape)), - source.shapes.join(', ')); - record(`\`${id}\` accepts the options it declares`, - (source.options || []).every((option) => surfaces.sourceSupportsOption(id, option)), - (source.options || []).join(', ') || 'none'); - - /* Honest emptiness. A source with nothing to read must say so rather than - reporting zeros that look like findings. */ - const empty = resolverModule.resolveSkillData({ source: id }, { shifts: [] }); - record(`\`${id}\` reports honestly when there are no shifts`, - empty.empty === true && Boolean(empty.emptyNote), - empty.emptyNote || 'NO NOTE'); - - const full = resolverModule.resolveSkillData({ source: id }, { shifts: SHIFTS }); - record(`\`${id}\` reads the seeded shifts`, - full.empty === false && full.steps.length > 0 && full.items.length > 0, - `${full.steps.length} figures, ${full.items.length} rows`); - - const periodic = resolverModule.resolveSkillData( - { source: id, periods: ['last-7-days', 'previous-month'] }, { shifts: SHIFTS } - ); - record(`\`${id}\` windows by period`, - periodic.steps.length === 2 && periodic.steps.some((s) => s.records.length > 0), - periodic.steps.map((s) => `${s.label}=${s.records.length}`).join(', ')); -} - -/* ── The analyses ────────────────────────────────────────────────────────── */ - -const summary = attendance.attendanceSummary(SHIFTS); -record('attendance counts every scheduled shift exactly once', - summary.worked + summary.missed === summary.scheduled, - `${summary.worked} worked + ${summary.missed} missed = ${summary.scheduled}`); -record('attendance rate is a percentage', - summary.attendanceRate >= 0 && summary.attendanceRate <= 100, `${summary.attendanceRate}%`); -record('punctuality is never better than attendance', - summary.punctualityRate <= summary.attendanceRate, - `${summary.punctualityRate}% punctual / ${summary.attendanceRate}% present`); - -const overtime = attendance.overtimeSummary(SHIFTS); -record('overtime is the gap between hours worked and hours scheduled', - overtime.hours > 0 && overtime.hoursWorked > overtime.hoursScheduled - summary.minutesLate / 60, - `${overtime.hours}h overtime`); - -const workers = attendance.attendanceByWorker(SHIFTS); -record('every worker on the roster is compared', - workers.length === new Set(SHIFTS.map((s) => s.staff_id)).size, `${workers.length} workers`); -record('workers are ordered worst attendance first', - workers.every((w, i) => i === 0 || workers[i - 1].attendanceRate <= w.attendanceRate), - workers.map((w) => `${w.name.split(' ')[0]} ${w.attendanceRate}%`).join(', ')); - -const overtimeWorkers = attendance.overtimeByWorker(SHIFTS); -record('overtime comparison is ordered most hours first', - overtimeWorkers.every((w, i) => i === 0 || overtimeWorkers[i - 1].hours >= w.hours), - overtimeWorkers.map((w) => `${w.name.split(' ')[0]} ${w.hours}h`).join(', ')); - -const departments = attendance.attendanceByDepartment(SHIFTS); -record('departments are compared too', - departments.length > 1 && departments.every((d) => d.people > 0), - departments.map((d) => `${d.name} ${d.attendanceRate}%`).join(', ')); - -const trend = attendance.weeklyTrend(SHIFTS); -record('the weekly trend runs oldest to newest', - trend.length >= 4 && trend.every((w, i) => i === 0 - || new Date(trend[i - 1].weekStart).getTime() < new Date(w.weekStart).getTime()), - `${trend.length} weeks`); -record('a week nobody was rostered for is not reported as 0% attendance', - trend.every((w) => w.scheduled > 0)); - -/* ── Anomalies: what it finds, and what it stays quiet about ─────────────── */ - -const findings = attendance.attendanceAnomalies(SHIFTS); -record('the seeded attendance decline is found', - findings.some((f) => f.kind === 'attendance'), - findings.filter((f) => f.kind === 'attendance').map((f) => f.title).join(' | ') || 'NOT FOUND'); -record('the seeded overtime climb is found', - findings.some((f) => f.kind === 'overtime'), - findings.find((f) => f.kind === 'overtime')?.detail || 'NOT FOUND'); -record('every finding carries the figures behind it', - findings.every((f) => f.title && f.detail && f.severity && f.metric)); -record('findings are ordered most severe first', - findings.every((f, i) => i === 0 - || ({ high: 0, medium: 1, low: 2 })[findings[i - 1].severity] <= ({ high: 0, medium: 1, low: 2 })[f.severity]), - findings.map((f) => f.severity).join(' → ')); - -/** - * The check that matters most, and the one a detector like this usually fails. - * - * A rule that flags everything is worse than no rule: it trains its reader to - * skim, and the one finding that mattered goes past unread. So a workforce with - * nothing wrong must produce *nothing at all* — not a low-severity note, not a - * "no issues" finding. - */ -const flawless = SHIFTS.map((s) => ({ - ...s, status: 'present', minutes_late: 0, overtime_hours: 0, actual_hours: s.scheduled_hours, -})); -record('a workforce with nothing wrong produces no findings', - attendance.attendanceAnomalies(flawless).length === 0, - `${attendance.attendanceAnomalies(flawless).length} findings`); - -record('no shift records produces no findings either', - attendance.attendanceAnomalies([]).length === 0); - -/* One bad week is a bad week; it takes a second to be a direction. */ -const oneOffWeek = SHIFTS.map((s) => { - const daysBack = Math.round((Date.now() - new Date(s.created_date).getTime()) / 86400000); - return daysBack > 21 && daysBack <= 28 ? { ...s, status: 'absent', actual_hours: 0, overtime_hours: 0 } : { ...s, status: 'present', minutes_late: 0, overtime_hours: 0, actual_hours: s.scheduled_hours }; -}); -record('a single bad week months ago is not reported as a current problem', - !attendance.attendanceAnomalies(oneOffWeek).some((f) => f.id === 'missed-shifts-rising'), - attendance.attendanceAnomalies(oneOffWeek).map((f) => f.id).join(', ') || 'none'); - -/* ── 13. Cross-domain data sources ──────────────────────────────────────── - * - * Eight readings over records the workspace already holds. The failure these - * guard against is a source that *looks* like it works: one that returns - * plausible zeros when it has nothing to read, or that quietly assumes a field - * the data does not carry. Either produces a skill that appears to answer and - * is answering about nothing. - * - * So every source is checked twice — against the seed, and against an empty - * workspace — and the empty case has to say so rather than report zeros. - */ -console.log('\n── Cross-domain data sources ──'); - -const signalsModule = await server.ssrLoadModule('/src/lib/activitySignals.js'); -const insightsModule = await server.ssrLoadModule('/src/components/ai-assistant/insights.js'); - -const expectedBaseline = JSON.parse(readFileSync(BASELINE_PATH, 'utf8')); -const SEED = seedModule.seedData; - -/* The workspace a declared reading is checked against — the same collections the - panel hands a resolver at runtime. Declared once here because both the - analysis-skill and agent sections read against it. */ -const AGENT_SKILL_CONTEXT = { - positions: SEED.JobPosting, - applications: SEED.JobApplication, - interviews: SEED.AIInterview, - staff: SEED.Staff, - profiles: SEED.WorkerProfile, - workerProfiles: SEED.WorkerProfile, - activity: SEED.UserActivity, - assignments: SEED.Assignment, - courses: SEED.Course, - shifts: SHIFTS, - trainingPaths: [], -}; -const FULL_CONTEXT = { - positions: SEED.JobPosting, - applications: SEED.JobApplication, - interviews: SEED.AIInterview, - staff: SEED.Staff, - profiles: SEED.WorkerProfile, - workerProfiles: SEED.WorkerProfile, - activity: SEED.UserActivity, - assignments: SEED.Assignment, - shifts: SHIFTS, -}; - -const NEW_SOURCES = [ - 'positions.risk', - 'candidates.quality', - 'talent.pool', - 'workforce.coverage', - 'activity.signals', - 'activity.breakdown', - 'operations.risk', - 'workspace.summary', -]; - -for (const id of NEW_SOURCES) { - const decl = surfaces.dataSourceFor(id); - - record(`\`${id}\` is a declarable source`, Boolean(decl)); - if (!decl) continue; - - /* Every one of these is a workspace-level reading. A source that needed a - record in context could not be answered from a page that has none, and - would validate then fail at read time. */ - record(`\`${id}\` needs no record in context`, decl.context === null); - - record(`\`${id}\` supports every shape it declares`, - decl.shapes.every((shape) => surfaces.sourceSupportsShape(id, shape)), - decl.shapes.join(', ')); - - record(`\`${id}\` accepts the options it declares`, - (decl.options || []).every((option) => surfaces.sourceSupportsOption(id, option)), - (decl.options || []).join(', ') || 'none'); - - /* Reads the workspace as it actually is. */ - const readFull = resolverModule.resolveSkillData({ source: id }, FULL_CONTEXT); - record(`\`${id}\` reads the seeded workspace`, - readFull.empty === false && (readFull.steps?.length > 0), - `${readFull.steps?.length ?? 0} figures, ${readFull.items?.length ?? 0} rows`); - - record(`\`${id}\` returns figures, never prose`, - (readFull.steps || []).every((step) => typeof step.value === 'number'), - (readFull.steps || []).map((s) => `${s.title}=${s.value}`).join(', ')); - - /** - * The check that separates a working source from one that only looks like it. - * - * An empty workspace must produce `empty: true` and a note saying why — not - * a row of zeros, which reads as a measured result rather than an absent one. - */ - const readEmpty = resolverModule.resolveSkillData({ source: id }, {}); - record(`\`${id}\` reports honestly when there is nothing to read`, - readEmpty.empty === true && Boolean(readEmpty.emptyNote), - readEmpty.emptyNote || 'NO NOTE'); - - record(`\`${id}\` never throws on a half-empty workspace`, - (() => { - try { - const partialRead = resolverModule.resolveSkillData({ source: id }, { positions: SEED.JobPosting }); - return partialRead && typeof partialRead.empty === 'boolean'; - } catch { - return false; - } - })()); -} - -/* ── Period filtering, where it applies ──────────────────────────────────── */ - -for (const id of ['candidates.quality', 'activity.breakdown']) { - const readPeriodic = resolverModule.resolveSkillData( - { source: id, periods: ['previous-month'] }, FULL_CONTEXT - ); - const all = resolverModule.resolveSkillData({ source: id }, FULL_CONTEXT); - record(`\`${id}\` windows by period`, - readPeriodic.total <= all.total, - `${readPeriodic.total} in the previous month / ${all.total} in total`); - - /* Overlapping windows must not count the same record twice — `today` sits - inside `last-7-days`, and a naive concatenation would double it. */ - const overlapping = resolverModule.resolveSkillData( - { source: id, periods: ['today', 'last-7-days'] }, FULL_CONTEXT - ); - const sevenOnly = resolverModule.resolveSkillData( - { source: id, periods: ['last-7-days'] }, FULL_CONTEXT - ); - record(`\`${id}\` does not double-count overlapping periods`, - overlapping.total === sevenOnly.total, - `${overlapping.total} vs ${sevenOnly.total}`); -} - -/* ── `activity.signals` reuses the assistant's own detection ─────────────── */ - -/** - * The reuse assertion. - * - * `activitySignals` moved out of `buildFacts` so a data source could read it - * without a library importing from the component tree. The whole value of that - * move is that there is still exactly one implementation — "two unusual - * patterns" has to mean the same two in the greeting and on the card. - */ -const factSheet = insightsModule.buildFacts({ - applications: SEED.JobApplication, - postings: SEED.JobPosting, - interviews: SEED.AIInterview, - staff: SEED.Staff, - profiles: SEED.WorkerProfile, - activity: SEED.UserActivity, - courses: SEED.Course, - profile: null, - user: seedModule.DEMO_USER, - today: new Date('2026-08-20T09:00:00.000Z'), -}); -const direct = signalsModule.activitySignals(SEED.UserActivity, new Date('2026-08-20T09:00:00.000Z')); - -record('the fact sheet and the extracted detection agree exactly', - JSON.stringify(factSheet.activitySignals) === JSON.stringify(direct), - JSON.stringify(direct.flags)); - -record('`PRIVILEGED_EVENTS` is still importable from insights.js', - JSON.stringify(insightsModule.PRIVILEGED_EVENTS) === JSON.stringify(signalsModule.PRIVILEGED_EVENTS), - JSON.stringify(insightsModule.PRIVILEGED_EVENTS)); - -const signalsRead = resolverModule.resolveSkillData({ source: 'activity.signals' }, FULL_CONTEXT); -record('the activity.signals source reports the same flags the greeting counts', - signalsRead.items.length === factSheet.activitySignals.flags.length, - `${signalsRead.items.length} signals`); - -record('every signal is explained rather than named', - signalsRead.items.every((i) => i.title && i.detail && i.title !== i.id)); - -/* A workspace with nothing out of pattern is a real answer, and a different - one from having no log at all. */ -const quiet = resolverModule.resolveSkillData({ source: 'activity.signals' }, { activity: [] }); -record('an empty log and a quiet log are different answers', - quiet.emptyNote !== signalsRead.emptyNote); - -/* ── `workforce.coverage` refuses to invent a headcount ──────────────────── */ - -/** - * No seeded position declares a headcount, and `demandFor` reports that rather - * than defaulting to one person per role. This check exists because the obvious - * shortcut — assume 1, report a fill rate — produces a confident percentage - * that means nothing, and nothing on screen would say so. - */ -const coverage = resolverModule.resolveSkillData({ source: 'workforce.coverage' }, FULL_CONTEXT); -const declaredStep = coverage.steps.find((s) => s.id === 'declared'); -record('coverage says out loud when no role states a headcount', - declaredStep.value === 0 && /does not|not state|No open role/i.test(declaredStep.detail), - declaredStep.detail); -record('coverage still reports who has actually been hired', - coverage.steps.find((s) => s.id === 'covered').value > 0, - `${coverage.steps.find((s) => s.id === 'covered').value} roles have someone hired`); -record('a role with no headcount reports hires, not a fill percentage', - coverage.items.every((i) => i.declared || /headcount not stated/.test(i.detail))); - -/* ── `positions.risk` distinguishes "nothing wrong" from "nothing posted" ── */ - -const noRoles = resolverModule.resolveSkillData({ source: 'positions.risk' }, { positions: [], applications: [] }); -const healthy = resolverModule.resolveSkillData({ source: 'positions.risk' }, { - positions: SEED.JobPosting.filter((p) => p.status === 'active').slice(0, 1), - applications: SEED.JobApplication.map((a) => ({ - ...a, job_posting_id: SEED.JobPosting.find((p) => p.status === 'active').id, ai_score: 88, status: 'hired', - })), -}); -record('positions.risk separates "no roles open" from "no roles at risk"', - noRoles.emptyNote !== healthy.emptyNote, - `${noRoles.emptyNote} / ${healthy.emptyNote}`); - -/* ── `operations.risk` stays quiet when the operation is running ─────────── */ - -record('operations.risk finds the real backlog', - resolverModule.resolveSkillData({ source: 'operations.risk' }, FULL_CONTEXT).findings.length > 0, - resolverModule.resolveSkillData({ source: 'operations.risk' }, FULL_CONTEXT) - .findings.map((f) => f.id).join(', ')); - -const readClean = resolverModule.resolveSkillData({ source: 'operations.risk' }, { - /* Everything screened and decided, every role has applicants, every shift worked. */ - applications: SEED.JobApplication.map((a) => ({ ...a, status: 'hired', ai_score: 90 })), - positions: SEED.JobPosting.filter((p) => p.status === 'active').map((p) => ({ ...p })), - shifts: SHIFTS.map((s) => ({ ...s, status: 'present' })), -}); -record('operations.risk reports nothing when nothing is wrong', - readClean.findings.length === 0 && readClean.empty === true, - readClean.findings.map((f) => f.id).join(', ') || 'no findings'); - -/* ── `talent.pool` does not average away the unscored ───────────────────── */ - -/** - * Four of the nine seeded profiles have no score. Averaging them in as zero - * would report a healthy pool as poor, in exact proportion to how much of it - * nobody has assessed yet — a figure that gets worse as the pool grows. - */ -const pool = resolverModule.resolveSkillData({ source: 'talent.pool' }, FULL_CONTEXT); -const scoredProfiles = SEED.WorkerProfile.filter((p) => (p.krow_score || 0) > 0); -const expectedAvg = Math.round( - scoredProfiles.reduce((sum, p) => sum + p.krow_score, 0) / scoredProfiles.length -); -record('talent.pool averages only the profiles that have been scored', - pool.steps.find((s) => s.id === 'quality').value === expectedAvg, - `${pool.steps.find((s) => s.id === 'quality').value} vs ${expectedAvg} expected`); -record('...and says how many are not yet assessed', - /not yet assessed/.test(pool.steps.find((s) => s.id === 'scored').detail), - pool.steps.find((s) => s.id === 'scored').detail); -record('an unscored person reads as unscored, not as a zero score', - pool.items.filter((i) => i.value === 0).every((i) => /Not yet scored/.test(i.detail))); - -/* ── `candidates.quality` reports coverage beside quality ────────────────── */ - -const quality = resolverModule.resolveSkillData({ source: 'candidates.quality' }, FULL_CONTEXT); -record('candidate quality reports how much of the pool was actually scored', - quality.steps.find((s) => s.id === 'coverage').value < 100, - quality.steps.find((s) => s.id === 'coverage').detail); -record('score bands add up to the number scored', - quality.bands.reduce((n, b) => n + b.value, 0) - === SEED.JobApplication.filter((a) => a.ai_score > 0).length, - quality.bands.map((b) => `${b.label}=${b.value}`).join(', ')); - -/* ── Nothing here reaches for a record it was not given ─────────────────── */ - -/** - * A resolver that throws takes the page down; one that invents a default is - * worse, because it reports a figure nobody can trace. Every source is called - * with each collection missing in turn. - */ -const COLLECTIONS = ['positions', 'applications', 'interviews', 'staff', 'profiles', 'activity', 'shifts', 'assignments']; -let survived = true; -let culprit = ''; -for (const id of NEW_SOURCES) { - for (const drop of COLLECTIONS) { - const withoutOne = { ...FULL_CONTEXT }; - delete withoutOne[drop]; - try { - const out = resolverModule.resolveSkillData({ source: id }, withoutOne); - if (!out || typeof out.empty !== 'boolean') { survived = false; culprit = `${id} without ${drop}`; } - } catch (error) { - survived = false; - culprit = `${id} without ${drop}: ${error.message}`; - } - } -} -record('every source survives any single collection being absent', survived, culprit || `${NEW_SOURCES.length} sources × ${COLLECTIONS.length} collections`); - -/* ── 14. Analysis skills ────────────────────────────────────────────────── - * - * Thirteen definitions written against the sources built in the two previous - * phases. The failure they guard against is a definition that registers, looks - * complete, and answers nothing — a capability bound to a source that cannot be - * read, or a trigger that quietly takes a question another definition was - * written to answer. - * - * Every one is therefore executed, not merely parsed: each declared capability - * is resolved against the seeded workspace and has to come back with figures. - */ -console.log('\n── Analysis skills ──'); - -const ANALYSIS_SKILLS = [ - 'staffing-risk', 'attendance-analysis', 'overtime-analysis', 'candidate-analysis', - 'talent-pool-analysis', 'workforce-analytics', 'anomaly-detection', 'activity-analysis', - 'operational-risk', 'executive-summary', 'hiring-history-analysis', 'learning-analysis', - 'hiring-pulse-analysis', -]; - -record('every analysis skill registers', - ANALYSIS_SKILLS.every((id) => reg.SKILLS.some((s) => s.id === id)), - ANALYSIS_SKILLS.filter((id) => !reg.SKILLS.some((s) => s.id === id)).join(', ') || `${ANALYSIS_SKILLS.length} skills`); - -for (const id of ANALYSIS_SKILLS) { - const skill = reg.SKILLS.find((s) => s.id === id); - if (!skill) continue; - - /* Written in the one format, read by the one parser. */ - record(`\`${id}\` is a valid definition`, - reg.validateSkillSource(skill.markdown) === null, - reg.validateSkillSource(skill.markdown) || 'ok'); - - record(`\`${id}\` is an Owliver skill on real pages`, - skill.facets.includes('owliver') - && skill.pages.length > 0 - && skill.pages.every((p) => surfaces.SUPPORTED_SKILL_PAGES.includes(surfaces.canonicalPage(p) || p)), - JSON.stringify(skill.pages)); - - /* The sections the brief asks every definition to carry. `Purpose` and - `Capabilities` are parsed into fields; the rest are prose the authoring - flow will later read back, so they are checked for presence rather than - for shape. */ - record(`\`${id}\` states its purpose and capabilities`, - skill.purpose.length > 0 && skill.capabilities.length > 0, - `${skill.purpose.length} purpose, ${skill.capabilities.length} capabilities`); - - record(`\`${id}\` documents its data, analysis, output and limitations`, - ['## Data', '## Analysis', '## Output', '## Limitations'].every((h) => skill.body.includes(h)), - ['Data', 'Analysis', 'Output', 'Limitations'].filter((h) => !skill.body.includes(`## ${h}`)).join(', ') || 'all four'); - - /* Every capability resolves, and comes back with figures rather than an - apology. This is what separates a definition that works from one that - merely parses. */ - const failures = []; - for (const capability of skill.owliver.capabilities) { - const response = skill.owliver.responses[capability]; - if (!response) { failures.push(`${capability}: no response`); continue; } - - const declared = surfaces.dataSourceFor(response.source); - if (!declared) { failures.push(`${capability}: unknown source ${response.source}`); continue; } - - /* Shape has to be one the source actually supports, or the definition - promises a rendering the data cannot produce. */ - const shape = surfaces.shapeForCapability(capability); - if (shape && !surfaces.sourceSupportsShape(response.source, shape)) { - failures.push(`${capability}: ${response.source} cannot draw ${shape}`); - continue; - } - - if (declared.context) continue; /* needs a record; asked for at runtime */ - - const read = dataResolver.resolveSkillData(response, AGENT_SKILL_CONTEXT); - if (read?.unavailable) failures.push(`${capability}: unreadable`); - else if (read?.empty && !read.emptyNote) failures.push(`${capability}: empty with no explanation`); - } - record(`\`${id}\` answers every capability it declares`, - failures.length === 0, - failures.join(' | ') || `${skill.owliver.capabilities.length} capabilities`); - - /* Every suggestion names the capability it asks for. Without this a chip - falls through to the first declared capability, and two chips silently - become one answer — the lesson recorded in hiring-activity-assistant.md. */ - record(`\`${id}\` names a capability on every suggestion`, - skill.owliver.suggestions.every((s) => s.capability - && skill.owliver.capabilities.includes(s.capability)), - skill.owliver.suggestions.map((s) => `${s.label} → ${s.capability}`).join(' | ')); - - /* Its own triggers, claimed rather than inherited from its name. */ - record(`\`${id}\` claims its own triggers`, - skill.declaredTriggers && skill.triggers.length > 0, - JSON.stringify(skill.triggers)); -} - -/* ── Reachable from the pages they name ──────────────────────────────────── */ - -const PAGE_EXPECTATIONS = { - 'admin.controlCenter': ['executive-summary', 'staffing-risk', 'operational-risk', 'anomaly-detection', 'attendance-analysis', 'overtime-analysis', 'hiring-pulse-analysis'], - 'admin.positions': ['staffing-risk'], - 'admin.candidatesList': ['candidate-analysis'], - 'admin.hiredHistory': ['hiring-history-analysis'], - 'admin.talentPool': ['talent-pool-analysis'], - 'admin.forge': ['learning-analysis'], - 'admin.analytics': ['workforce-analytics', 'attendance-analysis', 'overtime-analysis', 'hiring-pulse-analysis'], - 'admin.activity': ['activity-analysis', 'anomaly-detection', 'operational-risk'], -}; - -for (const [contextId, expected] of Object.entries(PAGE_EXPECTATIONS)) { - const available = reg.skillsForContext(contextId, [], []).map((s) => s.id); - record(`${contextId.replace('admin.', '')} offers its analysis skills`, - expected.every((id) => available.includes(id)), - expected.filter((id) => !available.includes(id)).join(', ') || `${expected.length} available`); -} - -/** - * Profile keeps none, and that is correct. - * - * There is no data source about an account, so a skill there would be a - * placeholder. The emptiness is pinned so a later phase cannot quietly fill it - * to make a list look complete. - */ -record('the Profile page still carries no analysis skill', - reg.skillsForContext('admin.profile', [], []).length === 0, - JSON.stringify(reg.skillsForContext('admin.profile', [], []).map((s) => s.id))); - -/* ── Triggers reach their skill, and take nothing that was not theirs ────── */ - -const TRIGGER_CASES = [ - ['admin.positions', 'Which roles are at risk?', 'staffing-risk'], - ['admin.analytics', 'How is attendance this month?', 'attendance-analysis'], - ['admin.analytics', 'How much overtime are we running?', 'overtime-analysis'], - ['admin.candidatesList', 'What is the candidate quality like?', 'candidate-analysis'], - ['admin.talentPool', 'How healthy is the talent pool?', 'talent-pool-analysis'], - ['admin.analytics', 'Show me workforce coverage', 'workforce-analytics'], - ['admin.activity', 'Is there anything unusual?', 'anomaly-detection'], - ['admin.activity', 'Give me an event breakdown', 'activity-analysis'], - ['admin.controlCenter', 'What is the operational risk?', 'operational-risk'], - ['admin.controlCenter', 'Give me an executive summary', 'executive-summary'], - ['admin.hiredHistory', 'What is our time to hire?', 'hiring-history-analysis'], - ['admin.forge', 'How is training progress?', 'learning-analysis'], - ['admin.controlCenter', 'What is the hiring pulse?', 'hiring-pulse-analysis'], -]; - -for (const [contextId, question, expected] of TRIGGER_CASES) { - const matched = reg.matchSkill(question, contextId, [], []); - record(`"${question}" reaches \`${expected}\``, - matched?.id === expected, matched?.id || 'no match'); -} - -/** - * The check that protects everything already shipped. - * - * Thirteen new definitions across shared pages is the likeliest way to take a - * question that an existing definition — or a page's own reader — was answering. - * Every baseline question is replayed on every page: whatever answered it before - * must still answer it. - */ -const stolen = []; -for (const contextId of Object.keys(expectedBaseline.contexts)) { - const before = expectedBaseline.contexts[contextId]; - for (const intent of before.intents) { - const now = reg.matchSkill(intent.question, contextId, [], [])?.id ?? null; - if (now !== intent.matchedSkill) { - stolen.push(`${contextId} "${intent.question}": ${intent.matchedSkill ?? 'page reader'} → ${now}`); - } - } -} -record('no new skill takes a question something else was answering', - stolen.length === 0, stolen.join(' | ') || `${Object.keys(expectedBaseline.contexts).length} contexts replayed`); - -/* ── The format stays suitable for non-technical authoring ──────────────── */ - -/** - * Every analysis definition round-trips through the field writer. - * - * The Markdown is the canonical representation, and a future authoring flow - * will compose it rather than replace it. That only holds if a definition can - * be read into fields and written back without losing what it said — so the - * property is asserted now, while there are thirteen definitions to test it - * against, rather than discovered later. - */ -const lossy = []; -for (const id of ANALYSIS_SKILLS) { - const skill = reg.SKILLS.find((s) => s.id === id); - if (!skill) continue; - const rewritten = fields.patchFrontmatter(skill.markdown, { description: skill.description }); - const reparsedSkill = reg.parseSkill(rewritten, { custom: true }); - if (reparsedSkill.body !== skill.body) lossy.push(`${id}: body`); - if (JSON.stringify(reparsedSkill.owliver) !== JSON.stringify(skill.owliver)) lossy.push(`${id}: owliver`); - if (JSON.stringify(reparsedSkill.pages) !== JSON.stringify(skill.pages)) lossy.push(`${id}: pages`); -} -record('an analysis definition survives being written back through the field writer', - lossy.length === 0, lossy.join(', ') || `${ANALYSIS_SKILLS.length} definitions`); - -record('every analysis skill declares a category for grouping', - ANALYSIS_SKILLS.every((id) => reg.SKILLS.find((s) => s.id === id)?.category), - [...new Set(ANALYSIS_SKILLS.map((id) => reg.SKILLS.find((s) => s.id === id)?.category))].join(', ')); - -/* ── 15. Agent registry ─────────────────────────────────────────────────── - * - * Agents are the layer above skills, and they are read by the *same* parser — - * `parseAgent` imports `parseFrontmatter` and the section readers from the - * skill registry rather than reimplementing them. These checks exist to keep - * that true, and to keep an agent honest about what it carries: an agent that - * names a skill nobody provides is a capability promised and not delivered, - * and it fails silently. - */ -console.log('\n── Agent registry ──'); - -const agentReg = await server.ssrLoadModule('/src/lib/agents/registry.js'); -const agentFields = await server.ssrLoadModule('/src/lib/agents/agentFields.js'); -const customAgents = await server.ssrLoadModule('/src/lib/agents/customAgents.js'); -const vocab = await server.ssrLoadModule('/src/lib/agents/vocabulary.js'); - -const agentFiles = readdirSync(join(ROOT, 'src/agents')).filter((f) => f.endsWith('.md')); - - -record('every .md under src/agents registers', - agentReg.AGENTS.length === agentFiles.length, - `${agentReg.AGENTS.length} registered / ${agentFiles.length} files`); - -record('no two agents share an id', - new Set(agentReg.AGENTS.map((a) => a.id)).size === agentReg.AGENTS.length); - -const shippedAgents = agentReg.readAgentRegistry([]); - -record('shipped agent registry reports no diagnostics', - shippedAgents.diagnostics.length === 0, - shippedAgents.diagnostics.map((d) => d.message).join(' | ') || 'none'); - -record('every shipped agent registered every field it declared', - agentReg.AGENTS.every((a) => !a.errors?.length), - agentReg.AGENTS.flatMap((a) => a.errors || []).join(' | ') || 'none'); - -/* The nine the product ships: one per Krow page, plus the root. */ -const EXPECTED_AGENTS = { - 'krow-workforce-agent': null, // covers every page - 'control-center-agent': 'control-center', - 'positions-agent': 'positions', - 'candidates-agent': 'candidates', - 'hired-history-agent': 'hired-history', - 'talent-pool-agent': 'talent-pool', - 'krow-forge-agent': 'krow-forge', - 'analytics-agent': 'analytics', - 'activity-agent': 'activity', -}; - -for (const [id, page] of Object.entries(EXPECTED_AGENTS)) { - const agent = agentReg.getAgent(agentReg.AGENTS, id); - record(`\`${id}\` is registered and published`, - Boolean(agent) && agent.status === 'published', - agent ? agent.status : 'MISSING'); - if (agent && page) { - record(`\`${id}\` covers \`${page}\``, agent.pages.includes(page), - JSON.stringify(agent.pages)); - } -} - -/* The root agent reaches every surface a skill may name, so it can stand in on - a page whose own agent carries nothing. */ -const root = agentReg.getAgent(agentReg.AGENTS, 'krow-workforce-agent'); -record('the root agent covers every supported page', - surfaces.SUPPORTED_SKILL_PAGES.every((p) => root.pages.includes(p)), - `${root.pages.length}/${surfaces.SUPPORTED_SKILL_PAGES.length}`); -record('the root agent carries the other eight as subagents', - root.subagents.length === 8 - && root.subagents.every((s) => s !== root.id && EXPECTED_AGENTS[s] !== undefined), - JSON.stringify(root.subagents)); - -/* Every address an agent states must resolve. */ -const registeredSkillIds = new Set(reg.SKILLS.map((s) => s.id)); -const registeredAgentIds = new Set(agentReg.AGENTS.map((a) => a.id)); - -record('every skill an agent names exists in the skill registry', - agentReg.AGENTS.every((a) => a.skills.every((s) => registeredSkillIds.has(s))), - agentReg.AGENTS.flatMap((a) => a.skills.filter((s) => !registeredSkillIds.has(s))).join(', ') || 'all resolve'); - -record('every subagent an agent names exists', - agentReg.AGENTS.every((a) => a.subagents.every((s) => registeredAgentIds.has(s)))); - -record('every page an agent names is a real surface', - agentReg.AGENTS.every((a) => a.pages.every((p) => surfaces.SUPPORTED_SKILL_PAGES.includes(p))), - agentReg.AGENTS.flatMap((a) => a.pages.filter((p) => !surfaces.SUPPORTED_SKILL_PAGES.includes(p))).join(', ') || 'all resolve'); - -/** - * No agent carries a placeholder skill. - * - * This replaces an earlier check that pinned four agents at zero skills, which - * was the honest assertion while no skill existed for their pages: the - * temptation when building an agent UI is to invent one so the list looks - * populated. Those skills now exist and are real, so pinning zero would be - * pinning the wrong thing — but the property worth protecting is unchanged, and - * this states it directly instead of by proxy. - * - * A skill is real when it declares a capability whose response binds to a - * registered data source, and that source resolves against the seeded workspace - * without reporting itself unavailable. A definition that names a source the - * product does not have, or one that cannot be read, is a promise the agent - * cannot keep. - */ -const skillById = new Map(reg.SKILLS.map((s) => [s.id, s])); -const placeholders = []; -const unreadable = []; - -for (const agent of agentReg.AGENTS) { - for (const skillId of agent.skills) { - const skill = skillById.get(skillId); - if (!skill) continue; /* already reported as `unattached` above */ - - const responses = Object.values(skill.owliver?.responses || {}); - const sources = responses.map((r) => r.source).filter(Boolean); - - /** - * Two kinds of real skill, and only one of them reads data. - * - * An *analysis* skill binds capabilities to data sources. An *action* skill - * — `create-position`, `forge-skill-management` — declares actions or a - * guided conversation instead, and carries no responses at all. Both are - * real; a definition that does neither is the placeholder this looks for. - */ - const doesSomething = sources.length > 0 - || (skill.actions || []).length > 0 - || (skill.conversation || []).length > 0; - - if (!doesSomething || !sources.every((src) => surfaces.dataSourceFor(src))) { - placeholders.push(`${agent.id}/${skillId}`); - continue; - } - - for (const response of responses) { - const declared = surfaces.dataSourceFor(response.source); - /* A source that needs a position or a candidate in context is *correctly* - unavailable when it is handed neither — `resolveEntity` asks the reader - which one at runtime. Only workspace-level sources can be read cold, - so only those are asserted here. */ - if (declared?.context) continue; - - const read = dataResolver.resolveSkillData(response, AGENT_SKILL_CONTEXT); - /* `unavailable` means the resolver has no reading for that source at all. - `empty` is fine and honest — the source read successfully and found - nothing. */ - if (read?.unavailable) unreadable.push(`${agent.id}/${skillId}:${response.source}`); - } - } -} - -record('no agent carries a skill without a real data source', - placeholders.length === 0, placeholders.join(', ') || 'every attached skill declares one'); - -record('every skill an agent carries reads a source the product can resolve', - unreadable.length === 0, unreadable.join(', ') || 'all resolve'); - -/* Every page agent now carries at least one skill of its own, except where the - product genuinely has none to give it. Stated as data rather than as a - number, so a future page with no source is visible rather than assumed. */ -const withoutSkills = agentReg.AGENTS.filter((a) => a.skills.length === 0).map((a) => a.id); -record('every agent that has a skill available to it carries one', - withoutSkills.length === 0, withoutSkills.join(', ') || 'all nine agents carry skills'); - -/* ── Diagnostics: nothing half-loads in silence ──────────────────────────── */ - -const UNATTACHED = `---\nid: ghost-agent\nname: Ghost Agent\npages:\n - positions\nskills:\n - no-such-skill\n---\n\n# Ghost\n`; -const withUnattached = agentReg.readAgentRegistry([{ path: 'custom/ghost.md', raw: UNATTACHED }]); -record('an agent naming a missing skill is reported', - withUnattached.diagnostics.some((d) => d.kind === 'unattached' && d.agentId === 'ghost-agent'), - withUnattached.diagnostics.find((d) => d.kind === 'unattached')?.message ?? 'NO DIAGNOSTIC'); - -const BROKEN_AGENT = `---\nid: broken\n name: bad indent\n---\n# Broken\n`; -const withBrokenAgent = agentReg.readAgentRegistry([{ path: 'custom/broken.md', raw: BROKEN_AGENT }]); -record('an unreadable stored agent is reported, not silently dropped', - withBrokenAgent.diagnostics.some((d) => d.kind === 'unreadable'), - withBrokenAgent.diagnostics.find((d) => d.kind === 'unreadable')?.message ?? 'NO DIAGNOSTIC'); - -const SHADOW_AGENT = `---\nid: positions-agent\nname: My Positions Agent\npages:\n - positions\n---\n\n# Shadow\n`; -const withShadowAgent = agentReg.readAgentRegistry([{ path: 'custom/shadow.md', raw: SHADOW_AGENT }]); -record('a stored agent overriding a built-in is reported', - withShadowAgent.diagnostics.some((d) => d.kind === 'shadowed' && d.agentId === 'positions-agent'), - withShadowAgent.diagnostics.find((d) => d.kind === 'shadowed')?.message ?? 'NO DIAGNOSTIC'); - -/* A definition with one bad field keeps the rest AND says so. */ -const PARTIAL_AGENT = `---\nid: partial-agent\nname: Partial Agent\npages:\n - positions\nreasoning: telepathy\n---\n\n# Partial\n`; -const withPartialAgent = agentReg.readAgentRegistry([{ path: 'custom/partial.md', raw: PARTIAL_AGENT }]); -const partialAgent = withPartialAgent.agents.find((a) => a.id === 'partial-agent'); -record('an agent with one bad field still registers', - Boolean(partialAgent) && partialAgent.pages.includes('positions')); -record('...falls back to the documented default', - partialAgent?.reasoning === 'balanced', partialAgent?.reasoning); -record('...and the field it lost is reported', - withPartialAgent.diagnostics.some((d) => d.kind === 'incomplete' && d.agentId === 'partial-agent'), - withPartialAgent.diagnostics.find((d) => d.kind === 'incomplete')?.message ?? 'NO DIAGNOSTIC'); - -/* A cycle would make subagent resolution non-terminating. */ -const SELF_SUB = `---\nid: loop-agent\nname: Loop Agent\npages:\n - positions\nsubagents:\n - loop-agent\n---\n\n# Loop\n`; -const looped = agentReg.parseAgent(SELF_SUB, { custom: true }); -record('an agent cannot be its own subagent', - looped.subagents.length === 0 && looped.errors.length > 0, - looped.errors[0] || 'NOT REPORTED'); - -/* ── Validation: what the editor refuses ─────────────────────────────────── */ - -const REFUSALS = [ - ['an empty definition', ''], - ['a malformed id', `---\nid: Not An Id\nname: Bad\npages:\n - positions\n---\n# x\n`], - ['no name', `---\nid: no-name\npages:\n - positions\n---\n# x\n`], - ['no pages', `---\nid: no-pages\nname: No Pages\n---\n# x\n`], - ['an unsupported page', `---\nid: bad-page\nname: Bad Page\npages:\n - the-moon\n---\n# x\n`], - ['an unsupported reasoning mode', `---\nid: bad-reason\nname: Bad Reason\npages:\n - positions\nreasoning: vibes\n---\n# x\n`], - ['an unsupported permission role', `---\nid: bad-role\nname: Bad Role\npages:\n - positions\npermissions:\n people:\n - user: a@b.com\n role: emperor\n---\n# x\n`], -]; - -for (const [label, source] of REFUSALS) { - record(`validateAgentSource refuses ${label}`, - Boolean(agentReg.validateAgentSource(source)), - agentReg.validateAgentSource(source) || 'ACCEPTED'); -} - -/** - * An agent that omits `id:` derives one from its name. - * - * The same fallback `parseSkill` applies, and deliberately not a refusal: an - * explicit id is an address other definitions refer to, so writing one is - * encouraged, but leaving it out means "call it after its name" rather than - * "this file is broken". - */ -const DERIVED_ID = `---\nname: Derived Id Agent\npages:\n - positions\n---\n\n# D\n`; -record('an agent with no `id:` derives one from its name', - agentReg.parseAgent(DERIVED_ID, { custom: true }).id === 'derived-id-agent' - && agentReg.validateAgentSource(DERIVED_ID) === null, - agentReg.parseAgent(DERIVED_ID, { custom: true }).id); - -record('an agent with neither id nor name is refused', - Boolean(agentReg.validateAgentSource(`---\npages:\n - positions\n---\n\n# x\n`))); - -record('validateAgentSource accepts a minimal agent', - agentReg.validateAgentSource(`---\nid: minimal\nname: Minimal\npages:\n - positions\n---\n\n# Minimal\n`) === null); - -/** - * An agent carrying no skills is accepted. - * - * Deliberately not a refusal, unlike a skill that declares no capabilities. - * A skill with nothing to say cannot answer; an agent with no skills still has - * its page's own reader — which is exactly how Control Center, Hired History, - * Talent Pool and Activity answer today. - */ -record('validateAgentSource accepts an agent with no skills', - agentReg.validateAgentSource(`---\nid: skill-less\nname: Skill-less\npages:\n - control-center\n---\n\n# S\n`) === null); - -/* ── Fields ⇄ definition, through the existing writer ────────────────────── */ - -const rootSource = root.markdown; -const patched = agentFields.applyAgentFields(rootSource, { - name: 'Renamed Agent', - permissions: { - owner: 'owner@krow.app', - access: 'specific', - people: [{ user: 'a@krow.app', role: 'editor' }, { user: 'b@krow.app', role: 'viewer' }], - }, - knowledge: [{ id: 'policy', label: 'Overtime policy', kind: 'note', body: 'Beyond 20h needs sign-off.' }], -}); -const reparsed = agentReg.parseAgent(patched, { custom: true }); - -/* The claim the whole editor rests on: `patchFrontmatter` already writes - nested block maps and `- key: value` sequences, so agents needed no second - writer. Asserted rather than assumed. */ -record('a nested `permissions.people` list round-trips through the existing writer', - JSON.stringify(reparsed.permissions.people) - === JSON.stringify([{ user: 'a@krow.app', role: 'editor' }, { user: 'b@krow.app', role: 'viewer' }]), - JSON.stringify(reparsed.permissions.people)); - -record('a nested `knowledge` entry round-trips too', - reparsed.knowledge.length === 1 && reparsed.knowledge[0].body === 'Beyond 20h needs sign-off.', - JSON.stringify(reparsed.knowledge)); - -record('patching one field leaves the body untouched', - reparsed.instructions === root.instructions); - -record('patching one field leaves the others untouched', - JSON.stringify(reparsed.skills) === JSON.stringify(root.skills) - && reparsed.subagents.length === root.subagents.length, - `${reparsed.skills.length} skills, ${reparsed.subagents.length} subagents`); - -record('the patched definition still validates', - agentReg.validateAgentSource(patched) === null, - agentReg.validateAgentSource(patched) || 'ok'); - -record('fields read back out match what was written in', - agentFields.agentFieldsFromSource(patched).permissions.access === 'specific'); - -/* ── Storage round trip ──────────────────────────────────────────────────── */ - -const template = customAgents.agentTemplate({ id: 'my-agent', name: 'My Agent', pages: ['positions'] }); -record('a fresh agent template validates', - agentReg.validateAgentSource(template) === null, - agentReg.validateAgentSource(template) || 'ok'); -record('a fresh agent is a draft, never published', - agentReg.parseAgent(template, { custom: true }).status === 'draft', - agentReg.parseAgent(template, { custom: true }).status); - -const storedAgent = customAgents.upsertCustomAgent([], template); -record('an authored agent stores as its own Markdown', - storedAgent.next.length === 1 && storedAgent.next[0].raw === template); -record('...and reads back with its id intact', - agentReg.parseAgent(storedAgent.next[0].raw, { custom: true }).id === 'my-agent'); - -const restored = customAgents.upsertCustomAgent(storedAgent.next, template); -record('re-saving an agent replaces its entry rather than duplicating it', - restored.next.length === 1, `${restored.next.length} stored`); - -record('customAgentSource finds a stored definition', - customAgents.customAgentSource(storedAgent.next, 'my-agent') === template); - -record('removeCustomAgent drops exactly one entry', - customAgents.removeCustomAgent(storedAgent.next, 'my-agent').length === 0); - -/* An unparseable storedAgent entry must not take its neighbours with it. */ -const mixed = [{ path: 'custom/bad.md', raw: BROKEN_AGENT }, ...storedAgent.next]; -record('a broken stored agent does not remove the good ones', - customAgents.removeCustomAgent(mixed, 'nobody').length === 2); - -/* ── Search ─────────────────────────────────────────────────────────────── */ - -record('an empty search returns every agent', - agentReg.searchAgents(agentReg.AGENTS, '').length === agentReg.AGENTS.length); -record('search matches on name', - agentReg.searchAgents(agentReg.AGENTS, 'analytics').some((a) => a.id === 'analytics-agent')); -record('search matches on what an agent is for', - agentReg.searchAgents(agentReg.AGENTS, 'audit trail').some((a) => a.id === 'activity-agent')); -record('search that matches nothing returns nothing', - agentReg.searchAgents(agentReg.AGENTS, 'zzzznope').length === 0); - -/* ── Vocabulary is closed ───────────────────────────────────────────────── */ - -record('every agent names an icon the product has', - agentReg.AGENTS.every((a) => vocab.AGENT_ICONS.includes(a.icon)), - agentReg.AGENTS.map((a) => a.icon).join(', ')); -record('every agent names a supported reasoning mode', - agentReg.AGENTS.every((a) => vocab.SUPPORTED_REASONING.includes(a.reasoning))); -record('every agent has a version of at least 1', - agentReg.AGENTS.every((a) => Number.isInteger(a.version) && a.version >= 1)); -record('every agent states when to use it', - agentReg.AGENTS.every((a) => a.trigger.length > 0)); -record('every agent states its instructions', - agentReg.AGENTS.every((a) => a.instructions.length > 0)); - -/* ── The parser is the skill parser ─────────────────────────────────────── */ - -/* A BOM, CRLF line endings, a blank line above the fence and trailing spaces - after it — the four things that used to take a skill definition down. An - agent gets the same tolerance for free, because it is the same function. */ -const HOSTILE = `\r\n\r\n--- \r\nid: hostile-agent\r\nname: Hostile Agent\r\npages:\r\n - positions\r\n--- \r\n\r\n# Hostile\r\n\r\n## Instructions\r\n\r\nStill readable.\r\n`; -const hostile = agentReg.parseAgent(HOSTILE, { custom: true }); -record('an agent survives a BOM, CRLF, a leading blank line and trailing spaces', - hostile.id === 'hostile-agent' && hostile.pages.includes('positions'), - `${hostile.id} / ${JSON.stringify(hostile.pages)}`); -record('...and its body still reads', - hostile.instructions.includes('Still readable.'), - hostile.instructions || 'LOST'); - -/* ── 16. Page boundary: runtime, knowledge and tools ────────────────────── - * - * The one property the whole design rests on: **an agent narrows a page and can - * never widen it.** Every layer added on top — data, knowledge, tools — has to - * inherit that, or selecting an agent becomes a way around the page boundary. - * - * It is asserted exhaustively rather than by example. Every agent is tried on - * every page, and the scoped result must be a subset of what the page offers - * with no agent at all. A subset cannot contain something the page did not have, - * so this is a proof rather than a spot-check. - */ -console.log('\n── Page boundary: runtime, knowledge and tools ──'); - -const runtime = await server.ssrLoadModule('/src/lib/agents/runtime.js'); -const contexts = await server.ssrLoadModule('/src/components/ai-assistant/contexts.js'); -const knowledge = await server.ssrLoadModule('/src/lib/agents/knowledge.js'); -const tools = await server.ssrLoadModule('/src/lib/skills/tools.js'); -const agentContext = await server.ssrLoadModule('/src/lib/agents/context.js'); -const routingModule = await server.ssrLoadModule('/src/components/ai-assistant/routing.js'); -const actions = await server.ssrLoadModule('/src/lib/skills/actions.js'); - -const ALL_AGENTS = agentReg.AGENTS; -const ALL_SKILLS = reg.SKILLS; -const PAGE_CONTEXTS = Object.keys(expectedBaseline.contexts); - -/* ── The invariant, over every page × agent pair ─────────────────────────── */ - -const widened = []; -const scopedCounts = []; - -for (const contextId of PAGE_CONTEXTS) { - const unscoped = reg.skillsForContext(contextId, [], []).map((s) => s.id); - - for (const agent of ALL_AGENTS) { - const disabled = runtime.agentScopedDisabledWith(agent, ALL_AGENTS, ALL_SKILLS, []); - const scoped = reg.skillsForContext(contextId, disabled, []).map((s) => s.id); - - const extra = scoped.filter((id) => !unscoped.includes(id)); - if (extra.length) widened.push(`${contextId} + ${agent.id}: ${JSON.stringify(extra)}`); - scopedCounts.push(scoped.length); - } -} - -record(`no agent widens any page (${PAGE_CONTEXTS.length} pages × ${ALL_AGENTS.length} agents)`, - widened.length === 0, - widened.slice(0, 3).join(' | ') || `${PAGE_CONTEXTS.length * ALL_AGENTS.length} combinations, every result a subset`); - -/* With no agent at all, nothing is withheld — the pre-agent behaviour. */ -record('no agent selected withholds nothing', - PAGE_CONTEXTS.every((contextId) => - JSON.stringify(reg.skillsForContext(contextId, runtime.agentScopedDisabled(null, ALL_SKILLS, []), []).map((s) => s.id)) - === JSON.stringify(reg.skillsForContext(contextId, [], []).map((s) => s.id)))); - -record('`agentScopedDisabled(null, …)` returns its input untouched', - JSON.stringify(runtime.agentScopedDisabled(null, ALL_SKILLS, ['a', 'b'])) === JSON.stringify(['a', 'b'])); - -/* ── The three named cross-boundary cases ────────────────────────────────── */ - -const CROSS_CASES = [ - ['Positions page + Analytics Agent', 'admin.positions', 'analytics-agent'], - ['Candidates page + Krow Workforce Agent', 'admin.candidatesList', 'krow-workforce-agent'], - ['Analytics page + Positions Agent', 'admin.analytics', 'positions-agent'], -]; - -for (const [label, contextId, agentId] of CROSS_CASES) { - const agent = agentReg.getAgent(ALL_AGENTS, agentId); - const pageOnly = reg.skillsForContext(contextId, [], []).map((s) => s.id); - const disabled = runtime.agentScopedDisabledWith(agent, ALL_AGENTS, ALL_SKILLS, []); - const scoped = reg.skillsForContext(contextId, disabled, []).map((s) => s.id); - - /* Data. */ - record(`${label}: reads nothing the page does not already offer`, - scoped.every((id) => pageOnly.includes(id)), - `page offers ${JSON.stringify(pageOnly)}, agent sees ${JSON.stringify(scoped)}`); - - /* Tools. */ - const pageTools = tools.toolsForContext(contextId, [], []).map((t) => t.name); - const agentTools = tools.toolsForContext(contextId, disabled, []).map((t) => t.name); - record(`${label}: reaches no tool the page does not already offer`, - agentTools.every((name) => pageTools.includes(name)), - `page offers ${JSON.stringify(pageTools)}, agent reaches ${JSON.stringify(agentTools)}`); - - /* Knowledge. */ - const covers = runtime.agentCovers(agent, contextId); - const read = knowledge.retrieveKnowledge({ - agent, contextId, question: 'What does the scope note say about pages?', - }); - if (!covers) { - record(`${label}: retrieves no knowledge, because the agent is constrained here`, - read.available === false && read.passages.length === 0, - read.note || 'NO NOTE'); - } else { - record(`${label}: knowledge stays inside the agent's own entries`, - read.passages.every((p) => (agent.knowledge || []).some((k) => k.id === p.documentId)), - `${read.passages.length} passage(s)`); - } - - /* Routing. */ - const intent = routingModule.resolveIntent({ - question: 'What needs my attention?', - contextId, - disabledSkills: disabled, - agent, - agentCoversPage: covers, - agentSuggestion: runtime.defaultAgentForContext(ALL_AGENTS, contextId), - }); - record(`${label}: ${covers ? 'answers from this page' : 'declines honestly instead of reaching'}`, - covers ? intent.kind !== 'constrained' : intent.kind === 'constrained', - `kind=${intent.kind}`); -} - -/** - * A constrained agent must not answer *from the pages it does cover*. - * - * The subtlest way to break the boundary: Analytics Agent on Positions could - * plausibly answer an analytics question "because that is what it is for". It - * must not — the page is the boundary, and an agent is a lens on the page in - * front of the reader, never a route to a different one. - */ -const analyticsAgent = agentReg.getAgent(ALL_AGENTS, 'analytics-agent'); -const leaked = routingModule.resolveIntent({ - question: 'How much overtime are we running?', - contextId: 'admin.positions', - disabledSkills: runtime.agentScopedDisabledWith(analyticsAgent, ALL_AGENTS, ALL_SKILLS, []), - agent: analyticsAgent, - agentCoversPage: false, - agentSuggestion: agentReg.getAgent(ALL_AGENTS, 'positions-agent'), -}); -record('a constrained agent does not answer from the pages it covers elsewhere', - leaked.kind === 'constrained' && !leaked.skill, - `kind=${leaked.kind}, skill=${leaked.skill?.id ?? 'none'}`); - -record('...and says which agent belongs here instead', - JSON.stringify(leaked.doc).includes('Positions Agent')); - -/* ── Tools ──────────────────────────────────────────────────────────────── */ - -record('every action a handler exists for is described', - tools.undescribedActions().length === 0, - tools.undescribedActions().join(', ') || `${tools.TOOL_NAMES.length} tools described`); - -record('a tool that writes a record requires approval', - tools.toolRequiresApproval('create_position') === true); - -record('a tool that only navigates does not', - ['navigate_to_positions', 'navigate_to_analytics', 'open_related_page'] - .every((name) => tools.toolRequiresApproval(name) === false)); - -record('every mutating tool requires approval', - tools.TOOLS.filter((t) => t.mutates).every((t) => t.requiresApproval), - tools.TOOLS.filter((t) => t.mutates).map((t) => t.name).join(', ') || 'none mutate'); - -record('every read-only tool is honest about mutating nothing', - tools.TOOLS.filter((t) => t.readOnly).every((t) => t.mutates === null)); - -/* A tool is reachable only through a skill on this page that declares it. */ -const undeclaredTools = []; -for (const contextId of PAGE_CONTEXTS) { - const declared = new Set( - reg.skillsForContext(contextId, [], []).flatMap((s) => s.actions || []) - ); - for (const tool of tools.toolsForContext(contextId, [], [])) { - if (!declared.has(tool.name)) undeclaredTools.push(`${contextId}: ${tool.name}`); - } -} -record('a tool is reachable only where a skill on that page declares it', - undeclaredTools.length === 0, undeclaredTools.join(', ') || 'every tool traced to a declaring skill'); - -/** - * The real action still refuses what a skill did not declare. - * - * `runAction` gated on `skill.actions` before any of this existed, and the tool - * layer describes actions rather than performing them — so the gate must be - * exactly where it was. - */ -const positionsSkill = reg.SKILLS.find((s) => s.id === 'staffing-risk'); -record('runAction still refuses an action the skill did not declare', - actions.runAction('create_position', { skill: positionsSkill, draft: {}, status: 'draft' }) === null); - -const creator = reg.SKILLS.find((s) => s.id === 'create-position'); -record('...and still performs one it did', - actions.runAction('create_position', { skill: creator, draft: { title: 'X' }, status: 'draft' })?.type - === 'create_position'); - -record('open_related_page resolves only addresses the product has', - actions.runAction('open_related_page', { skill: { actions: ['open_related_page'] }, page: 'positions' })?.route - === '/admin/positions' - && actions.runAction('open_related_page', { skill: { actions: ['open_related_page'] }, page: 'the-moon' }) === null); - -/* ── Knowledge ──────────────────────────────────────────────────────────── */ - -record('an agent with no knowledge says so rather than returning nothing', - (() => { - const bare = agentReg.getAgent(ALL_AGENTS, 'activity-agent'); - const read = knowledge.retrieveKnowledge({ agent: bare, contextId: 'admin.activity', question: 'policy' }); - return read.available === false && /no knowledge attached/i.test(read.note || ''); - })()); - -record('no agent is active means no knowledge, stated honestly', - (() => { - const read = knowledge.retrieveKnowledge({ agent: null, question: 'anything' }); - return read.available === false && read.passages.length === 0 && Boolean(read.note); - })()); - -const rootAgent = agentReg.getAgent(ALL_AGENTS, 'krow-workforce-agent'); -const found = knowledge.retrieveKnowledge({ - agent: rootAgent, contextId: 'admin.positions', question: 'What can this agent see across pages?', -}); -record('an agent with knowledge retrieves from its own entries', - found.available === true && found.passages.length > 0, - `${found.passages.length} passage(s)`); - -record('every passage names the document it came from', - found.passages.every((p) => p.documentId && p.chunkId && p.text)); - -record('a question the knowledge does not cover returns nothing, and says so', - (() => { - const miss = knowledge.retrieveKnowledge({ - agent: rootAgent, contextId: 'admin.positions', question: 'zzzz quantum bicycles', - }); - return miss.available === true && miss.passages.length === 0 && Boolean(miss.note); - })()); - -record('no knowledge document is invented', - ALL_AGENTS.every((a) => knowledge.knowledgeDocuments(a) - .every((d) => (a.knowledge || []).some((k) => k.id === d.id))), - `${ALL_AGENTS.reduce((n, a) => n + knowledge.knowledgeDocuments(a).length, 0)} declared documents in total`); - -/* ── Question classification ────────────────────────────────────────────── */ - -record('a question about records is structured, never retrieval', - runtime.classifyQuestion({ question: 'Which employees worked more than 20 overtime hours?' }) === 'structured'); - -record('a question about a document is knowledge', - runtime.classifyQuestion({ question: 'What does our overtime policy say?' }) === 'knowledge'); - -record('a question needing both is combined', - runtime.classifyQuestion({ question: 'Which employees exceeded the overtime policy this month?' }) === 'combined'); - -record('an ambiguous question stays structured rather than guessing at retrieval', - runtime.classifyQuestion({ question: 'How is attendance?' }) === 'structured'); - -/* ── Coverage and defaults ──────────────────────────────────────────────── */ - -const NATIVE = { - 'admin.controlCenter': 'control-center-agent', - 'admin.positions': 'positions-agent', - 'admin.candidatesList': 'candidates-agent', - 'admin.hiredHistory': 'hired-history-agent', - 'admin.talentPool': 'talent-pool-agent', - 'admin.forge': 'krow-forge-agent', - 'admin.analytics': 'analytics-agent', - 'admin.activity': 'activity-agent', -}; -for (const [contextId, expected] of Object.entries(NATIVE)) { - record(`${contextId.replace('admin.', '')} opens on its own agent`, - runtime.defaultAgentForContext(ALL_AGENTS, contextId)?.id === expected, - runtime.defaultAgentForContext(ALL_AGENTS, contextId)?.id || 'none'); -} - -record('every page has an agent to open with', - PAGE_CONTEXTS.every((contextId) => runtime.defaultAgentForContext(ALL_AGENTS, contextId)), - PAGE_CONTEXTS.filter((c) => !runtime.defaultAgentForContext(ALL_AGENTS, c)).join(', ') || 'all covered'); - -record('a requested agent is never silently swapped for another', - (() => { - const turn = runtime.resolveAgentForTurn(ALL_AGENTS, 'analytics-agent', 'admin.positions'); - return turn.agent.id === 'analytics-agent' && turn.covers === false && turn.suggestion?.id === 'positions-agent'; - })()); - -/* ── Subagents ──────────────────────────────────────────────────────────── */ - -record('subagent skills are inherited one level deep', - runtime.agentSkillIds(rootAgent, ALL_AGENTS).length >= rootAgent.skills.length, - `${rootAgent.skills.length} own → ${runtime.agentSkillIds(rootAgent, ALL_AGENTS).length} with subagents`); - -record('an unpublished subagent contributes nothing', - (() => { - const draftSub = { ...agentReg.getAgent(ALL_AGENTS, 'analytics-agent'), status: 'draft' }; - const others = ALL_AGENTS.map((a) => (a.id === 'analytics-agent' ? draftSub : a)); - const withDraft = runtime.agentSkillIds(rootAgent, others); - const withPublished = runtime.agentSkillIds(rootAgent, ALL_AGENTS); - return withDraft.length <= withPublished.length; - })()); - -record('a subagent cycle terminates', - (() => { - const a = { id: 'a', skills: ['s1'], subagents: ['b'], status: 'published' }; - const b = { id: 'b', skills: ['s2'], subagents: ['a'], status: 'published' }; - return runtime.agentSkillIds(a, [a, b]).length === 2; - })()); - -/* ── Starters ───────────────────────────────────────────────────────────── */ - -record('an agent offers no starters on a page it does not cover', - runtime.agentStarters(analyticsAgent, 'admin.positions').length === 0); - -record('an agent offers its starters on a page it does cover', - runtime.agentStarters(analyticsAgent, 'admin.analytics').length > 0, - runtime.agentStarters(analyticsAgent, 'admin.analytics').map((s) => s.label).join(' | ')); - -record('a starter names no capability, so it cannot address an unoffered skill', - ALL_AGENTS.every((a) => runtime.agentStarters(a, null).every((s) => s.capability === null))); - -/* ── Context envelope ───────────────────────────────────────────────────── */ - -const envelope = agentContext.buildOwliverContext({ - context: { id: 'admin.positions', page: 'Positions' }, - pathname: '/admin/positions', - pageContext: { position: { id: 'job_chef' } }, -}); -record('the envelope resolves its address from the placement table', - envelope.route === '/admin/positions' && envelope.pageKey === 'positions', - `${envelope.pageKey} @ ${envelope.route}`); - -record('a page that publishes only a record still yields a valid envelope', - envelope.position?.id === 'job_chef' - && Array.isArray(envelope.selectedItems) && envelope.selectedItems.length === 0 - && envelope.period === null, - JSON.stringify({ selected: envelope.selectedItems.length, period: envelope.period })); - -record('a page that publishes nothing at all still yields a valid envelope', - (() => { - const bare = agentContext.buildOwliverContext({ context: { id: 'admin.activity', page: 'Activity' } }); - return bare.pageKey === 'activity' && bare.position === null && Object.keys(bare.metrics).length === 0; - })()); - -record('the stored form of the envelope carries no records', - (() => { - const stored = agentContext.storableContext(envelope); - return !('selectedItems' in stored) && !('metrics' in stored) && !('position' in stored) - && stored.pageKey === 'positions'; - })()); - -/* ── Nothing above changed the unscoped product ─────────────────────────── */ - -/** - * The regression assertion for the whole phase. - * - * `resolveIntent` gained four parameters. With none of them supplied it must - * behave exactly as it did before — same branch, same skill, same document - * shape — or every existing caller has quietly changed. - */ -const UNSCOPED_CASES = [ - ['admin.positions', 'Which positions need attention?'], - ['admin.candidatesList', 'Who is waiting on a decision?'], - ['admin.analytics', 'What is the hiring trend?'], - ['admin.activity', 'What happened recently?'], - ['admin.profile', 'What are my permissions?'], -]; -const drifted = []; -for (const [contextId, question] of UNSCOPED_CASES) { - const out = routingModule.resolveIntent({ question, contextId }); - const before = expectedBaseline.contexts[contextId]?.intents.find((i) => i.question === question); - if (out.kind === 'constrained') drifted.push(`${contextId}: became constrained without an agent`); - if (before && out.kind !== before.kind) drifted.push(`${contextId} "${question}": ${before.kind} → ${out.kind}`); -} -record('resolveIntent with no agent behaves exactly as before', - drifted.length === 0, drifted.join(' | ') || `${UNSCOPED_CASES.length} cases unchanged`); - -/* ── 17. Agent switcher wiring ──────────────────────────────────────────── - * - * The switcher is a React component and there is no component harness here, so - * what is checked is the logic behind it: which agent a page opens on, what the - * list offers, and — the part that matters — that none of it can move the page. - * - * The rendering itself is verified by driving the real application; see the - * Phase 6 report. - */ -console.log('\n── Agent switcher wiring ──'); - -const icons = await server.ssrLoadModule('/src/components/agents/icons.js'); - -/* Every icon an agent names resolves, or falls back to the avatar on purpose. */ -record('every agent icon resolves to a component or to the avatar', - ALL_AGENTS.every((a) => a.icon === 'owliver' || icons.agentIconFor(a.icon)), - ALL_AGENTS.map((a) => `${a.icon}${icons.agentIconFor(a.icon) ? '' : '(avatar)'}`).join(', ')); - -record('the primary agent draws the Owliver avatar rather than a glyph', - icons.agentIconFor('owliver') === null); - -record('an unknown icon falls back to the avatar rather than crashing', - icons.agentIconFor('not-an-icon') === null); - -/* ── The list the switcher renders ──────────────────────────────────────── */ - -/** - * Agents that work here sort above those that do not. - * - * On a page where two of nine apply, the seven that cannot must not sit above - * them. Order is otherwise the registry's own, so the list does not reshuffle - * as the reader types. - */ -for (const contextId of ['admin.positions', 'admin.analytics', 'admin.activity']) { - const ordered = [...agentReg.searchAgents(ALL_AGENTS, '')] - .sort((a, b) => Number(runtime.agentCovers(b, contextId)) - Number(runtime.agentCovers(a, contextId))); - const firstConstrained = ordered.findIndex((a) => !runtime.agentCovers(a, contextId)); - const lastCovering = ordered.map((a) => runtime.agentCovers(a, contextId)).lastIndexOf(true); - record(`${contextId.replace('admin.', '')}: agents that work here are listed first`, - firstConstrained === -1 || lastCovering < firstConstrained, - ordered.slice(0, 3).map((a) => `${a.name}${runtime.agentCovers(a, contextId) ? '' : ' (constrained)'}`).join(', ')); -} - -record('search reaches every agent by name', - ALL_AGENTS.every((a) => agentReg.searchAgents(ALL_AGENTS, a.name).some((m) => m.id === a.id))); - -record('a constrained agent is still listed rather than hidden', - agentReg.searchAgents(ALL_AGENTS, 'Analytics').some((a) => a.id === 'analytics-agent'), - 'listed on every page, marked where it does not apply'); - -/* ── Native agent per page ──────────────────────────────────────────────── */ - -/** - * A page opens on the agent written for it, and that state is behaviourally - * identical to no agent at all. - * - * The second half is what makes automatic selection safe: a page agent carries - * every skill its own page offers, so selecting it withholds nothing. If that - * ever stopped being true, opening a page would silently lose a capability. - */ -for (const [contextId, expected] of Object.entries(NATIVE)) { - const native = runtime.defaultAgentForContext(ALL_AGENTS, contextId); - const unscoped = reg.skillsForContext(contextId, [], []).map((s) => s.id); - const withNative = reg.skillsForContext( - contextId, runtime.agentScopedDisabledWith(native, ALL_AGENTS, ALL_SKILLS, []), [] - ).map((s) => s.id); - - record(`${contextId.replace('admin.', '')}: opening on its native agent withholds nothing`, - JSON.stringify(unscoped) === JSON.stringify(withNative), - `${expected}: ${withNative.length}/${unscoped.length} skills`); -} - -/* ── Switching agent cannot move the page ───────────────────────────────── */ - -/** - * The protected contract, at the UI seam. - * - * `AgentProvider` is handed the context id and only reads it. There is no - * setter, no navigation and no write-back, so selecting an agent cannot change - * which page the reader is on. Asserted structurally: the page's context id and - * the skills it offers are identical whichever agent is active. - */ -const movedPage = []; -for (const contextId of PAGE_CONTEXTS) { - const pageOnly = reg.skillsForContext(contextId, [], []).map((s) => s.id); - for (const candidate of ALL_AGENTS) { - const turn = runtime.resolveAgentForTurn(ALL_AGENTS, candidate.id, contextId); - /* The context the panel resolves is unchanged by the choice. */ - if (turn.agent?.id !== candidate.id) movedPage.push(`${contextId}: ${candidate.id} was swapped`); - /* And the page still offers exactly what it offered. */ - const stillOffers = reg.skillsForContext(contextId, [], []).map((s) => s.id); - if (JSON.stringify(stillOffers) !== JSON.stringify(pageOnly)) { - movedPage.push(`${contextId}: page changed under ${candidate.id}`); - } - } -} -record('choosing an agent never changes the page or what it offers', - movedPage.length === 0, - movedPage.slice(0, 3).join(' | ') || `${PAGE_CONTEXTS.length * ALL_AGENTS.length} selections, page unchanged in every one`); - -/* ── The constrained state ──────────────────────────────────────────────── */ - -const constrainedPairs = []; -for (const contextId of PAGE_CONTEXTS) { - for (const candidate of ALL_AGENTS) { - if (runtime.agentCovers(candidate, contextId)) continue; - constrainedPairs.push([contextId, candidate]); - } -} - -record('a constrained agent offers no starters', - constrainedPairs.every(([contextId, candidate]) => runtime.agentStarters(candidate, contextId).length === 0), - `${constrainedPairs.length} constrained pairs`); - -/** - * A constrained agent does not answer — which is not the same as carrying no - * skills here. - * - * An earlier version of this check asserted the empty list and was wrong. - * Control Center Agent is constrained on Positions, yet it carries - * `staffing-risk`, and `staffing-risk` genuinely *is* a Positions skill. The - * list is non-empty and entirely within the page's boundary — the subset proof - * above already covers that. - * - * What actually protects the reader is that the constrained branch fires before - * any skill is consulted, so the agent declines rather than answering under a - * name that does not belong to this page. That is what is asserted here, over - * every constrained pair rather than a sample. - */ -const answeredWhileConstrained = []; -for (const [contextId, candidate] of constrainedPairs) { - const intent = routingModule.resolveIntent({ - question: 'What needs my attention?', - contextId, - disabledSkills: runtime.agentScopedDisabledWith(candidate, ALL_AGENTS, ALL_SKILLS, []), - agent: candidate, - agentCoversPage: false, - agentSuggestion: runtime.defaultAgentForContext(ALL_AGENTS, contextId), - }); - if (intent.kind !== 'constrained') { - answeredWhileConstrained.push(`${contextId} + ${candidate.id}: ${intent.kind}`); - } -} -record('a constrained agent declines rather than answering, on every page', - answeredWhileConstrained.length === 0, - answeredWhileConstrained.slice(0, 3).join(' | ') || `${constrainedPairs.length} constrained pairs all decline`); - -/* And whatever it does carry is still inside the page's own boundary. */ -record('a constrained agent still cannot exceed the page it is constrained on', - constrainedPairs.every(([contextId, candidate]) => { - const pageOnly = reg.skillsForContext(contextId, [], []).map((x) => x.id); - return reg.skillsForContext(contextId, runtime.agentScopedDisabledWith(candidate, ALL_AGENTS, ALL_SKILLS, []), []) - .every((x) => pageOnly.includes(x.id)); - })); - -record('every constrained pair has a native agent to point at instead', - constrainedPairs.every(([contextId]) => runtime.defaultAgentForContext(ALL_AGENTS, contextId))); - -/* ── Starters merge without duplicating a suggestion ────────────────────── */ - -/** - * A starter worded like a skill's suggestion must yield one chip, not two. - * - * The panel de-duplicates on what a chip *resolves to* as well as its label, and - * an agent starter deliberately carries no capability — so it can never address - * a skill the page has not offered, and a duplicate wording drops out. - */ -const dupes = []; -for (const [contextId] of Object.entries(NATIVE)) { - const native = runtime.defaultAgentForContext(ALL_AGENTS, contextId); - const disabled = runtime.agentScopedDisabledWith(native, ALL_AGENTS, ALL_SKILLS, []); - const starters = runtime.agentStarters(native, contextId); - const suggestions = resolver.owliverSuggestions(contextId, disabled, [], {}); - - const labels = [...starters, ...suggestions].map((c) => String(c.label).trim().toLowerCase()); - const unique = new Set(labels); - if (labels.length !== unique.size) { - /* Not a failure in itself — the panel drops the repeat — but it is worth - knowing which wordings collide. */ - dupes.push(`${contextId}: ${labels.length - unique.size}`); - } -} -record('agent starters carry no capability, so they cannot address an unoffered skill', - ALL_AGENTS.every((a) => runtime.agentStarters(a, null).every((s) => s.capability === null)), - dupes.length ? `overlapping wordings de-duplicated on: ${dupes.join(', ')}` : 'no overlapping wordings'); - -/** - * Every destination the switcher offers must exist. - * - * This check exists because it did not, and a visual review found two footer - * actions navigating to routes the router had no entry for. Tests covered what - * the switcher *computed* and nothing about where it *sent* the reader, which - * is exactly the gap a 404 lives in. - * - * Read out of `App.jsx` rather than asserted against a written list, so a route - * that is renamed or removed fails here rather than in someone's browser. - */ -const appSource = readFileSync(join(ROOT, 'src/App.jsx'), 'utf8'); -const switcherSource = readFileSync(join(ROOT, 'src/components/ai-assistant/AgentSwitcher.jsx'), 'utf8'); - -/* Only live navigations count: a disabled control goes nowhere by design. */ -const navigated = [...switcherSource.matchAll(/navigate\('([^']+)'\)/g)].map((m) => m[1]); - -const routed = new Set( - [...appSource.matchAll(/ m[1]) - .filter((path) => path !== '*') - .map((path) => (path.startsWith('/') ? path : `/admin/${path}`)) -); - -const dead = navigated.filter((to) => !routed.has(to)); -record('every route the agent switcher navigates to exists', - dead.length === 0, - dead.length ? `DEAD: ${dead.join(', ')}` : `${navigated.length} destination(s): ${navigated.join(', ')}`); - -/** - * Authoring is reachable from the switcher, and its screen exists. - * - * An earlier version of this asserted the opposite — that the control was - * disabled — which was correct while the management screens did not exist and - * a visual review had found it navigating to a 404. Now that it is built, the - * durable property is the one above: whatever the switcher offers must resolve. - * This states the specific case so the pair cannot silently invert again. - */ -record('creating an agent is reachable from the switcher', - /navigate\('\/admin\/workspace\/agents\/new'\)/.test(switcherSource) - && routed.has('/admin/workspace/agents/new'), - 'offered, and the screen exists'); - -record('every published agent offers at least one starter where it applies', - ALL_AGENTS.filter((a) => a.status === 'published') - .every((a) => a.pages.length === 0 || runtime.agentStarters(a, null).length > 0), - ALL_AGENTS.map((a) => `${a.id}:${a.starters.length}`).join(' ')); - -/* ── 18. Conversation records and insights ─────────────────────────────── - * - * Conversations gained fields: which agent answered, where, what it used, and - * how it was rated. The risk in changing a stored shape is not that the new - * records are wrong — it is that the old ones quietly stop reading, and a - * reader's history disappears without anything saying so. - * - * So the migration is asserted first, and the figures built on top are asserted - * to describe only records that exist. - */ -console.log('\n── Conversation records and insights ──'); - -const insightsSelectors = await server.ssrLoadModule('/src/lib/agents/conversationInsights.js'); -const historyModule = await server.ssrLoadModule('/src/components/ai-assistant/history.js'); - -/* `history.js` writes to localStorage, which does not exist under SSR. A - minimal in-memory stand-in lets the real module be exercised rather than a - reimplementation of it — the migration is the thing under test, and a mock of - it would reproduce none of the failures this section exists for. */ -const historyStore = new Map(); -globalThis.localStorage = { - getItem: (k) => (historyStore.has(k) ? historyStore.get(k) : null), - setItem: (k, v) => historyStore.set(k, String(v)), - removeItem: (k) => historyStore.delete(k), - clear: () => historyStore.clear(), -}; - -const HISTORY_KEY = 'krow_assistant:history'; -const seedHistory = (records) => historyStore.set(HISTORY_KEY, JSON.stringify(records)); - -/* ── A conversation held before any of this existed ──────────────────────── */ - -const V1_RECORD = { - id: 'c_old', - contextId: 'admin.positions', - page: 'Positions', - title: 'Which positions need attention?', - turns: 2, - updatedAt: new Date().toISOString(), - messages: [ - { role: 'user', text: 'Which positions need attention?' }, - { role: 'assistant', blocks: [{ type: 'text', text: 'Three roles need attention.' }] }, - ], -}; - -seedHistory([V1_RECORD]); -const migrated = historyModule.readHistory(); - -record('a conversation stored before agents existed still reads', - migrated.length === 1 && migrated[0].id === 'c_old', - `${migrated.length} record(s)`); - -record('...and its messages are returned byte-identical', - JSON.stringify(migrated[0].messages) === JSON.stringify(V1_RECORD.messages)); - -record('...with the new fields present and honestly empty', - migrated[0].schema === 2 - && migrated[0].agentId === null - && migrated[0].feedback === null - && Array.isArray(migrated[0].skillsUsed) && migrated[0].skillsUsed.length === 0, - JSON.stringify({ - schema: migrated[0].schema, agentId: migrated[0].agentId, skills: migrated[0].skillsUsed, - })); - -record('migration does not rewrite the stored copy', - JSON.parse(historyStore.get(HISTORY_KEY))[0].schema === undefined, - 'read-time migration, so nothing can fail half-written'); - -/* ── A conversation held now ─────────────────────────────────────────────── */ - -historyStore.clear(); -historyModule.saveConversation({ - id: 'c_new', - contextId: 'admin.positions', - page: 'Positions', - agentId: 'positions-agent', - pageContext: { page: 'Positions', pageKey: 'positions', route: '/admin/positions', period: null }, - skillsUsed: ['staffing-risk', 'staffing-risk'], - toolsUsed: ['create_position'], - knowledgeUsed: [], - messages: [ - { role: 'user', text: 'Which roles are at risk?' }, - { role: 'assistant', blocks: [{ type: 'text', text: 'Three.' }] }, - ], -}); - -const [savedConversation] = historyModule.readHistory(); - -record('a conversation records which agent answered', - savedConversation.agentId === 'positions-agent', savedConversation.agentId); - -record('...what it used, deduplicated', - JSON.stringify(savedConversation.skillsUsed) === JSON.stringify(['staffing-risk']) - && JSON.stringify(savedConversation.toolsUsed) === JSON.stringify(['create_position']), - `${JSON.stringify(savedConversation.skillsUsed)} / ${JSON.stringify(savedConversation.toolsUsed)}`); - -/** - * The stored context is the *reduced* envelope. - * - * Selections and computed figures are records; writing them per turn would put - * the dataset into localStorage a message at a time. What a reviewer needs - * later is where the question was asked, not a copy of what was on screen. - */ -record('the stored page context carries no records', - !('selectedItems' in savedConversation.pageContext) && !('metrics' in savedConversation.pageContext) - && !('position' in savedConversation.pageContext) && savedConversation.pageContext.pageKey === 'positions', - Object.keys(savedConversation.pageContext).join(', ')); - -/* ── Feedback ───────────────────────────────────────────────────────────── */ - -historyModule.recordFeedback('c_new', { rating: 'up' }); -record('a conversation can be rated', historyModule.readHistory()[0].feedback?.rating === 'up'); - -historyModule.recordFeedback('c_new', { rating: 'down', note: 'Missed the chef role' }); -const rerated = historyModule.readHistory(); -record('re-rating corrects rather than appends', - rerated.length === 1 && rerated[0].feedback.rating === 'down' && rerated[0].feedback.note === 'Missed the chef role', - `${rerated.length} record(s), rating=${rerated[0].feedback.rating}`); - -historyModule.recordFeedback('c_new', null); -record('clearing a rating leaves none behind, not a neutral one', - historyModule.readHistory()[0].feedback === null); - -record('rating a conversation that does not exist changes nothing', - historyModule.recordFeedback('c_nope', { rating: 'up' }).length === 1); - -/* A rating survives the thread growing. */ -historyModule.recordFeedback('c_new', { rating: 'up' }); -historyModule.saveConversation({ - id: 'c_new', - contextId: 'admin.positions', - page: 'Positions', - agentId: 'positions-agent', - messages: [ - { role: 'user', text: 'Which roles are at risk?' }, - { role: 'assistant', blocks: [{ type: 'text', text: 'Three.' }] }, - { role: 'user', text: 'And the chef role?' }, - ], -}); -record('a rating survives the conversation continuing', - historyModule.readHistory()[0].feedback?.rating === 'up' - && historyModule.readHistory()[0].turns === 2, - `rating kept across ${historyModule.readHistory()[0].turns} turns`); - -/* ── Insight selectors ──────────────────────────────────────────────────── */ - -const nowMs = Date.now(); -const day = (n) => new Date(nowMs - n * 86400000).toISOString(); - -const FIXTURE = [ - { id: 'a1', agentId: 'positions-agent', contextId: 'admin.positions', page: 'Positions', turns: 3, skillsUsed: ['staffing-risk'], toolsUsed: [], feedback: { rating: 'up' }, updatedAt: day(0), messages: [{ role: 'user', text: 'x' }] }, - { id: 'a2', agentId: 'positions-agent', contextId: 'admin.positions', page: 'Positions', turns: 1, skillsUsed: ['staffing-risk', 'create-position'], toolsUsed: ['create_position'], feedback: { rating: 'down' }, updatedAt: day(1), messages: [{ role: 'user', text: 'x' }] }, - { id: 'a3', agentId: 'analytics-agent', contextId: 'admin.analytics', page: 'Analytics', turns: 2, skillsUsed: ['overtime-analysis'], toolsUsed: [], feedback: null, updatedAt: day(1), messages: [{ role: 'user', text: 'x' }] }, -]; - -/** - * Nothing recorded is a different answer from nothing happening. - * - * A row of zeros reads as "the agent was asked and did nothing". The empty flag - * is what lets a view say "not asked yet" instead — which is the whole reason - * Insights can be honest before any conversation exists. - */ -const none = insightsSelectors.conversationStats([]); -record('no conversations reports empty rather than zeros', - none.empty === true && none.total === 0 && none.feedback.score === null, - `score=${none.feedback.score} (null, not 0)`); - -const stats = insightsSelectors.conversationStats(FIXTURE); -record('conversation counts match the records', stats.total === 3 && stats.turns === 6, - `${stats.total} conversations, ${stats.turns} turns`); - -record('pages counted are the distinct contexts', stats.pages === 2, `${stats.pages} pages`); - -record('feedback counts every rating and every absence', - stats.feedback.up === 1 && stats.feedback.down === 1 && stats.feedback.unrated === 1 - && stats.feedback.score === 50, - JSON.stringify(stats.feedback)); - -record('skills are tallied across conversations, most used first', - JSON.stringify(stats.bySkill) === JSON.stringify([ - { id: 'staffing-risk', count: 2 }, { id: 'create-position', count: 1 }, { id: 'overtime-analysis', count: 1 }, - ]), - JSON.stringify(stats.bySkill)); - -record('tools are tallied too', - JSON.stringify(stats.byTool) === JSON.stringify([{ id: 'create_position', count: 1 }])); - -record('conversations are grouped per agent', - JSON.stringify(stats.byAgent) === JSON.stringify([ - { id: 'positions-agent', count: 2 }, { id: 'analytics-agent', count: 1 }, - ])); - -record('a day nothing was asked is not charted as a zero', - stats.byDay.length === stats.activeDays && stats.byDay.every((d) => d.count > 0), - `${stats.byDay.length} active day(s)`); - -record('stats can be scoped to one agent', - insightsSelectors.conversationStats(FIXTURE, { agentId: 'analytics-agent' }).total === 1); - -record('an agent with no conversations reports empty, not zero', - insightsSelectors.conversationStats(FIXTURE, { agentId: 'activity-agent' }).empty === true); - -record('stats can be windowed by date', - insightsSelectors.conversationStats(FIXTURE, { since: day(0.5) }).total === 1, - `${insightsSelectors.conversationStats(FIXTURE, { since: day(0.5) }).total} in the last 12 hours`); - -record('conversationsForAgent narrows without mutating the input', - insightsSelectors.conversationsForAgent(FIXTURE, 'positions-agent').length === 2 - && FIXTURE.length === 3); - -record('unrated conversations are the review queue', - insightsSelectors.unratedConversations(FIXTURE).map((r) => r.id).join(',') === 'a3'); - -/** - * A review row carries no thread. - * - * A list renders forty of these and one is ever opened; including the messages - * would load every conversation to draw a table. - */ -const row = insightsSelectors.reviewRow(FIXTURE[0]); -record('a review row omits the thread itself', - !('messages' in row) && row.id === 'a1' && row.page === 'Positions', - Object.keys(row).join(', ')); - -/* ── Nothing here invents a figure ──────────────────────────────────────── */ - -const invented = []; -for (const key of ['total', 'turns', 'pages']) { - if (insightsSelectors.conversationStats([])[key] !== 0) invented.push(key); -} -for (const entry of [...stats.bySkill, ...stats.byTool, ...stats.byAgent]) { - const real = FIXTURE.some((r) => [...r.skillsUsed, ...r.toolsUsed, r.agentId].includes(entry.id)); - if (!real) invented.push(entry.id); -} -record('every figure traces to a record that exists', - invented.length === 0, invented.join(', ') || 'nothing invented'); - -delete globalThis.localStorage; - -/* ── 19. Agent management ───────────────────────────────────────────────── - * - * The management screens let someone who has never seen a Markdown file - * create, configure and publish an agent. What makes that safe is that they are - * not a second agent system: every screen writes fields, `agentPatch` turns - * those into frontmatter, and the existing parser reads them back. - * - * So what is checked here is the round trip — a form edit must survive being - * written and re-read — and the lifecycle rules that protect a published agent. - */ -console.log('\n── Agent management ──'); - -const lifecycle = await server.ssrLoadModule('/src/lib/agents/agentLifecycle.js'); - -const shippedSource = agentReg.getAgent(ALL_AGENTS, 'positions-agent').markdown; - -/* ── Fields survive the round trip ──────────────────────────────────────── */ - -/** - * The property the whole management UI rests on. - * - * A form holds fields; storage holds Markdown. If a field could not survive - * being written and read back, configuring an agent would silently lose part of - * it — and the loss would only show up later, in an answer that did not happen. - */ -const EDITS = { - name: 'Renamed Positions Agent', - description: 'A different description.', - trigger: 'Use when roles are not filling.', - instructions: 'Answer about open roles only.\n\nAsk which role when none is open.', - icon: 'briefcase', - reasoning: 'deep', - webSearch: true, - pages: ['positions', 'control-center'], - skills: ['staffing-risk', 'create-position'], - subagents: ['analytics-agent'], - starters: [{ label: 'Which roles are at risk?', prompt: 'Which roles are at risk?' }], - knowledge: [{ id: 'policy', label: 'Fill policy', kind: 'note', body: 'A role open 30 days is escalated.', url: '' }], - permissions: { owner: 'demo@krow.app', access: 'specific', people: [{ user: 'a@krow.app', role: 'editor' }] }, -}; - -const composed = agentFields.applyAgentFields(shippedSource, EDITS); -const readBack = agentFields.agentFieldsFromSource(composed); - -const lost = []; -for (const [key, value] of Object.entries(EDITS)) { - if (JSON.stringify(readBack[key]) !== JSON.stringify(value)) { - lost.push(`${key}: wrote ${JSON.stringify(value)}, read ${JSON.stringify(readBack[key])}`); - } -} -record('every configurable field survives being written and read back', - lost.length === 0, lost.slice(0, 2).join(' | ') || `${Object.keys(EDITS).length} fields`); - -record('the composed definition is valid', - agentReg.validateAgentSource(composed) === null, - agentReg.validateAgentSource(composed) || 'ok'); - -record('configuring an agent never has to touch Markdown', - /^---/.test(composed) && agentReg.parseAgent(composed, { custom: true }).name === EDITS.name, - 'fields in, frontmatter out, parsed by the one parser'); - -/* Editing one field leaves the rest of the file alone, including its prose. */ -const oneField = agentFields.applyAgentFields(shippedSource, { description: 'Just this.' }); -const before8 = agentReg.parseAgent(shippedSource, { custom: true }); -const after8 = agentReg.parseAgent(oneField, { custom: true }); -record('editing one field leaves the others untouched', - after8.description === 'Just this.' - && JSON.stringify(after8.skills) === JSON.stringify(before8.skills) - && after8.instructions === before8.instructions, - `${after8.skills.length} skills and the instructions kept`); - -/* ── Lifecycle ──────────────────────────────────────────────────────────── */ - -record('a duplicate is always a draft at v1', - (() => { - const copy = agentReg.parseAgent( - lifecycle.duplicateAgent(shippedSource, { existingIds: ALL_AGENTS.map((a) => a.id) }), - { custom: true } - ); - return copy.status === 'draft' && copy.version === 1 && copy.id !== 'positions-agent'; - })(), - 'a copy of a published agent must not enter the switcher unreviewed'); - -record('a duplicate keeps what the original carried', - (() => { - const copy = agentReg.parseAgent( - lifecycle.duplicateAgent(shippedSource, { existingIds: [] }), { custom: true } - ); - return JSON.stringify(copy.skills) === JSON.stringify(before8.skills) - && copy.instructions === before8.instructions; - })()); - -record('a duplicate never collides with an existing id', - (() => { - const taken = ALL_AGENTS.map((a) => a.id); - const first = agentReg.parseAgent(lifecycle.duplicateAgent(shippedSource, { existingIds: taken }), { custom: true }); - const second = agentReg.parseAgent( - lifecycle.duplicateAgent(shippedSource, { existingIds: [...taken, first.id] }), { custom: true } - ); - return first.id !== second.id; - })()); - -record('archiving takes an agent out of service without altering it', - (() => { - const archived = agentReg.parseAgent(lifecycle.archiveAgent(shippedSource), { custom: true }); - return archived.status === 'archived' - && JSON.stringify(archived.skills) === JSON.stringify(before8.skills); - })()); - -record('restoring brings it back as a draft, not straight back into service', - agentReg.parseAgent(lifecycle.restoreAgent(lifecycle.archiveAgent(shippedSource)), { custom: true }) - .status === 'draft'); - -/** - * Publishing must never discard a version somebody else published. - * - * The failure it prevents is silent: a draft taken from v1 published over a v2 - * looks exactly like the v2 change never having been made. - */ -record('publishing a draft moves it into service', - (() => { - const draft = lifecycle.restoreAgent(shippedSource); - const out = lifecycle.publishAgent(draft); - return !out.conflict && agentReg.parseAgent(out.source, { custom: true }).status === 'published'; - })()); - -record('republishing a published agent moves its version on', - (() => { - const out = lifecycle.publishAgent(shippedSource); - return agentReg.parseAgent(out.source, { custom: true }).version === before8.version + 1; - })(), - 'so "what is live" is always a specific version'); - -record('publishing over a newer version is refused, not silently applied', - (() => { - const out = lifecycle.publishAgent(lifecycle.restoreAgent(shippedSource), { publishedVersion: 5 }); - return Boolean(out.conflict) && !out.source; - })(), - 'a conflict the screen can explain, rather than a lost change'); - -/* ── An agent created from nothing ──────────────────────────────────────── */ - -/** - * The path a non-technical author actually takes: a blank template, filled in - * through the form, saved. It has to produce a definition the runtime accepts. - */ -const fresh = agentFields.applyAgentFields(customAgents.agentTemplate(), { - id: 'hr-helper', - name: 'HR Helper', - description: 'Answers hiring questions for the HR team.', - trigger: 'Use on Candidates for pipeline questions.', - instructions: 'Answer from candidate records on this page.', - icon: 'users', - reasoning: 'balanced', - pages: ['candidates'], - skills: ['candidate-analysis'], - starters: [{ label: 'How strong is the pool?', prompt: 'How strong is the pool?' }], -}); - -record('an agent created entirely through the form is valid', - agentReg.validateAgentSource(fresh) === null, - agentReg.validateAgentSource(fresh) || 'ok'); - -const freshAgent = agentReg.parseAgent(fresh, { custom: true }); -record('...and starts as a draft rather than live', - freshAgent.status === 'draft', freshAgent.status); - -record('...and registers alongside the shipped ones', - (() => { - const { agents, diagnostics } = agentReg.readAgentRegistry([{ path: 'custom/hr-helper.md', raw: fresh }]); - return agents.some((a) => a.id === 'hr-helper') && diagnostics.length === 0; - })(), - 'no diagnostics, so nothing it declared was dropped'); - -record('...and is bounded by the page exactly like a shipped agent', - (() => { - const disabled = runtime.agentScopedDisabledWith(freshAgent, ALL_AGENTS, ALL_SKILLS, []); - const pageOnly = reg.skillsForContext('admin.candidatesList', [], []).map((s) => s.id); - const scoped = reg.skillsForContext('admin.candidatesList', disabled, []).map((s) => s.id); - /* And it reaches nothing at all on a page it does not cover. */ - const elsewhere = reg.skillsForContext('admin.analytics', disabled, []).map((s) => s.id); - return scoped.every((x) => pageOnly.includes(x)) && elsewhere.length === 0; - })(), - 'a user-created agent gets the same boundary, not a weaker one'); - -/* ── Editing a shipped agent overrides rather than mutates ───────────────── */ - -record('editing a shipped agent is reported as an override', - (() => { - const { diagnostics } = agentReg.readAgentRegistry([{ path: 'custom/positions-agent.md', raw: composed }]); - return diagnostics.some((d) => d.kind === 'shadowed' && d.agentId === 'positions-agent'); - })(), - 'the shipped definition is never altered on disk'); - -record('the shipped definition is still intact after an override', - agentReg.AGENTS.find((a) => a.id === 'positions-agent').name === 'Positions Agent'); - -/* ── The Add Skills modal reads the one registry ────────────────────────── */ - -/** - * Asserted against the registry rather than the component, because the failure - * worth preventing is architectural: a separate list for agents would drift - * from the one Owliver runs, and an agent would offer a skill the runtime does - * not have. - */ -const attachable = reg.skillsWithFacet(reg.allSkills([]), 'owliver').filter((s) => s.status === 'active'); -record('every attachable skill comes from the shared registry', - attachable.every((s) => reg.SKILLS.some((r) => r.id === s.id)), - `${attachable.length} attachable`); - -record('workforce training paths are not offered as agent skills', - attachable.every((s) => s.kind !== 'workforce'), - 'a training path is something a person learns, not something an agent does'); - -record('every category offered by the picker matches at least one skill', - (() => { - const categories = [...new Set(attachable.map((s) => s.category).filter(Boolean))]; - return categories.every((c) => attachable.some((s) => s.category === c)); - })(), - [...new Set(attachable.map((s) => s.category).filter(Boolean))].join(', ')); - -/* ── Every management destination exists ────────────────────────────────── */ - -const managementRoutes = ['/admin/workspace/agents', '/admin/workspace/agents/new']; -const appRoutes = new Set( - [...readFileSync(join(ROOT, 'src/App.jsx'), 'utf8').matchAll(/ m[1]) - .map((path) => (path.startsWith('/') ? path : `/admin/${path}`)) -); -record('every agent management route is registered', - managementRoutes.every((r) => appRoutes.has(r)), - managementRoutes.filter((r) => !appRoutes.has(r)).join(', ') || managementRoutes.join(', ')); - -record('the dynamic agent route is registered after the static one', - (() => { - const source = readFileSync(join(ROOT, 'src/App.jsx'), 'utf8'); - return source.indexOf('workspace/agents/new') < source.indexOf('workspace/agents/:id'); - })(), - 'so `agents/new` cannot be read as an agent whose id is "new"'); - -/* ── 20. Owliver on the agent configuration screen ──────────────────────── - * - * Configure is a workspace page, not an operational one, and it is now a host - * for the *existing* Owliver rather than a second chat. Two things have to hold - * and they pull in opposite directions: - * - * - Owliver must actually mount there, at both addresses. - * - Standing there must not become standing on an operational page — most - * sharply when the agent being *edited* is an operational agent. - * - * The second is the one worth testing hardest: configuring the Analytics Agent - * must not put a reader on Analytics. - */ -console.log('\n── Owliver on Agent Configure ──'); - -const CONFIGURE_CONTEXT = 'admin.agentConfigure'; - -/* ── It mounts, at both addresses ────────────────────────────────────────── */ - -for (const route of ['/admin/workspace/agents/new', '/admin/workspace/agents/analytics-agent']) { - const resolved = placement.resolveAssistantContext('admin', route); - record(`Owliver mounts on \`${route}\``, - resolved?.id === CONFIGURE_CONTEXT, resolved?.id || 'no panel'); -} - -record('the configure context is the one Owliver already uses, not a new panel', - Boolean(contexts.ASSISTANT_CONTEXTS[CONFIGURE_CONTEXT]?.respond) - && Boolean(contexts.ASSISTANT_CONTEXTS[CONFIGURE_CONTEXT]?.capabilities?.length), - 'a context in the existing table, resolved by the existing placement'); - -/** - * The rest of the workspace hosts Owliver too — under its *own* context. - * - * These two checks used to assert the opposite: that the agents list and the - * other workspace routes carried no panel at all. That was right while Agent - * Configure was the only workspace host, and wrong as a general rule — it left - * Owliver dead on every configuration surface in the product, on the reasoning - * that a page with no specialist agent is a page with no assistant. It is not. - * - * What still matters, and is what these now assert, is that each one resolves - * to *its own* context rather than borrowing Agent Configure's: the page a - * reader is standing on is never something another page's context describes. - */ -const WORKSPACE_HOSTS = { - '/admin/settings': 'admin.settings', - '/admin/workspace': 'admin.workspace', - '/admin/workspace/agents': 'admin.workspaceAgents', - '/admin/workspace/skills': 'admin.workspaceSkills', - '/admin/workspace/skill-development': 'admin.skillDevelopment', - '/admin/workspace/skills/new': 'admin.skillConfigure', - '/admin/workspace/skills/owliver/new': 'admin.skillConfigure', - '/admin/workspace/skills/my-skill': 'admin.skillConfigure', - '/admin/workspace/skills/owliver/my-skill': 'admin.skillConfigure', -}; -const misplaced = Object.entries(WORKSPACE_HOSTS) - .filter(([route, id]) => placement.resolveAssistantContext('admin', route)?.id !== id) - .map(([route, id]) => `${route}: expected ${id}, got ${placement.resolveAssistantContext('admin', route)?.id ?? 'no panel'}`); -record('every workspace surface hosts Owliver under its own context', - misplaced.length === 0, - misplaced.join(' | ') || `${Object.keys(WORKSPACE_HOSTS).length} routes`); - -record('the agents list does not borrow the configure context', - placement.resolveAssistantContext('admin', '/admin/workspace/agents')?.id === 'admin.workspaceAgents'); - -/** - * The pattern must not swallow addresses beneath it. - * - * One segment after `agents/`, and no deeper. A nested route added later would - * otherwise silently inherit this panel. - */ -record('the dynamic pattern matches one segment only', - placement.resolveAssistantContext('admin', '/admin/workspace/agents/x/y') === null - && placement.resolveAssistantContext('admin', '/admin/workspace/agents/x') !== null); - -/* ── The eight operational pages are untouched ───────────────────────────── */ - -/** - * Exact matching still happens first, so none of the eight ever reaches the - * pattern table. Asserted rather than assumed, because "I added a fallback" is - * exactly the change that quietly re-routes something. - */ -const OPERATIONAL = { - '/admin': 'admin.controlCenter', - '/admin/positions': 'admin.positions', - '/admin/candidates': 'admin.candidatesList', - '/admin/hired': 'admin.hiredHistory', - '/admin/talent-pool': 'admin.talentPool', - '/admin/university': 'admin.forge', - '/admin/analytics': 'admin.analytics', - '/admin/activity': 'admin.activity', -}; -const rerouted = Object.entries(OPERATIONAL) - .filter(([route, expectedId]) => placement.resolveAssistantContext('admin', route)?.id !== expectedId); -record('all eight operational pages resolve exactly as before', - rerouted.length === 0, rerouted.map(([r]) => r).join(', ') || '8 pages unchanged'); - -/* ── The critical boundary ──────────────────────────────────────────────── */ - -/** - * Editing the Analytics Agent does not put the reader on Analytics. - * - * The edited agent is a record being changed, not the page anyone is standing - * on. Two independent reasons this holds, both asserted: no skill declares the - * configure page, and the Analytics Agent does not cover it. - */ -record('no skill is available on the configure page at all', - reg.skillsForContext(CONFIGURE_CONTEXT, [], []).length === 0, - JSON.stringify(reg.skillsForContext(CONFIGURE_CONTEXT, [], []).map((s) => s.id))); - -const analyticsAgentCfg = agentReg.getAgent(ALL_AGENTS, 'analytics-agent'); -record('the Analytics Agent does not cover the configure page', - runtime.agentCovers(analyticsAgentCfg, CONFIGURE_CONTEXT) === false); - -record('editing the Analytics Agent exposes no analytics skill', - reg.skillsForContext( - CONFIGURE_CONTEXT, - runtime.agentScopedDisabledWith(analyticsAgentCfg, ALL_AGENTS, ALL_SKILLS, []), - [] - ).length === 0, - 'the edited agent is metadata, not the current page'); - -/* The Analytics page keeps everything it had. A subset check, not equality: - the snapshot predates the analysis skills added since, and the baseline - section above already governs additions. What matters here is that mounting - Owliver on a workspace page took nothing away from an operational one. */ -const analyticsNow = reg.skillsForContext('admin.analytics', [], []).map((s) => s.id); -const analyticsLost = expectedBaseline.contexts['admin.analytics'].skills - .filter((id) => !analyticsNow.includes(id)); -record('...and the Analytics page kept every skill it had', - analyticsLost.length === 0, - analyticsLost.length ? `LOST ${JSON.stringify(analyticsLost)}` : `${analyticsNow.length} skills, none lost`); - -/* Every agent, on the configure page: none reaches an operational skill. */ -const leakedOnConfigure = ALL_AGENTS.filter((a) => - reg.skillsForContext( - CONFIGURE_CONTEXT, runtime.agentScopedDisabledWith(a, ALL_AGENTS, ALL_SKILLS, []), [] - ).length > 0); -record('no agent reaches an operational skill from the configure page', - leakedOnConfigure.length === 0, - leakedOnConfigure.map((a) => a.id).join(', ') || `${ALL_AGENTS.length} agents, none`); - -/* ── The page answers honestly ──────────────────────────────────────────── */ - -const configureContext = contexts.ASSISTANT_CONTEXTS[CONFIGURE_CONTEXT]; - -record('the configure page has a native agent to answer with', - runtime.defaultAgentForContext(ALL_AGENTS, CONFIGURE_CONTEXT)?.id === 'krow-workforce-agent', - runtime.defaultAgentForContext(ALL_AGENTS, CONFIGURE_CONTEXT)?.name || 'none'); - -record('it answers questions about agents and skills', - ['What agents can I configure?', 'Where do skills come from?', 'What does reasoning do?'] - .every((q) => routingModule.resolveIntent({ question: q, contextId: CONFIGURE_CONTEXT }).kind === 'answer'), - 'agent-management questions are in scope'); - -/** - * A workforce question asked here is declined, not answered. - * - * This is the honest half of the boundary: the page has no operational records, - * so it must say so and point at the page that does — rather than answering - * from whatever the configuration screen happens to know. - */ -const workforceHere = ['How many candidates applied this week?', 'What is our attendance rate?'] - .map((q) => routingModule.resolveIntent({ question: q, contextId: CONFIGURE_CONTEXT })); -record('a workforce question on the configure page is declined or routed away', - workforceHere.every((i) => i.kind === 'outOfScope' || i.kind === 'navigate'), - workforceHere.map((i) => i.kind).join(', ')); - -record('...and never answered from the configure page itself', - workforceHere.every((i) => i.kind !== 'answer' && !i.skill)); - -/* ── One Owliver, not two ───────────────────────────────────────────────── */ - -/** - * Asserted structurally: the configure screen must not import or define a chat. - * The panel it gets is the one the Admin shell already mounts. - */ -const detailSource = readFileSync(join(ROOT, 'src/pages/admin/AgentDetail.jsx'), 'utf8'); -const configureSource = readFileSync(join(ROOT, 'src/components/agents/AgentConfigure.jsx'), 'utf8'); - -record('the configure screen defines no chat of its own', - !/KrowAssistant|AssistantPanel|useConversation|createAssistantProvider/.test(detailSource + configureSource), - 'no second panel, provider or conversation'); - -record('the panel is mounted once, by the shell', - /* ` !s.pages.includes('workspace-agent-configure')), - 'so there is nothing operational to reach here'); - -/* ── The configure workspace ───────────────────────────────────────────── - * - * The screen is an authoring surface and the only one in the console that is, - * so it carries a visual treatment the eight operational pages do not. What - * follows guards the two things that treatment must not cost. - * - * **Every control still exists.** A redesign that quietly drops a field looks - * exactly like a redesign that kept it — until someone cannot set an icon. So - * each closed vocabulary is asserted to be *rendered from its own table*, not - * merely present as a word: the icon picker over `AGENT_ICONS`, the page chips - * over `SUPPORTED_SKILL_PAGES`, reasoning over `REASONING_MODES`, and the - * knowledge kinds over `KNOWLEDGE_KINDS`. A hand-written subset would pass a - * grep and still be wrong the day a value is added. - * - * **It stays Krow.** The palette is the tokens, and nothing else. - */ - -const canvasSource = readFileSync(join(ROOT, 'src/components/agents/AgentCanvas.jsx'), 'utf8'); -const agentUi = detailSource + configureSource + canvasSource; - -/* One surface with sections inside it, not four cards side by side. */ -record('the configure screen composes one workspace surface', - / m[1]); -const railLeaves = [...configureSource.matchAll(/\{\s*id:\s*'([a-z-]+)',\s*label:/g)].map((m) => m[1]); -const railIds = [...railSections, ...railLeaves]; -const missingTargets = railIds.filter((id) => !new RegExp(`id="${id}"`).test(configureSource)); -record('every rail entry addresses something the document renders', - railSections.length === 4 && railLeaves.length === 12 && missingTargets.length === 0, - missingTargets.length - ? `no target for ${missingTargets.join(', ')}` - : `${railSections.length} sections, ${railLeaves.length} leaves, all addressable`); - -/* Each closed vocabulary is rendered from its own table. */ -record('the icon picker offers every icon an agent may name', - /icons=\{AGENT_ICONS\}/.test(configureSource) && vocab.AGENT_ICONS.length === 10, - `${vocab.AGENT_ICONS.length} icons, from AGENT_ICONS`); - -record('the page chips offer every supported page', - /SUPPORTED_SKILL_PAGES\.map/.test(configureSource), - `${surfaces.SUPPORTED_SKILL_PAGES.length} pages, from SUPPORTED_SKILL_PAGES`); - -record('reasoning offers every mode', - /REASONING_MODES\.map/.test(configureSource), - vocab.REASONING_MODES.map((m) => m.label).join(' / ')); - -record('knowledge offers every kind', - /KNOWLEDGE_KINDS\.map/.test(configureSource), - vocab.KNOWLEDGE_KINDS.join(', ')); - -/* The controls that are not list-driven, one by one. */ -const CONTROLS = { - name: /value=\{fields\.name\}/, - description: /value=\{fields\.description\}/, - 'when to use': /value=\{fields\.trigger\}/, - instructions: /value=\{fields\.instructions\}/, - 'add skills': /setAddingSkills\(true\)/, - 'remove skill': /skills: fields\.skills\.filter/, - 'add knowledge': /knowledge: \[\s*\n?\s*\.\.\.fields\.knowledge/, - 'remove knowledge': /knowledge: fields\.knowledge\.filter/, - 'add starter': /starters: \[\.\.\.fields\.starters/, - 'remove starter': /starters: fields\.starters\.filter/, - 'web search': /onCheckedChange=\{\(webSearch\) => set\(\{ webSearch \}\)\}/, - 'add subagent': /subagents: \[\.\.\.fields\.subagents/, - 'remove subagent': /subagents: fields\.subagents\.filter/, -}; -const missingControls = Object.entries(CONTROLS) - .filter(([, re]) => !re.test(configureSource)).map(([k]) => k); -record('every field the editor had is still editable', - missingControls.length === 0, - missingControls.length ? `MISSING ${missingControls.join(', ')}` : `${Object.keys(CONTROLS).length} controls`); - -/* Lifecycle state stays legible: version, status, and the customized marker. */ -record('the header still states version, status and customization', - /v\{agent\.version\}/.test(detailSource) - && /\{agent\.status\}/.test(detailSource) - && /isOverridden\(id\) && Customized/.test(detailSource) - && /Publish update/.test(detailSource), - 'v · status · Customized · Publish update'); - -record('save state is shown and Save is offered only when there is something to save', - /dirty \? 'Unsaved changes' : 'Saved'/.test(detailSource) - && /onClick=\{persist\} disabled=\{!dirty\}/.test(detailSource)); - -/** - * Closed sections stay reachable by the rail, which means they stay mounted — - * and a mounted control nobody can see must not be in the tab order. - */ -record('collapsed content is inert rather than merely hidden', - /inert=\{open \? undefined : true\}/.test(canvasSource) - && !/aria-hidden=\{open/.test(canvasSource), - 'not tabbable while closed'); - -/** - * Motion is CSS transitions only, so the global `prefers-reduced-motion` rule - * in index.css governs it. A JavaScript animation would need its own guard and - * would eventually be written without one. - */ -record('the workspace animates in CSS, so reduced motion is inherited', - !/framer-motion/.test(configureSource + canvasSource) - && /transition-\[grid-template-rows\]/.test(canvasSource), - 'no JS animation on this screen'); - -/** - * The palette is the design system's. Not "no hex anywhere" — the check is - * that this screen introduced none of its own. - */ -const strayColour = [...agentUi.matchAll(/(?:bg|text|border|from|via|to|ring)-(?:\[#[0-9a-fA-F]{3,8}\]|purple|violet|fuchsia|pink|indigo|cyan|teal|lime|orange)-?\d*/g)] - .map((m) => m[0]); -record('the configure screen introduces no colour outside the Krow palette', - strayColour.length === 0, - strayColour.length ? `STRAY ${[...new Set(strayColour)].join(', ')}` : 'tokens only'); - -/* The redesign is scoped to this screen. Nothing else may import its parts. */ -const canvasImporters = [ - ...readdirSync(join(ROOT, 'src/pages/admin')).map((f) => ['src/pages/admin', f]), - ...readdirSync(join(ROOT, 'src/components/agents')).map((f) => ['src/components/agents', f]), -] - .filter(([, f]) => /\.jsx?$/.test(f)) - .filter(([, f]) => !/^(AgentConfigure|AgentDetail|AgentCanvas)\./.test(f)) - .filter(([dir, f]) => /agents\/AgentCanvas|from '\.\/AgentCanvas'/ - .test(readFileSync(join(ROOT, dir, f), 'utf8'))) - .map(([dir, f]) => `${dir}/${f}`); -record('the workspace treatment is used by the configure screen alone', - canvasImporters.length === 0, - canvasImporters.join(', ') || 'no other page imports it'); - -/* ── 20b. Native agent pages vs agent-less pages ────────────────────────── - * - * The correction this section pins down, in one sentence: **a page with no - * agent of its own is not a page without Owliver.** - * - * Two runtime modes, and only the first existed as a deliberate design: - * - * 1. **Native.** The eight operational pages open on the agent written for - * them, with that page's skills and that page's records. Unchanged, and - * most of what follows exists to prove it stayed unchanged. - * 2. **Fallback.** Settings, the workspace surfaces, Agent Configure and - * Profile have no specialist and need none. They open on the general Krow - * Workforce Agent, and Owliver works there — chat, chips, history, - * everything — while still declining any question about records the page - * does not hold. - * - * The failure this replaces was reading "no native agent" as "constrained", and - * a constrained panel is a dead one: no starters, no answers, a decline to every - * question. That is the right behaviour for an agent the reader *chose* which - * does not cover the page, and the wrong behaviour for a page nobody wrote an - * agent for. Both are asserted below, because the whole correction is the - * distinction between them. - */ -console.log('\n── Native agent pages vs agent-less pages ──'); - -/** Pages with an agent of their own. `NATIVE` above is the same eight. */ -const NATIVE_CONTEXTS = Object.keys(NATIVE); - -/** - * Pages with none. - * - * Profile is on this list and always was — it had no specialist before any of - * this and resolved to the general agent, which is exactly the behaviour the - * six new surfaces now share. Listing it here is what proves the fallback is - * one rule rather than a special case for the pages added last. - */ -const AGENTLESS_CONTEXTS = [ - 'admin.settings', - 'admin.workspace', - 'admin.workspaceAgents', - 'admin.workspaceSkills', - 'admin.skillConfigure', - 'admin.skillDevelopment', - 'admin.agentConfigure', - 'admin.profile', -]; - -const GENERAL = 'krow-workforce-agent'; - -/* ── 1–2. Resolution, both modes ────────────────────────────────────────── */ - -for (const contextId of NATIVE_CONTEXTS) { - const native = runtime.nativeAgentForContext(ALL_AGENTS, contextId); - const resolved = runtime.resolveDefaultAgent(ALL_AGENTS, contextId); - record(`${contextId.replace('admin.', '')}: resolves its own agent`, - native?.id === NATIVE[contextId] && resolved?.id === NATIVE[contextId], - resolved?.id || 'none'); -} - -for (const contextId of AGENTLESS_CONTEXTS) { - const page = contextId.replace('admin.', ''); - record(`${page}: has no agent of its own, and resolves the general one`, - runtime.nativeAgentForContext(ALL_AGENTS, contextId) === null - && runtime.resolveDefaultAgent(ALL_AGENTS, contextId)?.id === GENERAL, - runtime.resolveDefaultAgent(ALL_AGENTS, contextId)?.id || 'NONE'); -} - -record('the general agent covers every agent-less page', - AGENTLESS_CONTEXTS.every((c) => runtime.agentCovers(agentReg.getAgent(ALL_AGENTS, GENERAL), c)), - `${AGENTLESS_CONTEXTS.length} pages`); - -/* Nothing above changed which agent the eight operational pages open on. */ -record('the eight native pages still open on exactly the agents they did', - Object.entries(NATIVE).every(([c, id]) => runtime.defaultAgentForContext(ALL_AGENTS, c)?.id === id), - Object.entries(NATIVE).filter(([c, id]) => runtime.defaultAgentForContext(ALL_AGENTS, c)?.id !== id) - .map(([c]) => c).join(', ') || '8 pages unchanged'); - -/* ── 3–5. An agent-less page does not open constrained ──────────────────── */ - -/** - * The heart of it. - * - * With nothing chosen, the panel resolves an agent that covers the page — so - * `covers` is true, no constrained document is produced, and the reader gets an - * assistant rather than an apology. - */ -const openedConstrained = []; -for (const contextId of AGENTLESS_CONTEXTS) { - const resolved = runtime.resolveDefaultAgent(ALL_AGENTS, contextId); - const turn = runtime.resolveAgentForTurn(ALL_AGENTS, null, contextId); - const intent = routingModule.resolveIntent({ - question: 'What can I do here?', - contextId, - agent: resolved, - agentCoversPage: runtime.agentCovers(resolved, contextId), - agentSuggestion: resolved, - }); - if (!turn.covers || intent.kind === 'constrained') openedConstrained.push(contextId); -} -record('no agent-less page opens in the constrained state', - openedConstrained.length === 0, - openedConstrained.join(', ') || `${AGENTLESS_CONTEXTS.length} pages open with a working agent`); - -record('Settings opens on the general agent, not constrained', - runtime.resolveDefaultAgent(ALL_AGENTS, 'admin.settings')?.id === GENERAL - && runtime.resolveAgentForTurn(ALL_AGENTS, null, 'admin.settings').covers === true); - -record('Agent Configure opens on the general agent, not constrained', - runtime.resolveDefaultAgent(ALL_AGENTS, 'admin.agentConfigure')?.id === GENERAL - && runtime.resolveAgentForTurn(ALL_AGENTS, null, 'admin.agentConfigure').covers === true); - -/* ── 6–7. The edited agent is metadata, never the runtime ───────────────── */ - -/** - * Configuring an agent does not become standing on the page it covers. - * - * Section 20 proves the *data* half — no skill is reachable there. This is the - * *identity* half: whichever agent is being edited, the agent answering on the - * configure page is the general one, because nothing about the record on the - * form is an input to agent resolution. - */ -for (const edited of ['analytics-agent', 'positions-agent', 'activity-agent']) { - record(`editing \`${edited}\` does not make it the runtime agent`, - runtime.resolveDefaultAgent(ALL_AGENTS, 'admin.agentConfigure')?.id === GENERAL - && runtime.agentCovers(agentReg.getAgent(ALL_AGENTS, edited), 'admin.agentConfigure') === false, - 'the edited agent is configuration, not the active runtime identity'); -} - -/* ── 8–13. Stale selection ──────────────────────────────────────────────── */ - -/** - * A selection made on one page must not decide another. - * - * `resolveSelection` is a pure function of (agents, selection, context), so the - * whole rule is provable here rather than through a React tree. Three outcomes, - * and every case below is one of them: applied because it covers, applied - * because this is where it was chosen, or retired. - */ -const LEAKS = [ - ['positions-agent', 'admin.positions', 'admin.settings'], - ['analytics-agent', 'admin.analytics', 'admin.settings'], - ['activity-agent', 'admin.activity', 'admin.settings'], - ['positions-agent', 'admin.positions', 'admin.agentConfigure'], - ['control-center-agent', 'admin.controlCenter', 'admin.workspaceSkills'], - ['talent-pool-agent', 'admin.talentPool', 'admin.skillDevelopment'], -]; -const leakedSelections = []; -for (const [id, chosenOn, arrivingAt] of LEAKS) { - const applied = runtime.resolveSelection(ALL_AGENTS, { id, contextId: chosenOn }, arrivingAt); - const answering = runtime.resolveAgentForTurn(ALL_AGENTS, applied.id, arrivingAt); - if (applied.id !== null || !applied.retire || answering.agent?.id !== GENERAL || !answering.covers) { - leakedSelections.push(`${id} (${chosenOn}) → ${arrivingAt}: ${answering.agent?.id}`); - } -} -record('a selection made on another page never follows onto an agent-less page', - leakedSelections.length === 0, - leakedSelections.join(' | ') || `${LEAKS.length} navigations, every one resolved to the general agent`); - -record('a retired selection is retired, not merely ignored', - runtime.resolveSelection(ALL_AGENTS, { id: 'positions-agent', contextId: 'admin.positions' }, 'admin.settings').retire === true, - 'so returning to that page later does not resurrect it'); - -/* Arriving on a page the selection *does* cover keeps it — the choice is only - dropped where it could not answer. */ -record('a selection that covers the page it arrives on is kept', - (() => { - const applied = runtime.resolveSelection( - ALL_AGENTS, { id: 'analytics-agent', contextId: 'admin.settings' }, 'admin.analytics' - ); - return applied.id === 'analytics-agent' && applied.covers === true && applied.retire === false; - })(), - 'Settings → Analytics keeps a deliberate choice'); - -record('Agent Configure → Analytics resolves Analytics normally', - (() => { - const carried = runtime.resolveSelection(ALL_AGENTS, { id: null, contextId: 'admin.agentConfigure' }, 'admin.analytics'); - return carried.id === null - && runtime.resolveDefaultAgent(ALL_AGENTS, 'admin.analytics')?.id === 'analytics-agent'; - })()); - -record('Settings → Analytics resolves Analytics normally', - runtime.resolveDefaultAgent(ALL_AGENTS, 'admin.analytics')?.id === 'analytics-agent'); - -/* An account-level default is a choice made nowhere, and is read by the same - rule: it applies where it covers, and never constrains a page it does not. */ -record('an account default that does not cover a page does not constrain it', - runtime.resolveSelection(ALL_AGENTS, 'analytics-agent', 'admin.settings').id === null - && runtime.resolveSelection(ALL_AGENTS, 'analytics-agent', 'admin.analytics').id === 'analytics-agent'); - -/* A stored id for an agent that no longer exists resolves to nothing rather - than leaving the panel pointed at a ghost. */ -record('a selection naming an agent that no longer exists is retired', - runtime.resolveSelection(ALL_AGENTS, { id: 'deleted-agent', contextId: 'admin.settings' }, 'admin.settings').retire === true); - -/* ── 18. An explicit incompatible choice still constrains ───────────────── */ - -/** - * The other half, and the reason "stale" had to be defined rather than just - * cleared: choosing a specialist *here*, on a page it does not cover, is a - * deliberate act. It is honoured, shown as constrained, and it declines — the - * honest answer, and the same one every constrained pair gives above. - */ -const explicit = runtime.resolveSelection( - ALL_AGENTS, { id: 'analytics-agent', contextId: 'admin.settings' }, 'admin.settings' -); -const explicitTurn = runtime.resolveAgentForTurn(ALL_AGENTS, explicit.id, 'admin.settings'); -record('choosing an incompatible agent on this page is honoured, and constrained', - explicit.id === 'analytics-agent' && explicit.retire === false - && explicitTurn.agent?.id === 'analytics-agent' && explicitTurn.covers === false, - 'the reader chose it here, so it is not swapped out from under them'); - -record('...and it declines rather than answering', - routingModule.resolveIntent({ - question: 'What can I configure here?', - contextId: 'admin.settings', - agent: explicitTurn.agent, - agentCoversPage: false, - agentSuggestion: runtime.resolveDefaultAgent(ALL_AGENTS, 'admin.settings'), - }).kind === 'constrained'); - -record('...and the way out of it is named on a page with no specialist', - Boolean(runtime.resolveDefaultAgent(ALL_AGENTS, 'admin.settings')), - 'the general agent is what a constrained answer points at'); - -/* ── 14–17. Owliver actually works there ────────────────────────────────── */ - -/* Every one of these pages mounts the panel. A resolution rule is worth nothing - if there is no panel to resolve for. */ -record('every agent-less context is a real placement with a panel', - AGENTLESS_CONTEXTS.every((id) => Object.values(placement.PLACEMENT_ROUTES).includes(id) - || Object.values(placement.PLACEMENT_PATTERN_ROUTES).includes(id)), - AGENTLESS_CONTEXTS.filter((id) => !Object.values(placement.PLACEMENT_ROUTES).includes(id)).join(', ') || 'all mounted'); - -/* Each one names itself, so the header reads the page the reader is on. */ -record('every agent-less page states its own name, key and route', - AGENTLESS_CONTEXTS.every((id) => { - const context = contexts.ASSISTANT_CONTEXTS[id]; - const pageKey = reg.pageKeyForContext(id); - return Boolean(context?.page) && Boolean(pageKey) && Boolean(reg.routeForPageKey(pageKey)); - }), - AGENTLESS_CONTEXTS.map((id) => `${contexts.ASSISTANT_CONTEXTS[id].page}/${reg.pageKeyForContext(id)}`).join(', ')); - -/** - * The chips are real. - * - * Every suggestion offered on these pages is answered by the page it is offered - * on — a chip is a promise, and one that declines is worse than no chip. The - * count is asserted too: suggestions disappearing on an agent-less page was one - * of the reported symptoms. - */ -const emptyChips = []; -const brokenChips = []; -for (const contextId of AGENTLESS_CONTEXTS) { - const prompts = dynamic.buildPrompts(contextId, factSheet, null) || []; - if (!prompts.length) emptyChips.push(contextId); - for (const prompt of prompts) { - const intent = routingModule.resolveIntent({ question: prompt.prompt, contextId }); - if (intent.kind !== 'answer') brokenChips.push(`${contextId}: "${prompt.label}" → ${intent.kind}`); - } -} -record('every agent-less page offers suggestions', - emptyChips.length === 0, - emptyChips.join(', ') || AGENTLESS_CONTEXTS - .map((c) => `${c.replace('admin.', '')}:${(dynamic.buildPrompts(c, factSheet, null) || []).length}`).join(' ')); - -record('every suggestion an agent-less page offers is one it can answer', - brokenChips.length === 0, - brokenChips.slice(0, 3).join(' | ') || 'every chip resolves to an answer'); - -/* And the landing screen is furnished — a title, something to type into. */ -record('every agent-less page has a landing screen rather than an empty panel', - AGENTLESS_CONTEXTS.every((contextId) => { - const intro = dynamic.buildIntro(contextId, factSheet, 'Test'); - return Boolean(intro.title) && Boolean(intro.description) && intro.placeholders.length > 0; - })); - -/** - * No fake skills. - * - * The lazy way to make a configuration page answer is to invent a skill for it. - * That would show up in exactly two places, and both are checked: the page would - * carry skills, and its chips would resolve to one. Neither is true — the - * answers come from the page responder, which is the mechanism every context in - * the table already used. - */ -record('no agent-less page carries any skill', - AGENTLESS_CONTEXTS.every((c) => reg.skillsForContext(c, [], []).length === 0), - AGENTLESS_CONTEXTS.filter((c) => reg.skillsForContext(c, [], []).length).join(', ') || 'no skills, on any of them'); - -/* A chip may name one of the page's own capabilities — Profile's always have — - and that is a context responder, not a skill. What must not exist is a skill - behind any of them. */ -record('no chip on an agent-less page is backed by a skill', - AGENTLESS_CONTEXTS.every((contextId) => - (dynamic.buildPrompts(contextId, factSheet, null) || []).every((p) => - !reg.matchSkill(p.prompt, contextId, [], []))), - 'the fallback is architectural, not a skill layer'); - -record('no agent reaches a skill from an agent-less page', - AGENTLESS_CONTEXTS.every((contextId) => ALL_AGENTS.every((a) => - reg.skillsForContext(contextId, runtime.agentScopedDisabledWith(a, ALL_AGENTS, ALL_SKILLS, []), []).length === 0)), - `${AGENTLESS_CONTEXTS.length} pages × ${ALL_AGENTS.length} agents`); - -/** - * Honest about what it cannot answer. - * - * An operational question asked on a configuration screen must never be - * answered from that screen. It may route to the page that holds the records — - * which is the useful answer — or decline. What it may not do is produce a - * figure. - */ -const OPERATIONAL_QUESTIONS = [ - 'What are the open positions today?', - 'What is our attendance rate?', - 'Which candidates need review?', - 'How many people did we hire last month?', -]; -const fabricated = []; -for (const contextId of AGENTLESS_CONTEXTS) { - for (const question of OPERATIONAL_QUESTIONS) { - const intent = routingModule.resolveIntent({ question, contextId }); - if (intent.kind === 'answer' || intent.skill) fabricated.push(`${contextId}: "${question}" → ${intent.kind}`); - } -} -record('an operational question on an agent-less page is declined or routed, never answered', - fabricated.length === 0, - fabricated.slice(0, 3).join(' | ') - || `${AGENTLESS_CONTEXTS.length} pages × ${OPERATIONAL_QUESTIONS.length} questions`); - -/* The decline names what the page *can* do, so it is an offer rather than a - dead end. */ -record('the decline offers what the page can answer instead', - AGENTLESS_CONTEXTS.every((contextId) => { - const intent = routingModule.resolveIntent({ question: 'What is our attendance rate?', contextId }); - if (intent.kind !== 'outOfScope') return true; - return (intent.doc?.blocks || []).some((b) => b.type === 'list' && (b.items || []).length > 0); - })); - -/* ── The switcher still works there ─────────────────────────────────────── */ - -/** - * An agent-less page is not a page with no switcher. - * - * The list is the whole registry, ordered so the agents that work here come - * first — which on these pages is the general agent, because it is the one that - * covers them. - */ -record('the switcher lists every agent on an agent-less page', - AGENTLESS_CONTEXTS.every((contextId) => - agentReg.searchAgents(ALL_AGENTS, '').length === ALL_AGENTS.length), - `${ALL_AGENTS.length} agents, on every page`); - -record('the general agent sorts to the top of the list on an agent-less page', - AGENTLESS_CONTEXTS.every((contextId) => { - const ordered = [...agentReg.searchAgents(ALL_AGENTS, '')] - .sort((a, b) => Number(runtime.agentCovers(b, contextId)) - Number(runtime.agentCovers(a, contextId))); - return ordered[0]?.id === GENERAL; - }), - 'the one that can answer here is offered first'); - -record('choosing an agent on an agent-less page still cannot move the page', - AGENTLESS_CONTEXTS.every((contextId) => ALL_AGENTS.every((candidate) => - runtime.resolveAgentForTurn(ALL_AGENTS, candidate.id, contextId).agent?.id === candidate.id))); - -/* ── 21. Owliver behaviour baseline ─────────────────────────────────────── - * - * The Agent layer is additive, which is a claim rather than a guarantee: every - * page's skills, suggestions, prompts and intent routing must survive it - * unchanged. That is far more than fits in a reviewer's head, and all of it - * fails silently — a page quietly answering with one skill fewer looks exactly - * like a page that never had it. - * - * So today's behaviour was recorded before any of it was built - * (`scripts/__baseline__/owliver-baseline.json`) and is compared here, per - * context and per question. `captureBaseline` is the same function that wrote - * the file, so there is one definition of what is measured. - * - * A failure here is a regression in the existing product, not a stale - * expectation. The file is regenerated deliberately, never to turn a check - * green. - */ -console.log('\n── Owliver behaviour baseline ──'); - -if (!existsSync(BASELINE_PATH)) { - record('baseline snapshot exists', false, 'run `node scripts/owliver-baseline.mjs --write`'); -} else { - const expected = JSON.parse(readFileSync(BASELINE_PATH, 'utf8')); - const actual = await captureBaseline(server); - - const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); - - record('baseline schema matches', expected.schema === actual.schema, - `expected ${expected.schema}, got ${actual.schema}`); - - /** - * Additive is allowed; losing something is not. - * - * The snapshot records the product as it was before the agent layer existed, - * and later phases legitimately add skills — that is what they are for. So a - * strict equality here would fail on every intended change and force the - * snapshot to be regenerated, which is exactly how a baseline stops catching - * anything. - * - * The contract is therefore a *subset*: every skill that existed then must - * still exist now. A skill disappearing is a regression and still fails. - * What must not move at all — routes, page keys, suggested prompts and intent - * routing — is asserted strictly below, and those are the checks that catch a - * new definition quietly taking over an existing question. - */ - const lostSkills = expected.skillIds.filter((id) => !actual.skillIds.includes(id)); - const gainedSkills = actual.skillIds.filter((id) => !expected.skillIds.includes(id)); - record('every skill that existed before the agent layer still registers', - lostSkills.length === 0, - lostSkills.length - ? `LOST ${JSON.stringify(lostSkills)}` - : `${expected.skillIds.length} kept, ${gainedSkills.length} added since`); - - record('registry still reports no diagnostics', actual.diagnostics.length === 0, - actual.diagnostics.map((d) => d.message).join(' | ') || 'none'); - - /** - * Route → context: every address that resolved before must resolve the same - * way now. - * - * Additive, for the same reason page skills are. A later phase can mount - * Owliver somewhere new — the agent configuration screen is the first — and a - * strict equality would fail on that intended addition and force the snapshot - * to be regenerated, which is how a baseline stops catching anything. - * - * What must never happen is an *existing* route resolving somewhere else, or - * ceasing to resolve at all: that is one of the eight operational pages - * quietly changing which assistant it carries. Both are failures here. - */ - const movedRoutes = Object.entries(expected.routes) - .filter(([route, contextId]) => actual.routes[route] !== contextId) - .map(([route, contextId]) => `${route}: ${contextId} → ${actual.routes[route] ?? 'nothing'}`); - const addedRoutes = Object.keys(actual.routes).filter((route) => !(route in expected.routes)); - - record('every route that resolved before resolves the same way', - movedRoutes.length === 0, - movedRoutes.join(' | ') - || `${Object.keys(expected.routes).length} unchanged${addedRoutes.length ? `, added ${JSON.stringify(addedRoutes)}` : ''}`); - - /* Per context, so a failure names the page it broke rather than reporting - that "something" moved. */ - for (const contextId of Object.keys(expected.contexts)) { - const want = expected.contexts[contextId]; - const got = actual.contexts[contextId]; - const page = contextId.replace(/^admin\./, ''); - - if (!got) { - record(`${page}: context still exists`, false, 'context missing from the registry'); - continue; - } - - record(`${page}: page key and route unchanged`, - want.pageKey === got.pageKey && want.route === got.route, - `${got.pageKey} @ ${got.route}`); - - /* The page boundary itself. A page may gain skills as later phases add - them; it may never lose one, because that is a capability the page had - and silently stopped offering. */ - const lost = want.skills.filter((id) => !got.skills.includes(id)); - const gained = got.skills.filter((id) => !want.skills.includes(id)); - record(`${page}: keeps every skill it had`, lost.length === 0, - lost.length ? `LOST ${JSON.stringify(lost)}` - : gained.length ? `${want.skills.length} kept, gained ${JSON.stringify(gained)}` - : `${got.skills.length} skill(s), unchanged`); - - const lostSuggestions = want.suggestions.filter((label) => !got.suggestions.includes(label)); - record(`${page}: keeps every suggestion it offered`, lostSuggestions.length === 0, - lostSuggestions.length ? `LOST ${JSON.stringify(lostSuggestions)}` - : `${want.suggestions.length} kept, ${got.suggestions.length - want.suggestions.length} added`); - - record(`${page}: suggested prompts unchanged`, same(want.prompts, got.prompts), - same(want.prompts, got.prompts) - ? `${got.prompts.length} prompt(s)` - : `expected ${JSON.stringify(want.prompts)}, got ${JSON.stringify(got.prompts)}`); - - /* Intent routing, question by question — skill matching, the branch taken, - and the shape of the answer. */ - const drifted = want.intents.filter((w, i) => !same(w, got.intents[i])); - record(`${page}: intent routing unchanged`, drifted.length === 0, - drifted.length === 0 - ? `${want.intents.length} question(s)` - : drifted.map((d) => `"${d.question}"`).join(', ')); - } -} - -await server.close(); - -/* ── 8. Production bundle ─────────────────────────────────────────────────── */ -if (process.argv.includes('--dist')) { - console.log('\n── Production bundle ──'); - const dir = join(ROOT, 'dist/assets'); - if (!existsSync(dir)) { - record('dist/assets exists', false, 'run `npm run build` first'); - } else { - const bundle = readdirSync(dir) - .filter((f) => f.endsWith('.js')) - .map((f) => readFileSync(join(dir, f), 'utf8')) - .join('\n'); - - for (const s of reg.SKILLS) { - record(`bundle carries \`${s.id}\``, bundle.includes(s.id)); - } - /* Body content, not just the id — a manifest naming a skill whose Markdown - did not survive is the failure this is looking for. */ - record( - 'bundle carries skill body text', - bundle.includes('Which client is this role for'), - 'create-position conversation step found in bundle' - ); - } -} - -/* ── Summary ──────────────────────────────────────────────────────────────── */ -const failed = results.filter((r) => !r.pass); -console.log(`\n${results.length - failed.length}/${results.length} checks passed`); -if (failed.length) { - console.log('\nFailed:'); - failed.forEach((r) => console.log(` - ${r.name}${r.detail ? ` (${r.detail})` : ''}`)); - process.exit(1); -} diff --git a/skill-examples/board-invalid-context.md b/skill-examples/board-invalid-context.md deleted file mode 100644 index 3475ff8..0000000 --- a/skill-examples/board-invalid-context.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: board-invalid-context -name: Board (invalid context) -description: Negative test — a position-scoped source on a placement that supplies no position. -pages: - - positions -status: active -ui: - type: card - placement: after-position-list-summary - title: Board - source: position.activity - periods: - - today - - last-7-days - - previous-month ---- - -# Board (invalid context) - -## Purpose - -Deliberately wrong, and kept so the refusal stays tested. `position.activity` -needs one position to read; `after-position-list-summary` renders once above the -grid with no position in context. Saving this must be refused with a message that -names both halves of the mismatch. diff --git a/skill-examples/board-page-level.md b/skill-examples/board-page-level.md deleted file mode 100644 index 70ee7c1..0000000 --- a/skill-examples/board-page-level.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -id: board -name: Board -description: Helps Owliver understand, analyze, and act on the current task board. -pages: - - positions -status: active -ui: - type: card - placement: after-position-list-summary - title: Board - source: candidates.activity - periods: - - today - - last-7-days - - previous-month ---- - -# Board - -## Purpose - -Helps Owliver understand the current task board by summarizing relevant work activity, identifying items that need attention, and explaining the current status of tasks shown on the board. - -## Capabilities - -- Summarize relevant activity from the current board. -- Identify tasks or work items that need attention. -- Explain the current status of tasks and work items. -- Highlight recent activity and meaningful changes. -- Help Owliver analyze board activity using the available board data. diff --git a/skill-examples/board-position-card.md b/skill-examples/board-position-card.md deleted file mode 100644 index 749942e..0000000 --- a/skill-examples/board-position-card.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -id: board-position-card -name: Board (per position) -description: Control test — the same Board card, rendered inside every position card. -pages: - - positions -status: active -ui: - type: card - placement: grid-card - title: Board - source: position.activity - periods: - - today - - last-7-days - - previous-month ---- - -# Board (per position) - -## Purpose - -The position-aware control for the page-level Board card. `grid-card` resolves to -`after-position-card`, which renders inside each position card and supplies that -position as context — so `position.activity` reads and the card is drawn once per -role rather than once for the page. - -## Capabilities - -- Summarize activity for the position the card belongs to. -- Show that activity across today, the last 7 days and the previous month. diff --git a/skill-examples/board-ui-test.md b/skill-examples/board-ui-test.md deleted file mode 100644 index 150e53e..0000000 --- a/skill-examples/board-ui-test.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: board-ui-test -name: Board -description: Renders the Board card once above the position list on the Positions page. -pages: - - positions -status: active -ui: - type: card - placement: after-position-list-summary - title: Board - source: candidates.activity - periods: - - today - - last-7-days - - previous-month ---- - -# Board - -## Purpose - -The page half of the Board, and only that half. One card, above the position -grid, reading activity across the workspace. - -`after-position-list-summary` renders once for the page and supplies no -position — `surfaces.js` records that placement as providing nothing — so the -card reads `candidates.activity`, which declares `context: null` and counts -applications across the workspace over the periods named here. -`position.activity` is refused at this placement by `unresolvableSections`, and -correctly: it needs one position, and this placement has none to give. - -This definition declares no `owliver:` block, so it contributes no capability, -no suggestion chip and no conversational answer. It is a page extension and -nothing else. - -## Capabilities - -- Show workspace application activity above the position list. -- Compare today, the last 7 days and the previous month in one card. diff --git a/skill-examples/owliver-conversation-test.md b/skill-examples/owliver-conversation-test.md deleted file mode 100644 index 77053a6..0000000 --- a/skill-examples/owliver-conversation-test.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -id: owliver-conversation-test -name: Workspace Activity Assistant -description: Answer questions about applications across the workspace and the roles still open. -pages: - - positions -status: active -triggers: - - workspace applications - - applications across the workspace - - roles still open - - roles needing attention -owliver: - enabled: true - # Every suggestion names the capability it asks for, so a chip and an answer - # are one-to-one rather than both falling through to the first declared - # capability. - suggestions: - - label: Summarize workspace applications - capability: summary - - label: List the roles still open - capability: list - capabilities: - - summary - - list - responses: - summary: - title: Workspace Applications - source: candidates.activity - periods: - - today - - last-7-days - - previous-month - list: - title: Roles Still Open - source: positions.demand - limit: 5 ---- - -# Workspace Activity Assistant - -## Purpose - -The panel half, and only that half. No `ui:` block, so nothing is drawn on the -Positions page and no card appears above or inside the grid. - -Each response names its own `source:` directly, which is what a definition -without a `ui:` section must do — `normalizeSkillOwliver` inherits a source from -the first `ui:` section only when one exists, and reports -`owliver.responses.: a response needs a source, or a ui: section to -read from` when neither is present. - -Both sources declare `context: null`, so neither answer asks which position is -meant: `resolveEntity` returns immediately and no -"This section needs a position to read" is possible. The shapes match what each -source offers — `summary` is prose and is exempt from the shape check, and -`list` is a shape `positions.demand` declares. - -## Capabilities - -- Summarize applications across the workspace over today, the last 7 days and - the previous month. -- List the open positions and how many applicants each has. diff --git a/src/App.jsx b/src/App.jsx index 6378df3..3f7b6f0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,67 +1,59 @@ -import { Toaster } from "@/components/ui/toaster" +import { Suspense, lazy } from 'react'; +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 { QueryClientProvider } from '@tanstack/react-query'; +import { queryClientInstance } from '@/lib/query-client'; +import { Navigate, BrowserRouter as Router, Route, Routes } 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 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 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'; +import Login from '@/pages/Login'; -/** `/university/:id` → the same challenge inside the Admin shell, id preserved. */ -const LegacyCourseRedirect = () => { - const { id } = useParams(); - return ; -}; +/* Every console page is code-split. The Dispatch board alone pulls the map, + the solver payload builders and the comparison panels; loading that on the + way to the Hubs table would make every first paint pay for it. */ +const Dispatch = lazy(() => import('@/pages/doormile/dispatch/Dispatch.jsx')); +const DispatchPreview = lazy(() => import('@/pages/doormile/dispatch/Preview.jsx')); -/** `/positions/:id` → the same position inside the Admin shell, id preserved. */ -const LegacyPositionRedirect = () => { - const { id } = useParams(); - return ; -}; +const Orders = lazy(() => import('@/pages/doormile/orders/Orders')); +const OrdersPreview = lazy(() => import('@/pages/doormile/orders/OrdersPreview')); +const CreateOrder = lazy(() => import('@/pages/doormile/orders/CreateOrder')); +const MultipleOrders = lazy(() => import('@/pages/doormile/orders/MultipleOrders')); + +const Deliveries = lazy(() => import('@/pages/doormile/deliveries/Deliveries')); + +const Tenants = lazy(() => import('@/pages/doormile/clients/Tenants')); +const CreateClient = lazy(() => import('@/pages/doormile/clients/CreateClient')); +const CreateCustomer = lazy(() => import('@/pages/doormile/clients/CreateCustomer')); + +const Pricing = lazy(() => import('@/pages/doormile/pricing/Pricing')); + +const Riders = lazy(() => import('@/pages/doormile/riders/Riders')); +const CreateRider = lazy(() => import('@/pages/doormile/riders/CreateRider')); +const EditRider = lazy(() => import('@/pages/doormile/riders/EditRider')); + +const Customers = lazy(() => import('@/pages/doormile/customers/Customers')); +const Hubs = lazy(() => import('@/pages/doormile/hubs/Hubs')); +const Vehicles = lazy(() => import('@/pages/doormile/vehicles/Vehicles')); +const AppUsers = lazy(() => import('@/pages/doormile/appUsers/AppUsers')); +const Tripsheets = lazy(() => import('@/pages/doormile/tripsheets/Tripsheets')); +const Exceptions = lazy(() => import('@/pages/doormile/exceptions/Exceptions')); +const CompetitiveIntel = lazy(() => import('@/pages/doormile/competitiveIntel/CompetitiveIntel')); + +const OrdersSummary = lazy(() => import('@/pages/doormile/reports/OrdersSummary')); +const OrdersDetails = lazy(() => import('@/pages/doormile/reports/OrdersDetails')); +const RidersSummary = lazy(() => import('@/pages/doormile/reports/RidersSummary')); +const Profitability = lazy(() => import('@/pages/doormile/reports/Profitability')); + +const ViewProfile = lazy(() => import('@/pages/doormile/ViewProfile')); + +const PageFallback = () => ( +

+
+
+); const AuthenticatedApp = () => { const { isLoadingAuth, isLoadingPublicSettings } = useAuth(); @@ -75,83 +67,62 @@ const AuthenticatedApp = () => { } return ( - - {/* Admin sign-in — outside the protected boundary. */} - } /> + }> + + {/* Sign-in — outside the protected boundary. */} + } /> - }> - {/* 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. */} - } /> + }> + {/* Dispatch is the console's home: it is the board operators sit on + all day, and the one page that answers "what is happening right + now" without a filter being set first. */} + } /> - }> - }> - } /> - } /> - } /> - } /> - } /> - {/* 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. */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* 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. */} - } /> - } /> - } /> - {/* Static before dynamic, so `agents/new` cannot be read as an - agent whose id is "new". */} - } /> - } /> - } /> - {/* 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. */} - } /> - } /> - } /> - } /> - } /> + }> + } /> + + } /> + } /> + + {/* Static before dynamic, so `orders/create` cannot be read as an + order whose id is "create". */} + } /> + } /> + } /> + } /> + + } /> + + } /> + } /> + } /> + + } /> + + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + + } /> - {/* Redirect top-level legacy aliases to the unified global shell */} - } /> - } /> - {/* `/positions` only matches the exact path, so the sub-routes need - their own aliases or an inbound link lands on the 404 page. */} - } /> - } /> - } /> - } /> - } /> - } /> - {/* 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. */} - } /> - } /> - } /> - } /> - - } /> - + } /> + + ); }; @@ -165,13 +136,12 @@ function App() { {/* 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. */} + its own renderer mounted. Without this every toast in the app fires + into nothing — including the data layer's failure notices. */} - ) + ); } -export default App +export default App; diff --git a/src/agents/activity-agent.md b/src/agents/activity-agent.md deleted file mode 100644 index a8e3dad..0000000 --- a/src/agents/activity-agent.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: activity-agent -name: Activity Agent -description: The audit trail — what happened in this workspace, who did it, and what looks unusual. -icon: activity -status: published -version: 1 -reasoning: balanced -trigger: Use on Activity, for the event log, who did what, and anything that looks out of pattern. -pages: - - activity -skills: - - activity-analysis - - anomaly-detection - - operational-risk -starters: - - label: What happened recently? - prompt: What has happened in the workspace recently? - - label: Anything unusual? - prompt: Is there any unusual activity? -permissions: - owner: demo@krow.app - access: all ---- - -# Activity Agent - -## Instructions - -Answer about what has happened in this workspace: which events, by which -account, and when. - -Report something as unusual only when it genuinely departs from the pattern in -the log. Flagging ordinary activity trains the reader to ignore the flag. - -This agent carries no skills of its own; Activity answers from its own page -reader. - -## Purpose - -- Report recent workspace events and who performed them. -- Surface activity that departs from the usual pattern. diff --git a/src/agents/analytics-agent.md b/src/agents/analytics-agent.md deleted file mode 100644 index 8797eea..0000000 --- a/src/agents/analytics-agent.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: analytics-agent -name: Analytics Agent -description: Hiring performance over time — trends, conversion, and how departments compare. -icon: bar-chart -status: published -version: 1 -reasoning: balanced -trigger: Use on Analytics, for trends over time, conversion rates and department comparisons. -pages: - - analytics -skills: - - analytics-insights - - workforce-analytics - - attendance-analysis - - overtime-analysis - - hiring-pulse-analysis -starters: - - label: What is the hiring trend? - prompt: What is the hiring trend? - - label: Where does the funnel lose people? - prompt: Where does the funnel lose candidates? -permissions: - owner: demo@krow.app - access: all ---- - -# Analytics Agent - -## Instructions - -Answer about performance over time: how hiring is trending, where the funnel -converts and where it leaks, and how departments compare. - -Explain the figures the Analytics page is already showing rather than producing -different ones. When a movement is small enough to be noise, say so rather than -narrating it as a trend. - -## Purpose - -- Explain hiring trend and conversion. -- Compare department performance, and identify where the funnel loses people. diff --git a/src/agents/candidates-agent.md b/src/agents/candidates-agent.md deleted file mode 100644 index 51f0b60..0000000 --- a/src/agents/candidates-agent.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: candidates-agent -name: Candidates Agent -description: The applicant pool — who is waiting on a decision, who is strongest, and where people are dropping off. -icon: users -status: published -version: 1 -reasoning: balanced -trigger: Use on Candidates, for screening, shortlisting and pipeline questions about applicants. -pages: - - candidates - - candidates-analysis -skills: - - candidate-search - - candidate-analysis -starters: - - label: Who needs a decision? - prompt: Which candidates are waiting on a decision? - - label: Who is strongest? - prompt: Who are the strongest candidates right now? -permissions: - owner: demo@krow.app - access: all ---- - -# Candidates Agent - -## Instructions - -Answer about the people who have applied: who is waiting, who scores well, who -has not been screened, and where the pipeline is losing candidates. - -Quote a score only where one has been computed. An unscored candidate is -unscored — say so rather than implying a low score. - -Never advance, decline or hire a candidate without being asked to. - -## Purpose - -- Report who is waiting on a decision, and who is strongest. -- Find candidates matching what a role asks for. diff --git a/src/agents/control-center-agent.md b/src/agents/control-center-agent.md deleted file mode 100644 index c67ab19..0000000 --- a/src/agents/control-center-agent.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -id: control-center-agent -name: Control Center Agent -description: The operational picture — what needs attention across the workspace today. -icon: layers -status: published -version: 1 -reasoning: balanced -trigger: Use on the Control Center, for workspace health, urgency and what to do next. -pages: - - control-center -skills: - - executive-summary - - staffing-risk - - operational-risk - - anomaly-detection - - attendance-analysis - - overtime-analysis - - hiring-pulse-analysis -starters: - - label: What needs my attention? - prompt: What needs my attention right now? - - label: How is the pipeline? - prompt: How healthy is my hiring pipeline? -permissions: - owner: demo@krow.app - access: all ---- - -# Control Center Agent - -## Instructions - -Answer about the state of the workspace as a whole: what is urgent, where the -funnel is losing people, and what the reader should do next. - -Read the figures the Control Center already shows rather than recomputing them, -so the answer and the dashboard beside it can never disagree. - -This agent carries no skills of its own. That is deliberate — the Control -Center answers from its own page reader, and inventing skills to fill the list -would promise capabilities that do not exist. - -## Purpose - -- Say what needs attention across the workspace. -- Explain where the hiring funnel is losing candidates. diff --git a/src/agents/hired-history-agent.md b/src/agents/hired-history-agent.md deleted file mode 100644 index d43f204..0000000 --- a/src/agents/hired-history-agent.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: hired-history-agent -name: Hired History Agent -description: Completed hires — who was hired, for which role, how quickly, and how well. -icon: user-check -status: published -version: 1 -reasoning: balanced -trigger: Use on Hired History, for hiring outcomes, time-to-hire and quality by department. -pages: - - hired-history -skills: - - hiring-history-analysis -starters: - - label: Who did we hire recently? - prompt: Who did we hire recently? - - label: How is hire quality? - prompt: How is hire quality by department? -permissions: - owner: demo@krow.app - access: all ---- - -# Hired History Agent - -## Instructions - -Answer about hires that have already happened: who, for which role, how long it -took and how they scored. - -This is the record after the decision, not the pipeline before it. A question -about people still being considered belongs to Candidates. - -This agent carries no skills of its own. Hired History answers from its own -page reader, and a placeholder skill would promise a capability that does not -exist. - -## Purpose - -- Report recent hires, and how quickly they were made. -- Compare hiring outcomes across departments. diff --git a/src/agents/krow-forge-agent.md b/src/agents/krow-forge-agent.md deleted file mode 100644 index f3dcfa7..0000000 --- a/src/agents/krow-forge-agent.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: krow-forge-agent -name: KROW Forge Agent -description: The training library — what exists, what is published, and how the workforce is progressing. -icon: graduation-cap -status: published -version: 1 -reasoning: balanced -trigger: Use on KROW Forge, for training paths, challenges, verification and skill progression. -pages: - - krow-forge -skills: - - forge-skill-management - - learning-analysis -starters: - - label: What is in the library? - prompt: What training does the library hold? - - label: Where are the gaps? - prompt: Where are the gaps in workforce training? -permissions: - owner: demo@krow.app - access: all ---- - -# KROW Forge Agent - -## Instructions - -Answer about the training library and what the workforce has proved: which -paths exist, which are published, what a challenge checks, and where coverage -is thin. - -A skill in Forge is something a person learns and is verified in. It is not an -Owliver capability — never describe the two as the same thing. - -Never publish or archive training without being asked to. - -## Purpose - -- Report what the training library holds and what is live. -- Identify gaps between what roles need and what is taught. diff --git a/src/agents/krow-workforce-agent.md b/src/agents/krow-workforce-agent.md deleted file mode 100644 index 13e909e..0000000 --- a/src/agents/krow-workforce-agent.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -id: krow-workforce-agent -name: Krow Workforce Agent -description: The general workforce agent. Reasons across every Krow domain, within whatever page you are on. -icon: owliver -status: published -version: 1 -reasoning: balanced -trigger: Use when a question spans more than one Krow domain, or when you are on a page whose own agent cannot help. -pages: - - control-center - - positions - - create-position - - candidates - - candidates-analysis - - hired-history - - talent-pool - - krow-forge - - analytics - - activity - - profile - # The agent workspace. Carries no operational skill, so standing here the - # root agent answers about agents and skills and nothing else — which is the - # point: configuring the Analytics Agent must not put the reader on Analytics. - - workspace-agent-configure - # Settings and the rest of the workspace. Nobody wrote a specialist for a - # configuration screen and nobody should: these pages hold no workforce - # records, so what they need is a general agent, not a Settings Agent with - # invented skills. Listing them here is the whole of the fallback — a page - # named by this agent has an agent, and Owliver is alive on it. - - settings - - workspace - - workspace-agents - - workspace-skills - - workspace-skill-configure - - skill-development -skills: - - create-position - - hiring-activity-assistant - - candidate-search - - analytics-insights - - forge-skill-management - - staffing-risk - - attendance-analysis - - overtime-analysis - - candidate-analysis - - talent-pool-analysis - - workforce-analytics - - anomaly-detection - - activity-analysis - - operational-risk - - executive-summary - - hiring-history-analysis - - learning-analysis - - hiring-pulse-analysis -subagents: - - control-center-agent - - positions-agent - - candidates-agent - - hired-history-agent - - talent-pool-agent - - krow-forge-agent - - analytics-agent - - activity-agent -knowledge: - - id: page-boundary - label: What this agent can see - kind: note - body: Owliver answers from the page you are on. Covering every page does not mean reading every page at once — the page you are standing on decides which records are in reach. -starters: - - label: What needs my attention? - prompt: What needs my attention right now? - - label: Summarize this page - prompt: Summarize what this page is showing -permissions: - owner: demo@krow.app - access: all - people: - - user: demo@krow.app - role: manager ---- - -# Krow Workforce Agent - -## Instructions - -Answer from the records this workspace holds, for the page the reader is on. - -State a figure only where a skill has read it. When a reading needs a position -or a candidate and none is open, ask which one rather than choosing one. - -Covering every page is not permission to read every page at once. The page in -front of the reader decides what is in reach; a question that belongs somewhere -else should be answered by naming where it belongs, not by reaching for it. - -## Purpose - -- Answer questions that span more than one Krow domain. -- Stand in on pages whose own agent carries no skills. -- Hand a question that clearly belongs to another page back to that page. diff --git a/src/agents/positions-agent.md b/src/agents/positions-agent.md deleted file mode 100644 index 9fbc695..0000000 --- a/src/agents/positions-agent.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: positions-agent -name: Positions Agent -description: Open roles — what they need, who has applied, and which are at risk of going unfilled. -icon: briefcase -status: published -version: 1 -reasoning: balanced -trigger: Use on Positions, for open roles, applicant flow, and specifying a new role. -pages: - - positions - - create-position -skills: - - create-position - - hiring-activity-assistant - - staffing-risk -starters: - - label: Which positions need attention? - prompt: Which positions need attention? - - label: Show hiring activity - prompt: Show hiring activity as a flow -permissions: - owner: demo@krow.app - access: all ---- - -# Positions Agent - -## Instructions - -Answer about the roles this workspace has open: how they are filling, which are -starved of applicants, and what a role still needs before it can be published. - -When a question names a role, answer about that role. When it does not and one -is open on the page, answer about that one. When neither is true, ask which. - -Never create or publish a position without being asked to. - -## Purpose - -- Report how open roles are filling, and which are at risk. -- Help specify a new role and its screening weights. diff --git a/src/agents/talent-pool-agent.md b/src/agents/talent-pool-agent.md deleted file mode 100644 index 16ecd86..0000000 --- a/src/agents/talent-pool-agent.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: talent-pool-agent -name: Talent Pool Agent -description: Available talent — who is in the pool, who is verified, and who is ready to place. -icon: layers -status: published -version: 1 -reasoning: balanced -trigger: Use on Talent Pool, for supply, availability and readiness of known workers. -pages: - - talent-pool -skills: - - talent-pool-analysis -starters: - - label: Who is available? - prompt: Who is available in the talent pool? - - label: How verified is the pool? - prompt: How much of the talent pool is verified? -permissions: - owner: demo@krow.app - access: all ---- - -# Talent Pool Agent - -## Instructions - -Answer about the people this workspace already knows: who is in the pool, what -they are verified in, and who could be placed now. - -This is supply, not applicants. Someone in the pool has not applied to anything -by being here — do not describe them as a candidate for a role. - -This agent carries no skills of its own; Talent Pool answers from its own page -reader. - -## Purpose - -- Report who is available, and how ready they are. -- Describe the pool's segments and verification coverage. diff --git a/src/api/aiEngine.js b/src/api/aiEngine.js deleted file mode 100644 index 32268a6..0000000 --- a/src/api/aiEngine.js +++ /dev/null @@ -1,809 +0,0 @@ -/** - * Local AI engine for the KROW demo. - * - * The app funnels every AI workflow through a single call — - * `integrations.Core.InvokeLLM({ prompt, response_json_schema, model })` — so - * reimplementing that one function keeps `lib/krowAi.js` and - * `lib/provingGround.js` byte-for-byte identical to the reference. - * - * Each workflow is recognized by a stable phrase its prompt opens with, and the - * structured fields the prompt already carries ("Years Experience: 6", - * "Required Certifications: …", the talent-pool JSON block) are read back out - * and scored deterministically. Same input, same output — no network, no key, - * and results that actually respond to the data instead of being canned. - */ - -const clamp = (n, min = 0, max = 100) => Math.max(min, Math.min(max, Math.round(Number(n) || 0))); - -/** Simulated model latency so streaming/pending UI behaves as designed. */ -const think = (ms = 900) => new Promise((resolve) => setTimeout(resolve, ms)); - -/* ── Prompt field readers ──────────────────────────────────────────────── */ - -/** Reads a `Label: value` line out of a prompt. */ -function field(prompt, label) { - const match = prompt.match(new RegExp(`^${label}:[ \\t]*(.*)$`, 'mi')); - return match ? match[1].trim() : ''; -} - -function numField(prompt, label) { - const raw = field(prompt, label); - const match = raw.match(/-?\d+(\.\d+)?/); - return match ? Number(match[0]) : 0; -} - -/** Reads a comma-separated line, treating the app's "None" sentinel as empty. */ -function listField(prompt, label) { - const raw = field(prompt, label); - if (!raw || /^(none|not specified|not provided)$/i.test(raw)) return []; - return raw.split(',').map((s) => s.trim()).filter(Boolean); -} - -/** Pulls the first triple-quoted block (transcripts, free-text resumes). */ -function quotedBlock(prompt) { - const match = prompt.match(/"""\s*([\s\S]*?)\s*"""/); - return match ? match[1].trim() : ''; -} - -const ENGLISH_RANK = { basic: 1, conversational: 2, fluent: 3, native: 4 }; - -const isSpanish = (prompt) => /Spanish \(español\)/.test(prompt) && !/default to English/.test(prompt); - -/* ── 1. Candidate screening ────────────────────────────────────────────── */ - -function screenCandidate(prompt) { - // Weights are stated inline: "experience 25%, english 20%, …" - const weightOf = (name) => { - const match = prompt.match(new RegExp(`${name} (\\d+)%`)); - return match ? Number(match[1]) : 20; - }; - const weights = { - experience: weightOf('experience'), - english: weightOf('english'), - reliability: weightOf('reliability'), - certifications: weightOf('certifications'), - availability: weightOf('availability'), - }; - - const minYears = numField(prompt, 'Min Experience'); - const requiredEnglish = ENGLISH_RANK[field(prompt, 'English Required').toLowerCase()] || 1; - const requiredCertNames = listField(prompt, 'Required Certifications'); - const requiredCerts = requiredCertNames.map((c) => c.toLowerCase()); - const customRequirements = field(prompt, 'Custom Requirements'); - - const name = field(prompt, 'Name'); - const years = numField(prompt, 'Years Experience'); - const englishLevel = ENGLISH_RANK[field(prompt, 'English Level').toLowerCase()] || 1; - const certs = listField(prompt, 'Certifications'); - const skills = listField(prompt, 'Skills'); - const availability = listField(prompt, 'Availability'); - const summary = field(prompt, 'Professional Summary'); - const hasSummary = Boolean(summary) && !/^not provided$/i.test(summary); - - // Experience: meeting the minimum is a pass, exceeding it earns headroom. - const experience = minYears > 0 - ? clamp(years >= minYears ? 78 + Math.min(21, (years - minYears) * 4) : (years / minYears) * 70) - : clamp(45 + years * 8); - - // English: at or above the requirement scores well, each level short costs. - const english = clamp(englishLevel >= requiredEnglish - ? 84 + (englishLevel - requiredEnglish) * 5 - : 84 - (requiredEnglish - englishLevel) * 24); - - // Certifications: fraction of required credentials actually held. - const held = requiredCerts.filter((req) => - certs.some((c) => c.toLowerCase().includes(req) || req.includes(c.toLowerCase()))); - const certifications = requiredCerts.length - ? clamp((held.length / requiredCerts.length) * 92 + (certs.length > requiredCerts.length ? 8 : 0)) - : clamp(certs.length ? 88 : 55); - - const availabilityScore = clamp(availability.length ? 52 + availability.length * 14 : 40); - - // Reliability has no direct field — infer it from tenure, credentials and - // whether the candidate bothered to describe their own work. - const reliability = clamp( - 45 + Math.min(30, years * 4) + (certifications > 70 ? 14 : 0) + (hasSummary ? 8 : 0) - ); - - const weightTotal = Object.values(weights).reduce((a, b) => a + b, 0) || 100; - const overall = clamp( - (experience * weights.experience + - english * weights.english + - reliability * weights.reliability + - certifications * weights.certifications + - availabilityScore * weights.availability) / weightTotal - ); - - const skillDepth = clamp(skills.length ? 55 + skills.length * 9 : 30); - const seniority = clamp(years * 8); - - const label = overall >= 85 ? 'Excellent Match' - : overall >= 70 ? 'Good Match' - : overall >= 50 ? 'Possible Fit' - : 'Not a Fit'; - const recommendation = overall >= 85 ? 'Shortlisted' - : overall >= 65 ? 'Interview' - : overall >= 50 ? 'Maybe' - : 'Reject'; - - const strengths = []; - if (years >= Math.max(1, minYears)) strengths.push(`${years} year${years === 1 ? '' : 's'} of relevant experience`); - if (held.length) strengths.push(`${held.length === requiredCerts.length ? 'All' : 'Some'} required certifications on file`); - if (englishLevel >= requiredEnglish) strengths.push(`${field(prompt, 'English Level') || 'Adequate'} English meets the requirement`); - if (availability.length >= 2) strengths.push(`Broad availability (${availability.join(', ')})`); - if (skills.length >= 3) strengths.push(`Relevant skills: ${skills.slice(0, 3).join(', ')}`); - if (!strengths.length) strengths.push('Available and actively seeking work'); - - const gaps = []; - if (minYears > 0 && years < minYears) gaps.push(`${minYears - years} year${minYears - years === 1 ? '' : 's'} short of the experience minimum`); - // Report the credential using the posting's own capitalization. - const missing = requiredCertNames.filter((c) => !held.includes(c.toLowerCase())); - if (missing.length) gaps.push(`Missing certification${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`); - if (englishLevel < requiredEnglish) gaps.push('English level below the posting requirement'); - if (availability.length <= 1) gaps.push('Narrow availability window'); - if (!skills.length) gaps.push('No skills listed on the application'); - if (!hasSummary) gaps.push('No professional summary provided'); - if (!gaps.length) gaps.push('No material gaps against this posting'); - - const verdictSentence = overall >= 85 - ? `Strong fit for this posting.` - : overall >= 70 - ? `Meets the core requirements for this posting.` - : overall >= 50 - ? `Partial fit — worth a conversation, with caveats.` - : `Below the requirement bar for this posting.`; - - return { - overall_score: overall, - match_label: label, - summary: `${verdictSentence} ${name || 'The candidate'} brings ${years} year${years === 1 ? '' : 's'} of experience with ${certs.length ? certs.join(' and ') : 'no listed certifications'}. ${gaps[0]}.`, - strengths: strengths.slice(0, 4), - gaps: gaps.slice(0, 3), - recommendation, - score_breakdown: { - experience, - english, - reliability, - certifications, - availability: availabilityScore, - personality: clamp(58 + skillDepth * 0.25 + (hasSummary ? 10 : 0)), - culture_fit: clamp(overall * 0.9 + 6), - communication_style: clamp(english * 0.85 + (hasSummary ? 12 : 0)), - attendance_expectations: clamp(reliability * 0.95 + availabilityScore * 0.1), - physical_requirements: clamp(70 + Math.min(20, years * 3)), - leadership_expectations: clamp(seniority * 0.8 + (/lead|captain|chef|supervisor|manager/i.test(summary) ? 22 : 0)), - job_related_answers: clamp(overall * 0.92 + skillDepth * 0.08), - verified_skills: skillDepth, - scenario_judgment: clamp(overall * 0.88 + (hasSummary ? 8 : 0)), - employer_requirements: clamp(customRequirements && !/^none$/i.test(customRequirements) - ? overall * 0.85 - : overall * 0.95), - }, - }; -} - -/* ── 2. Job description generator ──────────────────────────────────────── */ - -const RESPONSIBILITY_LIBRARY = { - Bartender: [ - 'Set up and break down a full service bar', - 'Pour to spec at event pace without sacrificing presentation', - 'Verify guest age and refuse service responsibly', - 'Track consumption and flag low stock before service', - 'Keep the bar clean and compliant throughout the shift', - 'Reconcile counts and close out the bar after service', - ], - Server: [ - 'Execute synchronized coursed service for seated events', - 'Polish and set glassware, china, and flatware', - 'Describe menus and handle allergen questions accurately', - 'Clear and reset tables discreetly between courses', - 'Support captains through arrivals, toasts, and departures', - 'Complete side work and floor close-out', - ], - Security: [ - 'Staff entry points and verify credentials', - 'Conduct bag checks per venue policy', - 'De-escalate guest conflicts calmly and early', - 'Document incidents accurately and promptly', - 'Coordinate with venue staff and local authorities', - 'Patrol assigned zones throughout the event', - ], - Chef: [ - 'Design and cost menus to target margins', - 'Lead and schedule the kitchen brigade', - 'Own food safety compliance across every kitchen', - 'Manage vendor relationships and food cost', - 'Execute tastings and client consultations', - 'Hold plate standards at volume', - ], - Picker: [ - 'Pick and stage orders against pick lists', - 'Scan and verify SKUs for accuracy', - 'Stage outbound pallets for dispatch', - 'Report damaged inventory immediately', - 'Keep aisles and staging lanes clear', - 'Meet daily accuracy and throughput targets', - ], -}; - -const DEFAULT_RESPONSIBILITIES = [ - 'Deliver consistent, guest-ready service every shift', - 'Set up and break down your station', - 'Communicate clearly with leads and teammates', - 'Follow safety and compliance standards', - 'Adapt to changing event timing without losing composure', - 'Complete close-out duties before leaving', -]; - -function generateJobDescription(prompt) { - const title = field(prompt, 'Role Title'); - const category = field(prompt, 'Role Category'); - const minYears = numField(prompt, 'Min Experience'); - const english = field(prompt, 'English Level Required'); - const certs = listField(prompt, 'Required Certifications'); - const payMin = numField(prompt, 'Pay Range'); - const payMatch = prompt.match(/Pay Range: \$(\d+(?:\.\d+)?)–\$(\d+(?:\.\d+)?)/); - const payMax = payMatch ? Number(payMatch[2]) : payMin; - const location = field(prompt, 'Location'); - const custom = field(prompt, 'Custom Requirements'); - const hasCustom = custom && !/^none$/i.test(custom); - const hasLocation = location && !/^not specified$/i.test(location); - const role = title || category || 'team member'; - - const payLine = payMax > 0 ? `$${payMin}–$${payMax}/hr` : 'a competitive hourly rate'; - const whereLine = hasLocation ? ` in ${location}` : ''; - - const description = [ - `We are hiring ${/^[aeiou]/i.test(role) ? 'an' : 'a'} ${role}${whereLine} for premium events where the details are the product. This is hands-on work with a team that plans carefully, briefs properly, and backs you up when the room gets busy.`, - `You will own your station from setup through close. ${minYears > 0 ? `We are looking for at least ${minYears} year${minYears === 1 ? '' : 's'} in a comparable role` : 'We will train the right person from the ground up'}, ${english ? `${english} English` : 'clear communication'}, and the judgment to make a call without waiting to be told. ${certs.length ? `${certs.join(' and ')} must be current.` : 'No specific certifications required.'}`, - `We pay ${payLine}, publish schedules ahead of time, and promote from within — most of our leads started on the floor. ${hasCustom ? `One thing worth flagging: ${custom}` : 'If you take pride in work guests notice, you will fit here.'}`, - ].join('\n\n'); - - const responsibilities = (RESPONSIBILITY_LIBRARY[category] || DEFAULT_RESPONSIBILITIES).slice(0, 6); - - const qualifications = [ - minYears > 0 - ? `${minYears}+ year${minYears === 1 ? '' : 's'} of ${category ? `${category.toLowerCase()} ` : ''}experience` - : 'Willingness to learn and take direction', - english ? `${english.charAt(0).toUpperCase()}${english.slice(1)} English` : 'Clear communication', - ...certs.map((c) => `${c} — current and verifiable`), - 'Reliable transportation and consistent attendance', - 'Comfortable on your feet for a full shift', - ].filter(Boolean).slice(0, 6); - - const niceToHaves = [ - `Prior ${hasLocation ? `${location} ` : ''}event experience`, - 'Bilingual', - 'Additional certifications beyond the requirement', - 'Experience training newer staff', - ].slice(0, 4); - - return { description, responsibilities, qualifications, nice_to_haves: niceToHaves }; -} - -/* ── 3. Resume builder ─────────────────────────────────────────────────── */ - -const SKILL_KEYWORDS = [ - ['bartend', 'Bartending'], ['cocktail', 'Cocktail preparation'], ['serve', 'Table service'], - ['server', 'Table service'], ['banquet', 'Banquet service'], ['cook', 'Line cooking'], - ['chef', 'Kitchen leadership'], ['kitchen', 'Kitchen operations'], ['security', 'Event security'], - ['guard', 'Access control'], ['warehouse', 'Warehouse operations'], ['forklift', 'Forklift operation'], - ['pick', 'Order picking'], ['host', 'Guest relations'], ['cashier', 'Cash handling'], - ['clean', 'Sanitation'], ['train', 'Training others'], ['lead', 'Team leadership'], - ['supervis', 'Supervision'], ['manage', 'People management'], ['wine', 'Wine service'], - ['catering', 'Catering operations'], ['inventory', 'Inventory control'], -]; - -const CERT_KEYWORDS = [ - ['servsafe', 'ServSafe'], ['tips', 'TIPS Certified'], ['rbs', 'RBS Alcohol Server'], - ['guard card', 'Guard Card'], ['cpr', 'CPR / First Aid'], ['first aid', 'CPR / First Aid'], - ['food handler', 'Food Handler Card'], ['forklift', 'Forklift Operator'], ['osha', 'OSHA 10'], -]; - -function buildResume(prompt) { - const text = quotedBlock(prompt); - const lower = text.toLowerCase(); - - const yearsMatch = lower.match(/(\d+)\s*(?:\+)?\s*(?:years?|yrs?)/); - const years = yearsMatch ? Number(yearsMatch[1]) : 0; - - const skills = [...new Set( - SKILL_KEYWORDS.filter(([key]) => lower.includes(key)).map(([, label]) => label) - )].slice(0, 8); - - const certifications = [...new Set( - CERT_KEYWORDS.filter(([key]) => lower.includes(key)).map(([, label]) => label) - )]; - - // A capitalized two-word opener is almost always the person naming themselves. - const nameMatch = text.match(/\b([A-Z][a-z]+ [A-Z][a-z]+)\b/); - const inferredName = nameMatch ? nameMatch[1] : ''; - - const primary = skills[0] || 'hospitality'; - const professionalSummary = text - ? `${years > 0 ? `${years} year${years === 1 ? '' : 's'} of` : 'Hands-on'} experience in ${primary.toLowerCase()}${skills[1] ? ` and ${skills[1].toLowerCase()}` : ''}. ${certifications.length ? `Holds ${certifications.join(', ')}.` : 'Ready to certify as required.'} Dependable, quick to learn, and comfortable in a fast-moving service environment.` - : ''; - - const coverLetter = text - ? `I am applying because this role matches what I already do well. ${years > 0 ? `Over ${years} year${years === 1 ? '' : 's'} I have` : 'I have'} worked in ${primary.toLowerCase()}${skills[1] ? `, with hands-on ${skills[1].toLowerCase()}` : ''}, and I show up ready for the shift I am given.\n\n${certifications.length ? `My ${certifications.join(' and ')} ${certifications.length > 1 ? 'are' : 'is'} current. ` : ''}I would welcome the chance to prove this on a trial shift rather than on paper.` - : ''; - - return { - inferred_name: inferredName, - years_experience: years, - skills, - certifications, - professional_summary: professionalSummary, - cover_letter: coverLetter, - }; -} - -/* ── 4. Interview questions ────────────────────────────────────────────── */ - -const INTERVIEW_QUESTIONS = { - en: [ - (role) => `Thanks for joining. To start — what drew you to ${role} work, and how long have you been doing it?`, - () => `Walk me through your busiest shift. What actually happened, and what did you do about it?`, - () => `Something goes wrong mid-service and your lead is unreachable. Talk me through your next three moves.`, - () => `Tell me about feedback that changed how you work. What did you do differently afterward?`, - () => `Last one — what does your schedule realistically look like, and what do you want to be doing a year from now?`, - ], - es: [ - (role) => `Gracias por acompañarme. Para empezar — ¿qué te atrajo al trabajo de ${role}, y cuánto tiempo llevas haciéndolo?`, - () => `Cuéntame de tu turno más ocupado. ¿Qué pasó realmente, y qué hiciste al respecto?`, - () => `Algo sale mal a mitad del servicio y no puedes contactar a tu supervisor. Explícame tus siguientes tres pasos.`, - () => `Háblame de algún comentario que cambió tu manera de trabajar. ¿Qué hiciste diferente después?`, - () => `La última — ¿cómo es tu disponibilidad real, y qué te gustaría estar haciendo en un año?`, - ], -}; - -function interviewQuestion(prompt) { - const lang = isSpanish(prompt) ? 'es' : 'en'; - const numberMatch = prompt.match(/Ask question (\d+) of 5/); - const index = Math.min(4, Math.max(0, (numberMatch ? Number(numberMatch[1]) : 1) - 1)); - const roleMatch = prompt.match(/AI interviewer for a (.+?) position/); - const role = roleMatch ? roleMatch[1] : 'this'; - return INTERVIEW_QUESTIONS[lang][index](role); -} - -const OWLIVER_QUESTIONS = { - en: [ - (name) => `Hey ${name} — good to meet you. Tell me about yourself, in your own words.`, - () => `I like that. Tell me about something you got through that you're genuinely proud of.`, - () => `Picture this: you're the only one on shift, 300 guests arrive early, and the kitchen is behind. What do you do?`, - () => `What's a piece of feedback that stuck with you — and what changed after it?`, - () => `What do you want to be known for in five years?`, - () => `That's everything I needed. Anything you want to add before I put this together?`, - ], - es: [ - (name) => `Hola ${name} — un gusto conocerte. Cuéntame de ti, con tus propias palabras.`, - () => `Me gusta eso. Cuéntame de algo que superaste y de lo que estés realmente orgulloso.`, - () => `Imagina: estás solo en el turno, llegan 300 invitados antes de tiempo y la cocina va atrasada. ¿Qué haces?`, - () => `¿Qué comentario se te quedó grabado — y qué cambió después?`, - () => `¿Por qué te gustaría ser reconocido en cinco años?`, - () => `Eso es todo lo que necesitaba. ¿Algo que quieras agregar antes de que lo prepare?`, - ], -}; - -function owliverQuestion(prompt) { - const lang = isSpanish(prompt) ? 'es' : 'en'; - const numberMatch = prompt.match(/you are on question (\d+)/); - const index = Math.min(5, Math.max(0, (numberMatch ? Number(numberMatch[1]) : 1) - 1)); - const nameMatch = prompt.match(/voice chat with (.+?)\./); - const name = nameMatch ? nameMatch[1].split(' ')[0] : 'there'; - return OWLIVER_QUESTIONS[lang][index](name); -} - -/* ── 5. Interview evaluation ───────────────────────────────────────────── */ - -/** Scores a transcript on how substantive the candidate's own answers are. */ -function readTranscript(prompt) { - const transcript = prompt.split('TRANSCRIPT:')[1] || ''; - const answers = transcript - .split('\n') - .filter((line) => /^Candidate:|^Worker:/.test(line.trim())) - .map((line) => line.replace(/^\w+:\s*/, '').trim()) - .filter(Boolean); - const words = answers.reduce((sum, a) => sum + a.split(/\s+/).length, 0); - const avgWords = answers.length ? words / answers.length : 0; - // Concrete answers name specifics; vague ones stay abstract. - const specifics = (transcript.match(/\b\d+\b/g) || []).length; - return { answers, words, avgWords, specifics }; -} - -function evaluateInterview(prompt) { - const { answers, avgWords, specifics } = readTranscript(prompt); - const fastMatch = prompt.match(/(\d+) candidate response\(s\) were suspiciously fast/); - const fast = fastMatch ? Number(fastMatch[1]) : 0; - const spanish = isSpanish(prompt); - const role = field(prompt, 'Job Title') || 'the role'; - const candidate = field(prompt, 'Candidate') || 'The candidate'; - - // Depth of answers is the dominant signal; specifics lift it, silence sinks it. - const depth = clamp(avgWords * 2.1); - const engagement = clamp(answers.length * 16); - const concreteness = clamp(46 + specifics * 9); - const base = clamp(depth * 0.45 + engagement * 0.25 + concreteness * 0.3); - - const integrity = clamp(100 - fast * 12); - const flags = []; - if (fast >= 2) flags.push(`${fast} responses returned in under 8 seconds`); - if (avgWords > 90) flags.push('Answer length and register suggest possible assistance'); - if (answers.length < 3) flags.push('Interview ended before enough signal was gathered'); - - const overall = clamp(base - (fast >= 3 ? 8 : 0)); - const verdict = overall >= 78 ? 'hire' : overall >= 55 ? 'maybe' : 'no'; - - const strengths = spanish - ? ['Respuestas claras y concretas', 'Se mantuvo tranquilo bajo presión'] - : ['Clear, concrete answers', 'Stayed composed describing pressure']; - const concerns = spanish - ? ['Profundidad limitada en algunas respuestas'] - : ['Limited depth on some answers']; - if (fast >= 2) concerns.push(spanish ? 'Tiempos de respuesta inusualmente rápidos' : 'Unusually fast response times'); - - return { - overall_interview_score: overall, - verdict, - hire_recommendation: spanish - ? verdict === 'hire' - ? `Recomiendo avanzar con ${candidate} para ${role}.` - : verdict === 'maybe' - ? `Vale un turno de prueba antes de comprometerse.` - : `No recomiendo avanzar para este puesto.` - : verdict === 'hire' - ? `Move forward with ${candidate} for ${role}.` - : verdict === 'maybe' - ? `Worth a trial shift before committing to the season.` - : `Not a fit for this posting as interviewed.`, - integrity_score: integrity, - ai_flags: flags, - category_scores: { - communication: clamp(depth * 0.6 + concreteness * 0.4), - confidence: clamp(base * 0.95), - experience_relevance: clamp(concreteness * 0.8 + depth * 0.2), - culture_fit: clamp(base * 0.92 + 4), - problem_solving: clamp(concreteness * 0.7 + depth * 0.3), - personality: clamp(base * 0.98), - communication_style: clamp(depth * 0.55 + 30), - attendance_expectations: clamp(base * 0.9 + 8), - reliability: clamp(base * 0.93 + 6), - physical_requirements: clamp(72 + specifics * 2), - leadership_expectations: clamp(base * 0.78), - scenario_judgment: clamp(concreteness * 0.75 + depth * 0.25), - job_related_answers: clamp(base * 0.96), - verified_skills: clamp(base * 0.88), - }, - strengths, - concerns, - best_fit_roles: [role], - summary: spanish - ? `${candidate} dio ${answers.length} respuestas con detalle concreto. El criterio es sólido; la profundidad técnica aún está en desarrollo.` - : `${candidate} gave ${answers.length} answers grounded in specifics rather than generalities. Judgment reads sound; technical depth is still developing.`, - reasoning: spanish - ? `Puntuación basada en la profundidad de las respuestas (promedio ${Math.round(avgWords)} palabras) y ${specifics} detalles concretos. Integridad ${integrity} con ${fast} respuesta(s) rápida(s).` - : `Scored on answer depth (${Math.round(avgWords)} words average) and ${specifics} concrete details cited. Integrity ${integrity} with ${fast} fast response(s).`, - }; -} - -/* ── 6. Talent matching ────────────────────────────────────────────────── */ - -function matchTalent(prompt) { - const block = prompt.match(/AVAILABLE TALENT \(JSON\):\s*(\[[\s\S]*?\])\s*\n\nYou are NEVER/); - let pool = []; - try { - pool = block ? JSON.parse(block[1]) : []; - } catch { - pool = []; - } - - const title = (field(prompt, 'Title') || '').toLowerCase(); - const category = (field(prompt, 'Category') || '').toLowerCase(); - const minYears = numField(prompt, 'Min Experience'); - const requiredCerts = listField(prompt, 'Required Certifications').map((c) => c.toLowerCase()); - - const scored = pool.map((p) => { - const wants = `${p.desired_position || ''} ${p.current_position || ''}`.toLowerCase(); - const roleFit = (title && wants.includes(title)) || (category && wants.includes(category)) ? 30 - : (title && title.split(' ').some((w) => w.length > 3 && wants.includes(w))) ? 18 - : 0; - - const yearsFit = minYears > 0 - ? Math.min(20, ((p.experience_years || 0) / minYears) * 20) - : Math.min(20, (p.experience_years || 0) * 3); - - const certs = (p.certifications || []).map((c) => c.toLowerCase()); - const certFit = requiredCerts.length - ? (requiredCerts.filter((r) => certs.some((c) => c.includes(r) || r.includes(c))).length / requiredCerts.length) * 15 - : (certs.length ? 12 : 5); - - const scoreFit = ((p.krow_score || 0) / 100) * 20; - const verifiedFit = Math.min(10, (p.capabilities || []).length * 5); - const reliabilityFit = ((p.attendance_score || 0) / 100) * 5; - - const match = clamp(roleFit + yearsFit + certFit + scoreFit + verifiedFit + reliabilityFit); - - const reasons = []; - if (roleFit >= 18) reasons.push(`Targeting ${p.desired_position || p.current_position} — aligned with this posting`); - if ((p.experience_years || 0) >= minYears && minYears > 0) reasons.push(`${p.experience_years} years experience meets the ${minYears}-year minimum`); - if ((p.capabilities || []).length) reasons.push(`Verified: ${p.capabilities.join(', ')}`); - if ((p.krow_score || 0) >= 75) reasons.push(`Career score ${p.krow_score} with ${p.shifts_completed || 0} shifts completed`); - if (!reasons.length) reasons.push(`New to KROW — ${p.experience_years || 0} years experience, no verified evidence yet`); - - return { - profile_id: p.id, - match_score: match, - match_label: match >= 80 ? 'Strong Match' : match >= 65 ? 'Good Match' : match >= 50 ? 'Possible Fit' : 'Weak Fit', - reasons: reasons.slice(0, 3), - recommendation: match >= 80 ? 'Hire' : match >= 65 ? 'Interview' : match >= 50 ? 'Maybe' : 'Pass', - }; - }).sort((a, b) => b.match_score - a.match_score); - - const qualified = scored.filter((m) => m.match_score >= 50); - return { matches: qualified.length ? qualified : scored.slice(0, 5) }; -} - -/* ── 7. Owliver profile builder ────────────────────────────────────────── */ - -/** Job titles a worker might name for themselves, most specific first. */ -const ROLE_TITLES = [ - [/banquet captain|captain/, 'Banquet Captain'], - [/executive chef/, 'Executive Chef'], - [/sous chef/, 'Sous Chef'], - [/line cook/, 'Line Cook'], - [/\bchef\b/, 'Chef'], - [/bartender|bartend/, 'Bartender'], - [/barback/, 'Barback'], - [/event server|banquet server|\bserver\b/, 'Event Server'], - [/security officer|guard/, 'Security Officer'], - [/\bhost(ess)?\b/, 'Host'], - [/picker|warehouse/, 'Picker'], - [/supervisor/, 'Supervisor'], - [/manager/, 'Manager'], -]; - -const AVAILABILITY_TOKENS = [ - [/weekend|saturday|sunday|sábado|domingo/i, 'Weekends'], - [/weekday|monday|tuesday|wednesday|thursday|friday|semana/i, 'Weekdays'], - [/evening|night|noche|tarde/i, 'Evenings'], - [/morning|mañana|early/i, 'Mornings'], - [/overnight|graveyard/i, 'Overnight'], - [/on.?call|flexible/i, 'On-Call'], -]; - -function buildCareerDna(prompt) { - const transcript = quotedBlock(prompt); - const lower = transcript.toLowerCase(); - const { answers, avgWords, specifics } = (() => { - const lines = transcript.split('\n').filter((l) => /^Worker:/.test(l.trim())); - const texts = lines.map((l) => l.replace(/^Worker:\s*/, '').trim()).filter(Boolean); - const words = texts.reduce((s, t) => s + t.split(/\s+/).length, 0); - return { - answers: texts, - avgWords: texts.length ? words / texts.length : 0, - specifics: (transcript.match(/\b\d+\b/g) || []).length, - }; - })(); - - const nameMatch = prompt.match(/worker named (.+?),/) || prompt.match(/worker named (.+?)\b/); - const workerName = nameMatch ? nameMatch[1].trim() : 'This worker'; - const spanish = isSpanish(prompt); - - // People mention several spans ("4 years as captain, 7 years total") — the - // largest is the career total the profile wants. - const years = (lower.match(/(\d+)\s*(?:\+)?\s*(?:years?|yrs?|años)/g) || []) - .map((m) => Number(m.match(/\d+/)[0])) - .reduce((max, n) => Math.max(max, n), 0); - - // A stated job title, not a skill label — an empty string is more honest than - // labelling someone "Table service". - const role = ROLE_TITLES.find(([re]) => re.test(lower)); - const roleTitle = role ? role[1] : ''; - - const skills = [...new Set( - SKILL_KEYWORDS.filter(([key]) => lower.includes(key)).map(([, label]) => label) - )].slice(0, 8); - const certifications = [...new Set( - CERT_KEYWORDS.filter(([key]) => lower.includes(key)).map(([, label]) => label) - )]; - const availability = [...new Set( - AVAILABILITY_TOKENS.filter(([re]) => re.test(transcript)).map(([, token]) => token) - )]; - - const languages = ['English']; - if (/spanish|español/i.test(transcript) || spanish) languages.push('Spanish'); - - // Depth of the conversation is the only honest basis for these scores, so - // they stay conservative — the reference prompt is explicit about that. - const depth = clamp(avgWords * 1.9); - const engagement = clamp(answers.length * 15); - const base = clamp(depth * 0.5 + engagement * 0.25 + clamp(40 + specifics * 8) * 0.25); - const leadership = clamp(base * 0.7 + (/lead|supervis|manage|train|captain|chef/i.test(lower) ? 20 : 0)); - - const transportation = /\b(car|drive|vehicle|truck|coche|carro)\b/i.test(transcript) - ? 'Own vehicle' - : /\b(bus|transit|train|bart|metro)\b/i.test(transcript) - ? 'Public transit' - : ''; - - const primary = skills[0] || (spanish ? 'hospitalidad' : 'hospitality'); - - return { - career_goals: answers.length - ? spanish - ? `Crecer en ${primary.toLowerCase()} y asumir más responsabilidad en el equipo.` - : `Grow within ${primary.toLowerCase()} and take on more responsibility on the floor.` - : '', - desired_position: roleTitle, - current_position: years > 0 ? roleTitle : '', - experience_years: years, - experience: years > 0 && roleTitle - ? [{ company: spanish ? 'Empleador anterior' : 'Previous employer', role: roleTitle, years }] - : [], - skills, - languages, - availability, - transportation, - certifications, - personality: answers.length - ? spanish - ? 'Directo y tranquilo. Explica su razonamiento sin adornos y asume lo que le corresponde.' - : 'Direct and level. Explains their reasoning without embellishment and owns their part.' - : '', - strengths: answers.length - ? spanish - ? ['Mantiene la calma bajo presión', 'Comunica con claridad', 'Toma iniciativa'] - : ['Keeps composure under pressure', 'Communicates clearly', 'Takes initiative'] - : [], - weaknesses: answers.length - ? spanish ? ['Aún desarrollando experiencia de liderazgo'] : ['Still building formal leadership experience'] - : [], - industries: skills.length ? ['Hospitality', 'Events'] : [], - salary_expectations: '', - leadership_potential: leadership, - communication_style: answers.length - ? spanish - ? 'Conciso y concreto; da ejemplos en lugar de generalidades.' - : 'Concise and concrete; reaches for examples rather than generalities.' - : '', - summary: answers.length - ? spanish - ? `${workerName} habló con detalle sobre su trabajo real. Muestra criterio práctico y ganas de crecer.` - : `${workerName} talked about real work in real detail. Practical judgment, and clearly interested in getting better at it.` - : '', - career_dna: { - identity: answers.length - ? spanish - ? `Alguien que resuelve en el momento y aprende sobre la marcha.` - : `Someone who solves problems in the moment and learns on the way through.` - : '', - learning_speed: clamp(base * 0.9), - // Reliability is unverified until real shifts land, so it is held down. - reliability: clamp(base * 0.6 + 10), - communication_score: clamp(depth * 0.6 + 28), - leadership_index: leadership, - adaptability_score: clamp(base * 0.95), - work_style: /\bteam|equipo\b/i.test(lower) ? 'Team player' : /\balone|independ\b/i.test(lower) ? 'Independent' : 'Operator', - culture_match: spanish - ? ['Equipos con expectativas claras', 'Operaciones de ritmo rápido'] - : ['Teams with clear expectations', 'Fast-paced service operations'], - // Baseline trust from one conversation — deliberately modest. - reputation_score: clamp(30 + base * 0.35), - }, - }; -} - -/* ── 8. Proving Ground evaluator ───────────────────────────────────────── */ - -function evaluateChallenge(prompt) { - const criteriaMatch = prompt.match(/rubric" object: (.+?)\.\n/); - const criteria = criteriaMatch - ? criteriaMatch[1].split(',').map((c) => c.trim()).filter(Boolean) - : ['overall_performance']; - - const isPhotoIdentify = /marked these hazards/.test(prompt); - const isMedia = /uploaded a (photo|video)/.test(prompt); - - let base; - let strengths; - let concerns; - - if (isPhotoIdentify) { - const marks = (prompt.match(/^- "/gm) || []).length; - // Real hazard scenes have a handful of genuine risks; marking everything is - // as wrong as marking nothing. - base = clamp(marks === 0 ? 18 : 46 + Math.min(38, marks * 13) - Math.max(0, marks - 4) * 9); - strengths = marks ? [`Identified ${marks} hazard${marks === 1 ? '' : 's'}`] : []; - concerns = marks === 0 - ? ['No hazards marked'] - : marks > 4 - ? ['Several marks are likely false positives'] - : ['Some lower-visibility risks were missed']; - } else if (isMedia) { - base = 74; - strengths = ['Submitted a clear demonstration', 'Followed the challenge brief']; - concerns = ['Some steps happen off-camera and cannot be verified']; - } else { - const { answers, avgWords, specifics } = readTranscript( - `TRANSCRIPT:\n${quotedBlock(prompt).replace(/^Worker:/gm, 'Candidate:')}` - ); - const spoken = answers.length ? answers : quotedBlock(prompt) ? ['x'] : []; - base = spoken.length - ? clamp(avgWords * 2.4 + specifics * 6 + 20) - : 0; - strengths = base >= 70 - ? ['Went straight to a concrete remedy', 'Kept the guest experience intact'] - : base > 0 ? ['Engaged with the scenario'] : []; - concerns = base >= 70 - ? ['Could state the escalation path explicitly'] - : ['Response lacks the specifics an employer would need to trust it']; - } - - const verdict = base >= 70 ? 'verified' : base >= 50 ? 'needs_work' : 'failed'; - - // Spread the overall score across the rubric so criteria are not identical. - const rubric = {}; - criteria.forEach((criterion, i) => { - rubric[criterion] = clamp(base + (i % 3 === 0 ? 4 : i % 3 === 1 ? -3 : 1)); - }); - - return { - verdict, - score: base, - rubric, - feedback: verdict === 'verified' - ? `This holds up. You handled the core of the scenario the way the job actually needs it handled. Tighten the closing steps and this becomes strong evidence.` - : verdict === 'needs_work' - ? `You are close. The instinct is right, but an employer reading this could not yet tell you would do it consistently. Be more specific about what you do, in what order.` - : `This does not yet demonstrate the skill. Work through the scenario again and walk through your actual steps out loud, in order, with specifics.`, - strengths, - concerns, - }; -} - -/* ── Router ────────────────────────────────────────────────────────────── */ - -/** - * Recognizes each workflow by the phrase its prompt opens with. Order matters - * only in that every branch is mutually exclusive by design. - */ -const ROUTES = [ - [/^You are KROW's AI screening engine/m, screenCandidate, 1100], - [/^You are an expert hiring copywriter/m, generateJobDescription, 1400], - [/Extract and infer a structured resume/m, buildResume, 1200], - [/^You are "KROW", a friendly but sharp AI interviewer/m, interviewQuestion, 700], - [/^You are KROW's proactive talent-matching engine/m, matchTalent, 1300], - [/^You are KROW's AI interview evaluator/m, evaluateInterview, 1500], - [/^You are "Owliver"/m, owliverQuestion, 800], - [/^You are Owliver's Career DNA engine/m, buildCareerDna, 1600], - [/^You are KROW's Proving Ground evaluator/m, evaluateChallenge, 1400], - [/^You are "KROW", running a short proving-ground challenge/m, - () => 'Walk me through the exact order you would do that in — what happens first, and who do you tell?', 700], -]; - -/** Drop-in replacement for `integrations.Core.InvokeLLM`. */ -export async function invokeLLM({ prompt = '', response_json_schema: schema } = {}) { - const route = ROUTES.find(([pattern]) => pattern.test(prompt)); - - if (!route) { - // An unrecognized prompt should surface loudly in the demo rather than - // silently returning a shape the caller cannot use. - console.warn('[krow-demo] Unrecognized AI prompt — returning an empty result.', prompt.slice(0, 120)); - await think(400); - return schema ? {} : ''; - } - - const [, handler, latency] = route; - await think(latency); - return handler(prompt); -} - -/** Drop-in replacement for `integrations.Core.UploadFile`. */ -export async function uploadFile({ file } = {}) { - await think(500); - if (!file) return { file_url: '' }; - // A blob URL keeps uploaded media viewable for the rest of the session - // without any storage backend. - return { file_url: URL.createObjectURL(file), file_name: file.name, file_size: file.size }; -} diff --git a/src/api/attendanceSeed.js b/src/api/attendanceSeed.js deleted file mode 100644 index 9b9abda..0000000 --- a/src/api/attendanceSeed.js +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Shift records — the workforce actually turning up, or not. - * - * This is the one collection in the demo whose dates are **anchored to now** - * rather than written as calendar dates. Everything else in `seed.js` is a - * fixed narrative: 22 people applied on particular days and three were hired, - * and those dates are the story. Attendance is not a story, it is a rolling - * operational record — "how was attendance last week" has to mean *last week*, - * every week, or the feature reads as permanently empty and looks broken. - * - * The snapshot persists on first load (see `store.js`), so the figures are - * stable for a browser once written; only a fresh workspace re-anchors them. - * Every foreign key points at a fixed seeded record, so correlation with - * positions and hires stays exact however the dates land. - * - * Nothing here is random. The distribution is written out below and generated - * deterministically, so the same workspace always produces the same figures and - * a test can assert against them. What it is *shaped* to contain: - * - * - **Marco** — the control. Reliable, occasional event-night overtime. - * - **Marcus** — attendance degrading in the last fortnight: two absences, a - * no-show and repeated lateness, against a clean record before that. This - * is the attendance anomaly, and it is recent enough to be actionable. - * - **Antoine** — present throughout, but overtime climbing steadily week on - * week. This is the overtime anomaly, and it is a trend rather than a - * spike, which is the kind a person reading a table would miss. - * - * The roster is three people because three people have been hired — `Staff` is - * the workforce, and inventing a fourth to make the charts look busier would be - * inventing an employee. - */ - -/** Eight weeks: long enough for a week-on-week trend and a month comparison. */ -const WINDOW_DAYS = 56; - -/** - * A local instant `n` days back, at a given hour. - * - * Local rather than UTC because a shift belongs to the day it was worked in the - * place it was worked, and `periodRange` windows on local day boundaries too. - */ -function daysAgo(n, hour = 9, minute = 0) { - const d = new Date(); - d.setDate(d.getDate() - n); - d.setHours(hour, minute, 0, 0); - return d; -} - -const round1 = (n) => Math.round(n * 10) / 10; -const round2 = (n) => Math.round(n * 100) / 100; -const HOUR = 60 * 60 * 1000; - -/** - * The roster, joined to the records they were hired against. - * - * `job_posting_id` and `role_category` are the posting's own, so "department" - * means here exactly what it means on Hired History and Analytics — see the - * join in `lib/hiringRecords.js`. - */ -const ROSTER = [ - { - staff_id: 'staff_marco', - worker_name: 'Marco Rivera', - worker_email: 'marco.rivera@email.com', - job_posting_id: 'job_bartender_corp', - role: 'Experienced Bartender – Corporate Events', - role_category: 'Bartender', - /* Wed–Sat: corporate events run late in the week. */ - weekdays: [3, 4, 5, 6], - startHour: 16, - scheduledHours: 8, - }, - { - staff_id: 'staff_marcus', - worker_name: 'Marcus Williams', - worker_email: 'marcus.w@email.com', - job_posting_id: 'job_security', - role: 'Event Security Officer', - role_category: 'Security', - /* Mon–Fri: a fixed security rota, which is what makes the recent - absences stand out rather than read as an irregular schedule. */ - weekdays: [1, 2, 3, 4, 5], - startHour: 14, - scheduledHours: 8, - }, - { - staff_id: 'staff_antoine', - worker_name: 'Chef Antoine Dubois', - worker_email: 'antoine.dubois@email.com', - job_posting_id: 'job_chef', - role: 'Executive Chef – Catering', - role_category: 'Chef', - /* Tue–Sat: kitchen service. */ - weekdays: [2, 3, 4, 5, 6], - startHour: 12, - scheduledHours: 9, - }, -]; - -/** - * How each worker's series behaves, by position in it. - * - * `i` counts back from the most recent shift, so "the last fortnight" is a - * range of small indices and stays that way as the window rolls forward. - * Returning a plain record keeps every rule visible in one place instead of - * spread across the generator. - */ -const BEHAVIOUR = { - /* Reliable. One late arrival every couple of months, and overtime only on - the nights events actually overrun. */ - staff_marco: (i, weekday) => ({ - status: i === 14 ? 'late' : 'present', - minutesLate: i === 14 ? 9 : 0, - /* Friday and Saturday events overrun; midweek ones do not. */ - overtime: weekday === 5 || weekday === 6 ? 1 : 0, - notes: '', - }), - - /** - * Clean for six weeks, then coming apart. - * - * Indices 0–9 are roughly the last fortnight. Two absences, one no-show and - * three late arrivals inside that window, against a single late arrival in - * the six weeks before it — a change big enough to be worth surfacing and - * specific enough to act on. - */ - staff_marcus: (i) => { - if (i === 2 || i === 7) { - return { status: 'absent', minutesLate: 0, overtime: 0, notes: 'Called in sick' }; - } - if (i === 4) { - return { status: 'no_show', minutesLate: 0, overtime: 0, notes: 'No contact' }; - } - if (i === 1) return { status: 'late', minutesLate: 24, overtime: 0, notes: '' }; - if (i === 5) return { status: 'late', minutesLate: 16, overtime: 0, notes: '' }; - if (i === 9) return { status: 'late', minutesLate: 12, overtime: 0, notes: '' }; - if (i === 26) return { status: 'late', minutesLate: 7, overtime: 0, notes: '' }; - return { status: 'present', minutesLate: 0, overtime: 0, notes: '' }; - }, - - /** - * Always there, increasingly late leaving. - * - * Overtime rises about half an hour a week as the kitchen carries more - * covers, on the three busiest shifts of each week. A steady climb rather - * than a spike, which is exactly the shape that hides in a table of totals. - */ - staff_antoine: (i, weekday) => { - const weekIndex = Math.floor(i / 5); - const busy = weekday === 4 || weekday === 5 || weekday === 6; - const overtime = busy ? Math.max(0.5, round1(3.5 - weekIndex * 0.45)) : 0; - return { status: 'present', minutesLate: 0, overtime, notes: '' }; - }, -}; - -/** Every shift date for one worker, most recent first. */ -function shiftOffsets(weekdays) { - const offsets = []; - for (let offset = 0; offset <= WINDOW_DAYS; offset += 1) { - const day = daysAgo(offset).getDay(); - if (weekdays.includes(day)) offsets.push(offset); - } - return offsets; -} - -const pad = (n) => String(n).padStart(2, '0'); -const localDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; - -function buildShifts() { - const records = []; - - for (const worker of ROSTER) { - const offsets = shiftOffsets(worker.weekdays); - - offsets.forEach((offset, i) => { - const scheduledStart = daysAgo(offset, worker.startHour); - const weekday = scheduledStart.getDay(); - const scheduledEnd = new Date(scheduledStart.getTime() + worker.scheduledHours * HOUR); - - const { status, minutesLate, overtime, notes } = BEHAVIOUR[worker.staff_id](i, weekday); - const worked = status !== 'absent' && status !== 'no_show'; - - const actualStart = worked - ? new Date(scheduledStart.getTime() + minutesLate * 60 * 1000) - : null; - const actualEnd = worked - ? new Date(scheduledEnd.getTime() + overtime * HOUR) - : null; - - records.push({ - id: `shift_${worker.staff_id.replace('staff_', '')}_${pad(offsets.length - i)}`, - staff_id: worker.staff_id, - worker_name: worker.worker_name, - worker_email: worker.worker_email, - job_posting_id: worker.job_posting_id, - role: worker.role, - role_category: worker.role_category, - - shift_date: localDate(scheduledStart), - scheduled_start: scheduledStart.toISOString(), - scheduled_end: scheduledEnd.toISOString(), - scheduled_hours: worker.scheduledHours, - - actual_start: actualStart ? actualStart.toISOString() : null, - actual_end: actualEnd ? actualEnd.toISOString() : null, - /* Arriving late shortens the shift; staying on lengthens it. A missed - shift is zero hours worked, not a short one. */ - actual_hours: worked - ? round2(worker.scheduledHours - minutesLate / 60 + overtime) - : 0, - overtime_hours: worked ? round1(overtime) : 0, - minutes_late: worked ? minutesLate : 0, - status, - notes, - - /* Load-bearing: `inPeriod` in `lib/skills/dataResolver.js` windows every - collection on `created_date`, so a shift's created date *is* the - instant it was worked. Without that, every period reading of this - collection would be empty and nothing would say why. */ - created_date: scheduledStart.toISOString(), - updated_date: (actualEnd || scheduledEnd).toISOString(), - }); - }); - } - - /* Most recent first, matching the `-created_date` order every other - collection is listed in. */ - return records.sort( - (a, b) => new Date(b.created_date).getTime() - new Date(a.created_date).getTime() - ); -} - -export const SHIFT_RECORDS = buildShifts(); diff --git a/src/api/base44Client.js b/src/api/base44Client.js deleted file mode 100644 index 89b3e05..0000000 --- a/src/api/base44Client.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Application data client. - * - * The reference app talks to a Base44 backend through this module. The demo - * keeps the module path, the export name, and the full method contract, and - * swaps the transport for the local store in `store.js` and the local AI engine - * in `aiEngine.js`. Nothing downstream — hooks, pages, components — knows or - * cares, which is exactly the point: the seam stays where it was. - */ - -import { createEntity, initStore, resetStore } from './store'; -import { invokeLLM, uploadFile } from './aiEngine'; -import { DEMO_USER, seedData } from './seed'; - -initStore(seedData); - -const ENTITY_NAMES = [ - 'JobPosting', 'JobApplication', 'AIInterview', 'Staff', 'WorkerProfile', - 'Course', 'Badge', 'LearningPath', 'Certification', 'RoleCategory', - 'UserActivity', 'Evidence', 'User', - /* Who is on which position, and for how long. The record that turns "hired" - into workforce allocation: without it a position knows its demand and its - applicants but not who is actually covering it. */ - 'Assignment', - /* Shifts worked, missed and overrun. The operational record behind - attendance and overtime analysis — see `api/attendanceSeed.js`. */ - 'ShiftRecord', -]; - -const entities = Object.fromEntries( - ENTITY_NAMES.map((name) => [name, createEntity(name)]) -); - -/* ── Auth ──────────────────────────────────────────────────────────────── */ - -const SESSION_KEY = 'krow_demo_user'; - -function loadUser() { - try { - const raw = localStorage.getItem(SESSION_KEY); - return raw ? { ...DEMO_USER, ...JSON.parse(raw) } : { ...DEMO_USER }; - } catch { - return { ...DEMO_USER }; - } -} - -let currentUser = loadUser(); - -/** - * Writes the session user, and says whether it actually landed. - * - * The old version was `try { setItem } catch {}` — a swallowed - * `QuotaExceededError` or a private-browsing refusal, and the caller was handed - * a user object indistinguishable from a successful write. For preferences that - * is invisible; for `customSkills`, which is where every account-authored skill - * definition lives, it is the whole "I saved it and it was gone" report: the - * toast said added, the list showed it, the reload did not. - * - * The read-back matters as much as the catch. A write can be accepted and then - * evicted, and a serialisation can land truncated; comparing what came back - * with what went in is the only way to know the record is really there. - */ -function persistUser() { - const payload = JSON.stringify(currentUser); - try { - localStorage.setItem(SESSION_KEY, payload); - } catch (error) { - return { persisted: false, error }; - } - try { - if (localStorage.getItem(SESSION_KEY) !== payload) { - return { persisted: false, error: new Error('The session record did not survive the write.') }; - } - } catch (error) { - return { persisted: false, error }; - } - return { persisted: true, error: null }; -} - -const auth = { - /** The demo is always signed in as the seeded employer/admin user. */ - async me() { - return { ...currentUser }; - }, - - async updateMe(patch) { - currentUser = { ...currentUser, ...patch }; - persistUser(); - return { ...currentUser }; - }, - - /** - * Preferences, read synchronously. - * - * `me()` is async because the real client fetches, but the panel provider has - * to decide whether Owliver starts open during its first render — one tick - * later is a visible flash of the wrong workspace. The session user is already - * hydrated from localStorage at module load, so this is a plain read of the - * same record `me()` returns, not a second copy of the state. - */ - preferences() { - return { ...DEMO_USER.preferences, ...(currentUser.preferences || {}) }; - }, - - /** - * Merges into the stored preferences and persists with the rest of the user. - * - * Returns the write's outcome alongside the record, rather than the record - * alone. Preferences are where account-authored skills live, so "did this - * survive the reload" is a question the caller has to be able to answer — - * see `persistUser`. - */ - async updatePreferences(patch) { - currentUser = { ...currentUser, preferences: { ...auth.preferences(), ...patch } }; - const write = persistUser(); - return { user: { ...currentUser }, ...write }; - }, - - isAuthenticated() { - return true; - }, - - /** - * There is no identity provider to sign out of, so this clears the local - * session and returns to the requested page. - */ - logout(redirectTo = '/') { - try { - localStorage.removeItem(SESSION_KEY); - } catch { - // Ignore. - } - currentUser = { ...DEMO_USER }; - window.location.href = typeof redirectTo === 'string' ? redirectTo : '/'; - }, - - redirectToLogin() { - window.location.href = '/'; - }, -}; - -/* ── Integrations & analytics ───────────────────────────────────────────── */ - -const integrations = { - Core: { - InvokeLLM: invokeLLM, - UploadFile: uploadFile, - }, -}; - -const analytics = { - track({ eventName, properties } = {}) { - if (import.meta.env.DEV) { - console.debug('[krow-demo] analytics', eventName, properties || {}); - } - }, -}; - -export const base44 = { entities, auth, integrations, analytics }; - -/** Restores the shipped demo data, discarding local edits. */ -export function resetDemoData() { - resetStore(seedData); - window.location.reload(); -} diff --git a/src/api/doormile/client.js b/src/api/doormile/client.js new file mode 100644 index 0000000..1e1fbdf --- /dev/null +++ b/src/api/doormile/client.js @@ -0,0 +1,93 @@ +import axios from 'axios'; + +/** + * The Doormile Express admin API client. + * + * One axios instance for `api.doormile.com/api/v1/admin/*`. Every request needs + * a JWT Bearer token, so the token is attached here rather than at each call + * site, and an expired one logs the console out the same way a manual sign-out + * does — a half-authenticated shell that renders but 401s on every fetch is + * worse than being sent back to the login page. + * + * Ported from the Express Console's `utils/doormileAxios.js`. The only change + * is the environment seam: Vite exposes `import.meta.env`, not `process.env`. + */ + +export const DOORMILE_TOKEN_KEY = 'doormileToken'; +export const DOORMILE_USER_KEY = 'doormileUser'; + +/** The backend, for every `/admin/*` call the console makes. */ +export const DOORMILE_API_URL = 'https://api.doormile.com/api/v1'; + +/** + * `VITE_DOORMILE_URL` exists to point a local build at a staging or mock + * backend. It is never set in production: `.dockerignore` keeps every `.env` + * file out of the image, so a container build always resolves to the real API. + * + * An override is announced loudly in development, because a console silently + * talking to the wrong backend looks exactly like a console talking to the + * right one with no data in it. + */ +export const DOORMILE_BASE_URL = import.meta.env.VITE_DOORMILE_URL || DOORMILE_API_URL; + +if (import.meta.env.DEV && DOORMILE_BASE_URL !== DOORMILE_API_URL) { + console.warn( + `[Doormile] API overridden to ${DOORMILE_BASE_URL} — the real backend is ${DOORMILE_API_URL}. ` + + 'Unset VITE_DOORMILE_URL to use it.' + ); +} + +const doormileAxios = axios.create({ baseURL: DOORMILE_BASE_URL }); + +doormileAxios.interceptors.request.use((config) => { + const token = localStorage.getItem(DOORMILE_TOKEN_KEY); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +doormileAxios.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401 && !window.location.href.includes('/login')) { + localStorage.removeItem(DOORMILE_TOKEN_KEY); + localStorage.removeItem(DOORMILE_USER_KEY); + window.location.replace('/login'); + } + + /* Reject with the server's own body — callers read `err.message` — but keep + the HTTP status reachable as `err.httpStatus`. Without this the status is + lost entirely and a 404, a 500 and a validation failure all collapse into + the same generic failure, which is exactly the distinction the write + paths need. Non-enumerable so nothing that spreads or stringifies the + body starts carrying it around. */ + const body = error.response?.data; + if (body === undefined || body === null) return Promise.reject(error); + const payload = typeof body === 'object' ? body : { message: String(body) }; + return Promise.reject( + Object.defineProperty(payload, 'httpStatus', { + value: error.response?.status, + enumerable: false, + }) + ); + } +); + +/** Reads the signed-in admin back out of storage, or null when signed out. */ +export const readStoredUser = () => { + try { + const raw = localStorage.getItem(DOORMILE_USER_KEY); + return raw ? JSON.parse(raw) : null; + } catch { + return null; + } +}; + +export const readStoredToken = () => localStorage.getItem(DOORMILE_TOKEN_KEY); + +/** The message a failed call should show — server text first, then the throw. */ +export const errorMessage = (err, fallback = 'Something went wrong') => + err?.message || err?.response?.data?.message || fallback; + +export default doormileAxios; diff --git a/src/api/doormile/endpoints.js b/src/api/doormile/endpoints.js new file mode 100644 index 0000000..f949288 --- /dev/null +++ b/src/api/doormile/endpoints.js @@ -0,0 +1,655 @@ +import doormileAxios, { DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY } from './client'; + +// The Doormile Express admin API — one named async export per +// `api.doormile.com/api/v1/admin/*` endpoint. +// +// Ported verbatim from the Express Console's `pages/api/doormileApi.js`, since +// this is the contract the backend actually speaks; only the client import +// changed. Every function unwraps the envelope for its caller: list/GET +// functions return `response.data.data` directly, mutations return the full +// `response.data` (`{ success, data | message }`) so callers can check +// `res.success`. +// +// Pages consume these through the React Query hooks in `@/lib/doormileHooks` +// rather than calling them directly, so cache keys stay coherent across the app. + +const buildQuery = (params = {}) => { + const usp = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') usp.append(key, value); + }); + const qs = usp.toString(); + return qs ? `?${qs}` : ''; +}; + +// ==============================|| Auth ||============================== // + +export const loginAdmin = async (email, password) => { + const response = await doormileAxios.post('/admin/login', { email, password }); + if (response.data.success) { + localStorage.setItem(DOORMILE_TOKEN_KEY, response.data.token); + localStorage.setItem(DOORMILE_USER_KEY, JSON.stringify(response.data.user)); + } + return response.data; +}; + +export const logoutAdmin = () => { + localStorage.removeItem(DOORMILE_TOKEN_KEY); + localStorage.removeItem(DOORMILE_USER_KEY); +}; + +// Drains a paginated list endpoint instead of taking whatever the server's +// default page size happens to be. +// +// GET /admin/customers defaults to `pagesize: 20` — confirmed live against an +// account with 37 customers, which came back as +// { pageno: 1, pagesize: 20, pages: 2, total: 37 }. Every caller of +// getAdminCustomers was therefore seeing 20 of 37 and had no way to know: +// the Customers page reported "Total Customers 20", and the customer lookups +// on the orders pages silently missed anyone on page 2. +// +// (express-console-api.md claims the default is 500. It is not, at least not +// for this endpoint. Trust the envelope, not the doc.) +const PAGE_SIZE = 100; // the doc's own example value — safer than assuming the 1000 cap holds +const MAX_PAGES = 12; + +const fetchAllPages = async (path) => { + const first = await doormileAxios.get(`${path}${buildQuery({ pageno: 1, pagesize: PAGE_SIZE })}`); + const body = first.data || {}; + const rows = body.data || []; + const total = Number(body.total); + if (!Number.isFinite(total) || rows.length >= total) return rows; + + const pageCount = Math.min(Math.ceil(total / PAGE_SIZE), MAX_PAGES); + for (let page = 2; page <= pageCount; page += 1) { + + const next = await doormileAxios.get(`${path}${buildQuery({ pageno: page, pagesize: PAGE_SIZE })}`); + const more = next.data?.data || []; + if (!more.length) break; + rows.push(...more); + } + return rows; +}; + +// ==============================|| Dashboard, profile, reports ||============================== // + +export const getDashboard = async () => { + const response = await doormileAxios.get('/admin/dashboard'); + return response.data.data; +}; + +// jupiter used fromdate/todate + applocationid (city); Doormile uses from/to +// and no city filter at all (filters by hubid instead — see +// jupiter2doormile.md §2.3/§6). Response carries by_location/by_hub/by_tenant/ +// by_rider blocks per that doc, not documented in express-console-api.md. +export const getReports = async (from, to, tenantid, locationid, hubid) => { + const response = await doormileAxios.get(`/admin/reports${buildQuery({ from, to, tenantid, locationid, hubid })}`); + return response.data.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done) as the replacement for +// jupiter's getlocationsummary — per-tenant-site report breakdown. Not in +// express-console-api.md. +export const getLocationsSummary = async (tenantid, locationid, from, to) => { + const response = await doormileAxios.get(`/admin/locations/summary${buildQuery({ tenantid, locationid, from, to })}`); + return response.data.data; +}; + +export const getProfile = async () => { + const response = await doormileAxios.get('/admin/profile'); + return response.data.data; +}; + +export const updateProfilePassword = async (current_password, new_password) => { + const response = await doormileAxios.put('/admin/profile/password', { current_password, new_password }); + return response.data; +}; + +// ==============================|| App users (staff logins) ||============================== // + +export const getAppUsers = async () => { + const response = await doormileAxios.get('/admin/users'); + return response.data.data; +}; + +export const createAppUser = async (data) => { + const response = await doormileAxios.post('/admin/users', data); + return response.data; +}; + +export const updateAppUser = async (id, data) => { + const response = await doormileAxios.put(`/admin/users/${id}`, data); + return response.data; +}; + +export const deleteAppUser = async (id) => { + const response = await doormileAxios.delete(`/admin/users/${id}`); + return response.data; +}; + +// ==============================|| Partners (fleet / rider suppliers) ||============================== // + +export const getPartners = async () => { + const response = await doormileAxios.get('/admin/partners'); + return response.data.data; +}; + +export const createPartner = async (data) => { + const response = await doormileAxios.post('/admin/partners', data); + return response.data; +}; + +export const getPartner = async (id) => { + const response = await doormileAxios.get(`/admin/partners/${id}`); + return response.data.data; +}; + +export const updatePartner = async (id, data) => { + const response = await doormileAxios.put(`/admin/partners/${id}`, data); + return response.data; +}; + +export const deletePartner = async (id) => { + const response = await doormileAxios.delete(`/admin/partners/${id}`); + return response.data; +}; + +// ==============================|| Tenants (client companies) ||============================== // + +export const getAdminTenants = async () => { + const response = await doormileAxios.get('/admin/tenants'); + return response.data.data; +}; + +export const createAdminTenant = async (data) => { + const response = await doormileAxios.post('/admin/tenants', data); + return response.data; +}; + +export const getAdminTenant = async (id) => { + const response = await doormileAxios.get(`/admin/tenants/${id}`); + return response.data.data; +}; + +export const updateAdminTenant = async (id, data) => { + // requiredeliveryotp is a pointer server-side — omit the key entirely to leave the setting alone. + const response = await doormileAxios.put(`/admin/tenants/${id}`, data); + return response.data; +}; + +export const deleteAdminTenant = async (id) => { + const response = await doormileAxios.delete(`/admin/tenants/${id}`); + return response.data; +}; + +export const getTenantLocations = async (tenantId) => { + const response = await doormileAxios.get(`/admin/tenants/${tenantId}/locations`); + return response.data.data; +}; + +export const createTenantLocation = async (tenantId, data) => { + const response = await doormileAxios.post(`/admin/tenants/${tenantId}/locations`, data); + return response.data; +}; + +export const updateTenantLocation = async (locationId, data) => { + // Not nested under the tenant — PUT /admin/tenantlocations/:id. + const response = await doormileAxios.put(`/admin/tenantlocations/${locationId}`, data); + return response.data; +}; + +// ==============================|| Tenant customers (a client's own end customers) ||============================== // + +export const getTenantCustomers = async () => fetchAllPages('/admin/tenantcustomers'); + +export const createTenantCustomer = async (data) => { + const response = await doormileAxios.post('/admin/tenantcustomers', data); + return response.data; +}; + +export const getTenantCustomer = async (id) => { + const response = await doormileAxios.get(`/admin/tenantcustomers/${id}`); + return response.data.data; +}; + +export const updateTenantCustomer = async (id, data) => { + const response = await doormileAxios.put(`/admin/tenantcustomers/${id}`, data); + return response.data; +}; + +export const deleteTenantCustomer = async (id) => { + const response = await doormileAxios.delete(`/admin/tenantcustomers/${id}`); + return response.data; +}; + +// ==============================|| B2C app customers ||============================== // + +export const getAdminCustomers = async () => fetchAllPages('/admin/customers'); + +// POST /admin/customers. +// +// express-console-api.md lists this resource as GET + PATCH only, and states +// that a customer there comes into existence through a booking. Added on +// explicit instruction to create into the store the Customers page actually +// reads. If the route genuinely does not exist the call returns 404/405, and +// the assistant surfaces that verbatim with the endpoint name rather than +// failing silently — so an unsupported route is immediately visible. +// +// Body mirrors this resource's own GET shape. +export const createAdminCustomer = async (data) => { + const response = await doormileAxios.post('/admin/customers', data); + return response.data; +}; + +export const updateAdminCustomer = async (id, data) => { + const response = await doormileAxios.patch(`/admin/customers/${id}`, data); + return response.data; +}; + +// ==============================|| Hubs ||============================== // + +export const getHubs = async () => { + const response = await doormileAxios.get('/admin/hubs'); + return response.data.data; +}; + +export const createHub = async (data) => { + const response = await doormileAxios.post('/admin/hubs', data); + return response.data; +}; + +export const getHub = async (id) => { + const response = await doormileAxios.get(`/admin/hubs/${id}`); + return response.data.data; +}; + +export const updateHub = async (id, data) => { + const response = await doormileAxios.put(`/admin/hubs/${id}`, data); + return response.data; +}; + +export const deleteHub = async (id) => { + // Soft delete server-side. + const response = await doormileAxios.delete(`/admin/hubs/${id}`); + return response.data; +}; + +// ==============================|| Vehicles ||============================== // + +export const getVehicles = async () => { + const response = await doormileAxios.get('/admin/vehicles'); + return response.data.data; +}; + +export const createVehicle = async (data) => { + const response = await doormileAxios.post('/admin/vehicles', data); + return response.data; +}; + +export const getVehicle = async (id) => { + const response = await doormileAxios.get(`/admin/vehicles/${id}`); + return response.data.data; +}; + +export const updateVehicle = async (id, data) => { + const response = await doormileAxios.put(`/admin/vehicles/${id}`, data); + return response.data; +}; + +export const deleteVehicle = async (id) => { + // Soft delete server-side. + const response = await doormileAxios.delete(`/admin/vehicles/${id}`); + return response.data; +}; + +// ==============================|| Milers (riders) ||============================== // + +export const getMilers = async () => { + // Tenant-scoped server-side from the token — no tenantid param needed/accepted. + const response = await doormileAxios.get('/admin/milers'); + return response.data.data; +}; + +export const createMiler = async (data) => { + // configid defaults to 1001 server-side (the miler-app login partition) — do not override it. + const response = await doormileAxios.post('/admin/milers', data); + return response.data; +}; + +export const getMiler = async (id) => { + const response = await doormileAxios.get(`/admin/milers/${id}`); + return response.data.data; +}; + +export const updateMiler = async (id, data) => { + const response = await doormileAxios.put(`/admin/milers/${id}`, data); + return response.data; +}; + +export const blockMiler = async (id, data) => { + const response = await doormileAxios.put(`/admin/milers/${id}/block`, data); + return response.data; +}; + +export const assignMilerVehicle = async (id, data) => { + const response = await doormileAxios.put(`/admin/milers/${id}/assign-vehicle`, data); + return response.data; +}; + +export const notifyMiler = async (milerProfileId, title, message) => { + const response = await doormileAxios.post(`/admin/milers/${milerProfileId}/notify`, { title, message }); + return response.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done — tested against a real +// request), not in express-console-api.md. jupiter used fromdate/todate; +// Doormile uses from/to (both YYYY-MM-DD). Aggregate rider KPI counts + +// current position (currentlatitude/currentlongitude/lastpingat per the doc — +// unconfirmed beyond those three field names). +export const getMilerSummary = async (applocationid, from, to, tenantid, hubid) => { + const response = await doormileAxios.get(`/admin/milers/summary${buildQuery({ applocationid, from, to, tenantid, hubid })}`); + return response.data.data; +}; + +// Replaces jupiter's `riderlogs` table — the rider's GPS/status ping history. +export const getMilerLogs = async (id, from, to, limit) => { + const response = await doormileAxios.get(`/admin/milers/${id}/logs${buildQuery({ from, to, limit })}`); + return response.data.data; +}; + +// No jupiter equivalent — new in Doormile. +export const getMilerActivity = async (id, from, to) => { + const response = await doormileAxios.get(`/admin/milers/${id}/activity${buildQuery({ from, to })}`); + return response.data.data; +}; + +// ==============================|| Bookings ||============================== // + +export const getBookings = async (pageno, pagesize) => { + const response = await doormileAxios.get(`/admin/bookings${buildQuery({ pageno, pagesize })}`); + return response.data.data; +}; + +// Same request as getBookings, but keeps the envelope's `total`/`page` instead +// of throwing them away. +// +// getBookings returns `response.data.data` — just the rows — which means no +// caller can tell a full result from a truncated one. `pagesize` is capped at +// 1000 server-side (express-console-api.md, "Conventions": default 500, cap +// 1000), so any account with more than 1000 bookings silently gets a partial +// list from a single call. That's tolerable for a paginated table (it fetches +// the next page as you scroll) but NOT for anything that counts or sums, which +// would report a confidently wrong number. +// +// Kept as a separate export rather than changing getBookings' return shape: +// four callers (fetchDeliveries, fetchCountAPI, orders.js, the assistant's own +// per-order lookup) already destructure it as a bare array. +export const getBookingsPage = async (pageno, pagesize) => { + const response = await doormileAxios.get(`/admin/bookings${buildQuery({ pageno, pagesize })}`); + const body = response.data || {}; + return { + rows: body.data || [], + // `total` is documented as present on list responses; fall back to the row + // count so a backend that omits it degrades to "this is everything" rather + // than to NaN-driven pagination. + total: Number.isFinite(Number(body.total)) ? Number(body.total) : (body.data || []).length, + page: Number(body.page) || pageno + }; +}; + +export const createExpressBooking = async (data) => { + // tenantid is forced to the caller's own tenant server-side on client logins. + // CityGate applies: pickup pincode prefix must be an open city (641/600/560/500/629). + const response = await doormileAxios.post('/admin/expressbooking', data); + return response.data; +}; + +export const createExpressBookingBulk = async (bookings) => { + // Max 200 per call; response carries per-row results. + const response = await doormileAxios.post('/admin/expressbooking/bulk', { bookings }); + return response.data; +}; + +export const getBooking = async (id) => { + const response = await doormileAxios.get(`/admin/bookings/${id}`); + return response.data.data; +}; + +export const assignMilerToBooking = async (id, data) => { + const response = await doormileAxios.post(`/admin/bookings/${id}/assign-miler`, data); + return response.data; +}; + +// Doormile-native bulk assign: picks real milers via Redis GEO + AI scoring, no +// external solver involved. Commits server-side in this one call — unlike the +// workolik solver flow, there's no separate preview/reconcile/commit step. +// Sequencing (POST /optimization/doormile/sequence) is not deployed yet, so +// riders with more than one stop come back unsequenced (step: 0) — see +// doormile-flow.md's State table. +export const batchAssignBookings = async (bookingIds, maxPerRider = 5) => { + const response = await doormileAxios.post('/hub/bookings/batch-assign', { + bookingids: bookingIds, + max_per_rider: maxPerRider + }); + return response.data; +}; + +export const assignVehicleToBooking = async (id, data) => { + const response = await doormileAxios.post(`/admin/bookings/${id}/assign-vehicle`, data); + return response.data; +}; + +export const updateBookingStatus = async (id, data) => { + const response = await doormileAxios.put(`/admin/bookings/${id}/status`, data); + return response.data; +}; + +export const cancelBooking = async (id, data) => { + const response = await doormileAxios.post(`/admin/bookings/${id}/cancel`, data); + return response.data; +}; + +export const bulkCancelBookings = async (bookingIds) => { + const response = await doormileAxios.post('/admin/bookings/bulk-cancel', { bookingIds }); + return response.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done), not in +// express-console-api.md. +export const getBookingTrack = async (id) => { + const response = await doormileAxios.get(`/admin/bookings/${id}/track`); + return response.data.data; +}; + +// ==============================|| Consignments ||============================== // + +export const getConsignments = async () => { + const response = await doormileAxios.get('/admin/consignments'); + return response.data.data; +}; + +// Confirmed against jupiter2doormile.md (Status: Done) — the actual GPS/status +// trail for a consignment, not in express-console-api.md. +export const getConsignmentLogs = async (id) => { + const response = await doormileAxios.get(`/admin/consignments/${id}/logs`); + return response.data.data; +}; + +export const getConsignment = async (id) => { + const response = await doormileAxios.get(`/admin/consignments/${id}`); + return response.data.data; +}; + +export const trackConsignment = async (trackingno) => { + const response = await doormileAxios.get(`/admin/consignments/track/${trackingno}`); + return response.data.data; +}; + +export const updateConsignmentStatus = async (id, data) => { + const response = await doormileAxios.put(`/admin/consignments/${id}/status`, data); + return response.data; +}; + +// ==============================|| Tripsheets (hub-to-hub transport) ||============================== // + +export const getTripsheets = async () => { + const response = await doormileAxios.get('/admin/tripsheets'); + return response.data.data; +}; + +export const createTripsheet = async (data) => { + const response = await doormileAxios.post('/admin/tripsheets', data); + return response.data; +}; + +export const getTripsheet = async (id) => { + const response = await doormileAxios.get(`/admin/tripsheets/${id}`); + return response.data.data; +}; + +export const addTripsheetItem = async (id, consignmentid) => { + const response = await doormileAxios.post(`/admin/tripsheets/${id}/items`, { consignmentid }); + return response.data; +}; + +export const removeTripsheetItem = async (id, itemId) => { + const response = await doormileAxios.delete(`/admin/tripsheets/${id}/items/${itemId}`); + return response.data; +}; + +export const dispatchTripsheet = async (id) => { + const response = await doormileAxios.put(`/admin/tripsheets/${id}/dispatch`); + return response.data; +}; + +export const arriveTripsheet = async (id) => { + const response = await doormileAxios.put(`/admin/tripsheets/${id}/arrive`); + return response.data; +}; + +// ==============================|| Pricing ||============================== // + +export const getAdminPricing = async () => { + const response = await doormileAxios.get('/admin/pricing'); + return response.data.data; +}; + +export const createAdminPricing = async (data) => { + const response = await doormileAxios.post('/admin/pricing', data); + return response.data; +}; + +export const updateAdminPricing = async (id, data) => { + const response = await doormileAxios.put(`/admin/pricing/${id}`, data); + return response.data; +}; + +export const deleteAdminPricing = async (id) => { + const response = await doormileAxios.delete(`/admin/pricing/${id}`); + return response.data; +}; + +export const simulatePricing = async (data) => { + const response = await doormileAxios.post('/admin/pricing/simulate', data); + return response.data; +}; + +export const quotePricing = async (data) => { + // Same handler as simulate. + const response = await doormileAxios.post('/admin/pricing/quote', data); + return response.data; +}; + +export const getDoormilePricing = async () => { + const response = await doormileAxios.get('/admin/doormile-pricing'); + return response.data.data; +}; + +export const createDoormilePricing = async (data) => { + const response = await doormileAxios.post('/admin/doormile-pricing', data); + return response.data; +}; + +export const updateDoormilePricing = async (id, data) => { + const response = await doormileAxios.put(`/admin/doormile-pricing/${id}`, data); + return response.data; +}; + +export const deleteDoormilePricing = async (id) => { + // Soft delete server-side. + const response = await doormileAxios.delete(`/admin/doormile-pricing/${id}`); + return response.data; +}; + +// ==============================|| Exceptions ||============================== // + +export const getExceptions = async () => { + const response = await doormileAxios.get('/admin/exceptions'); + return response.data.data; +}; + +export const createException = async (data) => { + const response = await doormileAxios.post('/admin/exceptions', data); + return response.data; +}; + +export const getException = async (id) => { + const response = await doormileAxios.get(`/admin/exceptions/${id}`); + return response.data.data; +}; + +export const updateExceptionStatus = async (id, status, resolution) => { + // status: 'Resolved' | 'Closed'. + const response = await doormileAxios.put(`/admin/exceptions/${id}/status`, { status, resolution }); + return response.data; +}; + +// ==============================|| Competitive intel ||============================== // + +// Confirmed live: this resource paginates on page/limit, not the +// pageno/pagesize convention used everywhere else in the API — and defaults +// to only 50 rows (there are 339+ competitor branches on a real tenant), so +// callers must page through it explicitly rather than trusting one fetch to +// return everything. Returns the full envelope (data/limit/page/total) so +// the caller can build pagination controls. +export const getCompetitorBranches = async (page, limit) => { + const response = await doormileAxios.get(`/admin/competitor-branches${buildQuery({ page, limit })}`); + return response.data; +}; + +export const createCompetitorBranch = async (data) => { + const response = await doormileAxios.post('/admin/competitor-branches', data); + return response.data; +}; + +export const updateCompetitorBranch = async (id, data) => { + const response = await doormileAxios.put(`/admin/competitor-branches/${id}`, data); + return response.data; +}; + +export const deleteCompetitorBranch = async (id) => { + const response = await doormileAxios.delete(`/admin/competitor-branches/${id}`); + return response.data; +}; + +// Same page/limit pagination convention as competitor-branches (confirmed +// live) — 368+ rows on a real tenant, default page returns only 100. +export const getCarrierPricing = async (page, limit) => { + const response = await doormileAxios.get(`/admin/carrier-pricing${buildQuery({ page, limit })}`); + return response.data; +}; + +export const createCarrierPricing = async (data) => { + const response = await doormileAxios.post('/admin/carrier-pricing', data); + return response.data; +}; + +export const updateCarrierPricing = async (id, data) => { + const response = await doormileAxios.put(`/admin/carrier-pricing/${id}`, data); + return response.data; +}; + +export const deleteCarrierPricing = async (id) => { + const response = await doormileAxios.delete(`/admin/carrier-pricing/${id}`); + return response.data; +}; diff --git a/src/api/doormile/index.js b/src/api/doormile/index.js new file mode 100644 index 0000000..51b2bbd --- /dev/null +++ b/src/api/doormile/index.js @@ -0,0 +1,29 @@ +/** + * The Doormile Express data layer — the single import surface for pages. + * + * import { getHubs, fetchDeliveries } from '@/api/doormile'; + * + * Three files sit underneath: + * + * - `client.js` — the axios instance, the bearer token, the 401 handling. + * - `endpoints.js` — one function per `api.doormile.com/api/v1/admin/*` route. + * - `queries.js` — the page-facing layer: joins, status mapping, the solver + * calls, and the safe empty defaults for the resources the + * admin API has no equivalent for. + * + * Pages should generally reach for the React Query hooks in + * `@/lib/doormileHooks` instead, so cache keys stay coherent across the app. + */ + +export { default as doormileAxios } from './client'; +export { + DOORMILE_BASE_URL, + DOORMILE_TOKEN_KEY, + DOORMILE_USER_KEY, + errorMessage, + readStoredToken, + readStoredUser, +} from './client'; +export { OpenToast, messageOf } from './notify'; +export * from './endpoints'; +export * from './queries'; diff --git a/src/api/doormile/notify.js b/src/api/doormile/notify.js new file mode 100644 index 0000000..ea7b85d --- /dev/null +++ b/src/api/doormile/notify.js @@ -0,0 +1,22 @@ +import { toast } from '@/components/ds/toast'; + +/** + * The data layer's toast seam. + * + * The Express Console signals API failures with `OpenToast(message, variant, + * duration)`. Keeping that call signature means the ported query functions read + * exactly like their originals — the variants just resolve to the KROW design + * system's toast instead of notistack, so a failed fetch looks like every other + * notification in this app rather than importing a second toast system. + */ +export const OpenToast = (message, variant = 'error', duration) => { + const text = String(message || 'Something went wrong'); + const options = duration ? { duration } : undefined; + if (variant === 'success') return toast.success(text, options); + if (variant === 'warning' || variant === 'info') return toast.info(text, options); + return toast.error(text, options); +}; + +/** The message a rejected Doormile call should show. */ +export const messageOf = (err, fallback = 'Something went wrong') => + err?.response?.data?.message || err?.message || fallback; diff --git a/src/api/doormile/queries.js b/src/api/doormile/queries.js new file mode 100644 index 0000000..5740b10 --- /dev/null +++ b/src/api/doormile/queries.js @@ -0,0 +1,1107 @@ +import axios from 'axios'; +import { OpenToast } from './notify'; +import logger from '@/lib/logger'; +import { parseDoormileTimestamp } from '@/lib/doormileTimestamp'; +import { + getMilers, + getMiler, + getMilerSummary, + getMilerLogs, + notifyMiler, + getAdminTenants, + getAdminCustomers, + getTenantLocations, + getHubs, + getBookings, + getBooking, + getConsignments, + assignMilerToBooking, + cancelBooking, + updateConsignmentStatus, + getAdminPricing, + getReports, + getLocationsSummary, + getProfile +} from './endpoints'; + +// The page-facing query layer, ported from the Express Console's +// `pages/api/api.js`. Everything here talks to api.doormile.com through +// `./endpoints` — the one exception is the AI dispatch optimiser +// (routes.workolik.com / routemate.workolik.com) and its final delivery commit, +// which is a separate solver service with no equivalent in the admin API. +// +// Several resources the old backend exposed (payment-mode types, tenant tab +// counts, rider check-in history, per-location/per-rider report breakdowns, +// rider login/checkout audit logs, live rider GPS/battery logs) have no +// equivalent in the new API. Those functions below return safe empty defaults +// (rather than throwing) so pages render an empty state instead of crashing — +// each is commented with why. + +// The Deliveries page's STATUS_META/STATUS_TABS still use the old jupiter +// lifecycle keys (pending/accepted/arrived/picked/active/skipped/delivered/ +// cancelled) for its tabs, chip counts, and row badges. The new booking +// status enum uses different strings entirely (Pending_Pickup, +// Miler_Assigned, Pickup_Scheduled, Converted_To_Consignment, Out_for_Delivery, +// ...) — Pending_Pickup, Converted_To_Consignment, Out_for_Delivery (named in +// express-console-api.md's CityGate note), and now Miler_Assigned / +// Pickup_Scheduled (confirmed live via a real booking-status breakdown logged +// right after an assign-miler call — see orders.js's ORDERS_STATUS_TABS +// comment) are confirmed; the rest of this mapping is still a best-effort +// guess. Unmapped statuses pass through lowercased, which the page's own +// fallback renders as an "unknown" badge rather than crashing, AND the +// Deliveries page now surfaces those rows under an "Other" tab that names the +// unmapped enum — previously such a row was counted nowhere and shown nowhere. +// `picked` now has a confirmed source (Converted_To_Consignment, below). +// `arrived` and `skipped` still have none: the rider actions that produce them +// (POST /miler/bookings/:id/reached, POST /miler/consignments/:id/skip) have no +// booking-status equivalent in the confirmed enum, so those two tabs stay at 0 +// until the backend is confirmed to expose them. +// +// Miler_Assigned is deliberately kept on 'pending', NOT bumped to 'accepted'. +// Assigning a rider is an OPERATOR action (POST /admin/bookings/:id/assign-miler +// from this console); "Accepted" is meant to reflect the RIDER's own action +// (doormile-flow.md's POST /miler/assignments/:id/accept). Conflating the two +// made a just-assigned, not-yet-acknowledged order look already-accepted — +// wrong from an ops standpoint (per explicit product requirement: stays +// "Pending" in the operator's eyes until the rider actually accepts). +// Pickup_Scheduled — the status that appears once a rider has accepted and +// the pickup is on their route — is the best available proxy for "rider +// accepted" in the currently-confirmed enum, so that one maps to 'accepted'. +// If the backend turns out to have a distinct status specifically for the +// accept action, add it here rather than reusing Miler_Assigned for it. +// Converted_To_Consignment is PICKED, not accepted. doormile-flow.md §5 is +// explicit: `POST /miler/bookings/:bookingid/pickup-complete` "converts the +// booking into a consignment" — so the status is written at the moment the +// rider completes pickup, and the parcel is in their hands. deliveries.js says +// the same thing from the other side ("this order hasn't been picked up yet — +// status can only be updated once it becomes a consignment"). +// +// It sat on 'accepted', which held a picked-up parcel in the Accepted tab and +// left Picked permanently empty — nothing else in the enum maps to it. +const BOOKING_STATUS_TO_DELIVERY_STATUS = { + pending_pickup: 'pending', + miler_assigned: 'pending', + pickup_scheduled: 'accepted', + converted_to_consignment: 'picked', + out_for_delivery: 'active', + delivered: 'delivered', + cancelled: 'cancelled' +}; +// Exported (not just module-local) so anything else that needs to classify a +// raw booking status — the operator bot's status-breakdown intent included — +// reuses this instead of growing a second copy of BOOKING_STATUS_TO_DELIVERY_STATUS +// that can drift from it. +export const mapBookingStatusToDeliveryStatus = (status) => { + const key = String(status || '').toLowerCase(); + return BOOKING_STATUS_TO_DELIVERY_STATUS[key] || key; +}; + +// A consignment's own status, when this booking has one and it looks like a +// status. `GET /admin/consignments` has no documented response schema, so this +// reads defensively: only a non-empty string on one of the plausible field +// names counts, and anything else returns undefined so the caller keeps using +// the booking's status. +const consignmentStatusFor = (booking, consignmentMap) => { + if (!booking?.consignmentid || !consignmentMap?.size) return undefined; + const record = consignmentMap.get(String(booking.consignmentid)); + if (!record) return undefined; + const raw = record.status ?? record.consignmentstatus ?? record.currentstatus; + return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined; +}; + +// Haversine straight-line distance in km — used wherever a resource carries +// lat/lng but no road-distance field (bookings, and by extension deliveries). +const haversineKm = (lat1, lon1, lat2, lon2) => { + if (![lat1, lon1, lat2, lon2].every(Number.isFinite)) return undefined; + const R = 6371; + const toRad = (d) => (d * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lon2 - lon1); + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +}; + +// ==============================|| getRiderPeriodicLogs ||============================== // +// Confirmed by jupiter2doormile.md as the direct replacement for jupiter's +// riderlogs table (Status: Done): GET /admin/milers/:id/logs. Field names on +// a log entry aren't documented anywhere (no example JSON in either +// reference doc) — this returns the most recent entry as-is and lets the +// page's own optional-chaining render whatever fields actually come back. +// Falls back to the miler record itself (which may carry a last-known +// position) if the logs call fails or returns nothing. +// Despite the parameter name (kept for caller compatibility), this must be +// a milerprofileid — /admin/milers/:id/* routes 404 on a plain userid +// (confirmed live). +export const getRiderPeriodicLogs = async (userid) => { + if (!userid) return null; + try { + const logs = await getMilerLogs(userid, undefined, undefined, 1); + const latest = Array.isArray(logs) ? logs[0] : logs; + if (latest) return latest; + } catch { + // Falls through to the getMiler() fallback below — this is polled every + // 15s by the live map, so a toast here would spam the operator on every + // failed tick. Only surface an error if the fallback also fails. + } + try { + return (await getMiler(userid)) || null; + } catch (err) { + return null; + } +}; + +// ==============================|| fetchAppLocations (zone/location picker) ||============================== // +// The new API has no "zones" resource — applocationid only exists as a field +// on Hubs. Derive a zone picker list from the distinct cities in GET /admin/hubs. +// +// Scoped per tenant: Hub has no tenantid field at all (confirmed against +// express-console-api.md), so GET /admin/hubs always returns every hub +// nationwide with no server-side tenant filtering possible. A client-tenant +// login should only see their own city's hub(s) — inferred from the +// tenant's own GET /admin/tenants/:id/locations (city is free text there, +// and on the hub side too, so matched via normalized exact-then-substring +// comparison, not a clean foreign key). Staff logins (tenantid falsy) keep +// seeing every hub, unchanged. Fails open (full list) whenever the tenant's +// city can't be determined or nothing matches, rather than ever locking an +// operator out with an empty picker. +const normCity = (s) => + String(s || '') + .trim() + .toLowerCase(); + +export const fetchAppLocations = async () => { + try { + const hubs = await getHubs(); + const seen = new Map(); + (hubs || []).forEach((hub) => { + if (hub.applocationid != null && !seen.has(hub.applocationid)) { + seen.set(hub.applocationid, { applocationid: hub.applocationid, locationname: hub.city || hub.hubname }); + } + }); + const allLocations = [...seen.values()]; + + const tenantid = localStorage.getItem('tenantid'); + const isStaff = !tenantid || tenantid === '0'; + if (isStaff) { + return [...allLocations, { locationname: 'All', applocationid: 0 }]; + } + + const tenantLocations = await gettenantlocations(tenantid); + const tenantCities = new Set((tenantLocations || []).map((loc) => normCity(loc.city)).filter(Boolean)); + + if (tenantCities.size === 0) { + OpenToast("Could not determine your tenant's city — showing all zones.", 'warning', 3000); + return [...allLocations, { locationname: 'All', applocationid: 0 }]; + } + + let matched = allLocations.filter((loc) => tenantCities.has(normCity(loc.locationname))); + if (matched.length === 0) { + matched = allLocations.filter((loc) => { + const hubNorm = normCity(loc.locationname); + return [...tenantCities].some((cityNorm) => hubNorm.includes(cityNorm) || cityNorm.includes(hubNorm)); + }); + } + + if (matched.length === 0) { + OpenToast("No zones matched your tenant's city — showing all zones.", 'warning', 3000); + return [...allLocations, { locationname: 'All', applocationid: 0 }]; + } + + return matched; + } catch (err) { + OpenToast(err.message, 'error', 2000); + return [{ locationname: 'All', applocationid: 0 }]; + } +}; + +// ==============================|| fetchPercentageData (orders) ||============================== // +// Best-effort against /admin/reports — field names are our assumption since +// the doc only promises "counts + today's numbers", not an exact schema. +export const fetchPercentageData = async ({ queryKey }) => { + const [, , startdate, enddate] = queryKey; + try { + const details = (await getReports(startdate, enddate)) || {}; + const total = details.total || 0; + return { + created: String(details.created || 0), + uncoveredOrders: String(details.pending || 0), + coveredOrders: String(details.delivered || 0), + cancelled: String(details.cancelled || 0), + percentage1: String(Math.round(((details.created || 0) / total) * 100) || 0), + percentage2: String(Math.round(((details.pending || 0) / total) * 100) || 0), + percentage3: String(Math.round(((details.delivered || 0) / total) * 100) || 0), + percentage4: String(Math.round(((details.cancelled || 0) / total) * 100) || 0) + }; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load order percentages', 'error', 2000); + return { + created: '0', + uncoveredOrders: '0', + coveredOrders: '0', + cancelled: '0', + percentage1: '0', + percentage2: '0', + percentage3: '0', + percentage4: '0' + }; + } +}; + +// ===================================================== || getTenants || ===================================================== + +export const getTenants = async () => { + try { + const tenants = await getAdminTenants(); + return (tenants || []).map((val) => ({ ...val, label: val.tenantname })); + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load tenants', 'error', 2000); + return []; + } +}; + +// ============================================= || gettenantlocations (branches) || ============================================= +export const gettenantlocations = async (tenantId) => { + try { + return (await getTenantLocations(tenantId)) || []; + } catch (err) { + // Log only the message — the raw error object carries the request + // config, including the Authorization: Bearer header + // doormileAxios attaches, which shouldn't hit the console. + OpenToast(err.response?.data?.message || err.message || 'Failed to load tenant locations', 'error', 2000); + return []; + } +}; + +// ==============================|| fetchPaymentType (orders) ||============================== // +// No payment-mode-types endpoint in the new API. +export const fetchPaymentType = async () => []; + +// ==============================|| fetchRidersList (orders) ||============================== // + +export const fetchRidersList = async () => { + try { + const milers = await getMilers(); + return (milers || []).map((val) => { + const name = val.displayname || val.authname || ''; + return { + ...val, + // Only append " | phone" when a phone actually exists — an + // unconditional template literal left a dangling " | " on every + // rider missing contactno, rendering literally in every dropdown + // that falls back to this default label (e.g. OrdersPreview.js, + // Preview.js's Change Rider dialog). + label: val.contactno ? `${name} | ${val.contactno}` : name + }; + }); + } catch (err) { + // Was `throw`ing after already toasting here — deliveries.js also wires + // its own onError toast on this same query, so a real failure showed + // the operator the same error twice. Matches every sibling function's + // convention now: toast once, return a safe default, never throw (the + // other 4 call sites had no onError of their own and got zero feedback + // on failure before this fix). + OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000); + return []; + } +}; + +// ==============================|| createOptimisationDeliveries (orders) Arrange the order ||============================== // +// AI dispatch optimiser — a separate solver service, left untouched by this +// migration (no equivalent exists in the new admin API). +export const createOptimisationDeliveries = async (deliveryData) => { + const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/createdeliveries`, deliveryData.deliveries); + return response.data; +}; +// ==============================|| reconcileSteps (Preview - validate rider/order step assignments) ||============================== // + +export const reconcileSteps = async ({ riders }) => { + logger.debug(`reconcileSteps: posting ${riders?.length ?? 0} rider(s)`, riders); + const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/reconcile-steps`, { riders }); + // Diagnostic: this is an external, unverified solver contract (see this + // area's CLAUDE.md) — Preview.js's reconcileMutation only clears the + // dirty-rider set (which is what re-enables Assign Orders) when + // response.data.riders is an array. If the solver's real response shape + // is different (wrapped in an envelope, a different key name, etc.), + // that check silently fails every time and Assign Orders can never + // re-enable — logging the exact raw shape here settles it either way. + logger.debug('reconcileSteps: raw response.data', response.data); + return response.data; +}; + +// ==============================|| fetchBatchEfficiency (Dispatch - Analysis view) ||============================== // + +export const fetchBatchEfficiency = async ({ batch, tenantId }) => { + const response = await axios.post( + `https://routes.workolik.com/api/v1/batch/efficiency`, + { batch, tenant_id: tenantId }, + { headers: { 'Content-Type': 'application/json' }, validateStatus: () => true } + ); + return response.data; +}; +// ==============================|| finalCreatedeliveries (orders) ||============================== // +// Final delivery commit. Used to POST the whole batch to jupiter's +// /deliveries/createdeliveries in one shot — retired for two confirmed +// reasons: (1) it 500'd because jupiter's Deliveries Go struct types +// pickuplat/pickuplon/droplat/droplon as string, while GET /admin/bookings +// (the new Doormile API) feeds orders.js's solver payload with those as +// numbers; (2) even once that's patched, jupiter writes into jupiter's own +// now-orphaned database — neither the Orders "pending" list nor the +// Deliveries "dispatched" filter read it, since both come from Doormile's +// GET /admin/bookings. jupiter2doormile.md §4 (the "assign a rider" row, +// mapped from jupiter's old PUT /deliveries/updatedelivery) and +// express-console-api.md's exercised-routes list both confirm +// POST /admin/bookings/:id/assign-miler as the correct, live-proven +// replacement for this exact action — not POST /admin/expressbooking +// (that one creates a brand-new booking from scratch; wrong here, since +// every order already exists as a Doormile booking pulled from +// GET /admin/bookings). +// Shared by finalCreatedeliveries (here) and Preview.js's pre-commit +// verification UI, so both use the IDENTICAL matching rule rather than two +// copies that could silently drift apart. userid/milerprofileid matching is +// tried first (the solver's rider pool is fed full miler objects for +// Auto/multi-trip mode — see Preview.js's handleCreateDelivery -> `riders: +// autoRiders`, raw GET /admin/milers data carrying both fields per rider) +// but confirmed live that for Bike hypertuning mode NEITHER matches: that +// solver never receives a rider pool at all and assigns from its own +// internal roster, seeded against jupiter rider ids when the integration +// was first built — disconnected from Doormile's id space entirely (see +// jupiter2doormile.md comparison). Falls back to matching the rider's NAME +// (also echoed by the solver, see flattenRiders' rider_name) against each +// miler's displayname/authname — the only other correlatable field. +const normMilerName = (s) => + String(s || '') + .trim() + .toLowerCase(); + +export const buildMilerLookup = (milers) => { + const byUserId = new Map((milers || []).map((m) => [String(m.userid), m])); + const byProfileId = new Map((milers || []).map((m) => [String(m.milerprofileid), m])); + const byName = new Map(); + (milers || []).forEach((m) => { + [m.displayname, m.authname].forEach((n) => { + const key = normMilerName(n); + if (key && !byName.has(key)) byName.set(key, m); + }); + }); + return { byUserId, byProfileId, byName }; +}; + +export const resolveMilerForOrder = (order, lookup) => { + const riderUserId = order.rider_id ?? order.userid; + const riderName = order.rider_name ?? order.rider; + const matchedVia = lookup.byUserId.has(String(riderUserId)) + ? 'userid' + : lookup.byProfileId.has(String(riderUserId)) + ? 'milerprofileid' + : lookup.byName.has(normMilerName(riderName)) + ? 'name' + : null; + const rider = + lookup.byUserId.get(String(riderUserId)) ?? lookup.byProfileId.get(String(riderUserId)) ?? lookup.byName.get(normMilerName(riderName)); + return rider?.milerprofileid ? { rider, matchedVia } : null; +}; + +export const finalCreatedeliveries = async (deliveryData) => { + const deliveries = deliveryData.deliveries || []; + logger.debug(`finalCreatedeliveries: ${deliveries.length} order(s) to assign`); + if (!deliveries.length) return { success: true, assigned: 0, failed: 0 }; + + // Diagnostic only (no PII beyond a rider's display name, which every + // operator already sees in the UI). Shows exactly which candidate field + // names are actually present, so a wrong guess in the fallback chain + // below is visible instead of silently mismatching. + deliveries.forEach((d, i) => { + logger.debug(` order[${i}] id fields:`, { + bookingid: d.bookingid, + orderheaderid: d.orderheaderid, + deliveryid: d.deliveryid, + orderid: d.orderid, + rider_id: d.rider_id, + userid: d.userid, + rider_name: d.rider_name ?? d.rider + }); + }); + + let milers = []; + try { + milers = (await getMilers()) || []; + } catch (err) { + logger.error('finalCreatedeliveries: GET /admin/milers failed', err.response?.status, err.response?.data || err.message); + } + logger.debug( + `finalCreatedeliveries: ${milers.length} miler(s) available for rider resolution`, + milers.map((m) => ({ userid: m.userid, milerprofileid: m.milerprofileid, name: m.displayname || m.authname })) + ); + const lookup = buildMilerLookup(milers); + + // Booking id resolution: confirmed live that guessing a single field name + // (bookingid first, on the assumption the solver passes unknown fields + // through untouched) sends assign-miler a wrong id — a small, sequential- + // looking number that 404s. The AI solver is a separate, unverified + // service (see this area's CLAUDE.md); there's no reliable way to know + // which candidate field it actually preserves. Instead of guessing, + // fetch the tenant's real current booking list once and VALIDATE each + // candidate against it, using whichever one actually matches a real + // booking — this is correct regardless of which field the solver happens + // to preserve, and regardless of which one changes in a future solver + // update. + let realBookingIds = new Set(); + try { + const realBookings = (await getBookings(1, 1000)) || []; + realBookingIds = new Set(realBookings.map((b) => String(b.bookingid))); + logger.debug(`finalCreatedeliveries: ${realBookingIds.size} real booking id(s) fetched for validation`); + } catch (err) { + logger.error( + 'finalCreatedeliveries: GET /admin/bookings failed — cannot validate booking ids', + err.response?.status, + err.response?.data || err.message + ); + } + + const results = await Promise.allSettled( + deliveries.map(async (d) => { + const candidates = [d.bookingid, d.orderheaderid, d.deliveryid, d.orderid]; + const bookingId = candidates.find((c) => c != null && realBookingIds.has(String(c))); + const riderUserId = d.rider_id ?? d.userid; + const riderName = d.rider_name ?? d.rider; + const resolved = resolveMilerForOrder(d, lookup); + if (bookingId == null || !resolved) { + const reason = + bookingId == null + ? `no candidate id matched a real booking (tried bookingid=${d.bookingid}, orderheaderid=${d.orderheaderid}, deliveryid=${d.deliveryid}, orderid=${d.orderid})` + : `no miler found for rider id ${riderUserId} / name "${riderName}" (checked userid, milerprofileid, name)`; + logger.error(`finalCreatedeliveries: skipping order — ${reason}`); + throw new Error(`order ${d.orderid ?? bookingId ?? '?'}: ${reason}`); + } + const { rider, matchedVia } = resolved; + logger.debug(`finalCreatedeliveries: order booking ${bookingId} -> rider ${riderUserId} ("${riderName}") matched via ${matchedVia}`); + try { + // doormile-flow.md (confirmed current, authoritative): assign-miler's + // body key is `mileruserid`, and it's the miler's userid — a + // DIFFERENT identity space than milerprofileid, which is what admin + // miler endpoints (notify, block, etc.) key on instead. Sending + // { milerid: rider.milerprofileid } was wrong on both the key name + // and the value's identity space — the actual root cause of the + // persistent 404s on this call. + await assignMilerToBooking(bookingId, { mileruserid: rider.userid }); + // Return the REAL resolved milerprofileid, not the solver's own + // rider_id/userid — the caller (Preview.js) needs this for + // notifyRider, which takes a milerprofileid specifically. Before + // this, Preview.js notified using the raw solver id directly, which + // is neither a real userid nor a milerprofileid (see the matching + // comment above) — so rider push notifications were going out with + // a bogus id and very likely silently failing server-side. + return { milerprofileid: rider.milerprofileid }; + } catch (err) { + logger.error( + `finalCreatedeliveries: assign-miler failed for booking ${bookingId}`, + err.response?.status, + err.response?.data || err.message + ); + throw err; + } + }) + ); + + const failed = results.filter((r) => r.status === 'rejected'); + if (failed.length === deliveries.length) { + // Total failure — throw so the caller's mutation onError fires instead + // of a false "success" toast + navigate-away with nothing assigned. + throw new Error(`Couldn't assign any of the ${deliveries.length} order(s)`); + } + if (failed.length) { + OpenToast(`${failed.length} of ${deliveries.length} order(s) couldn't be assigned — check Orders/Deliveries`, 'warning', 4000); + } + const resolvedMilerProfileIds = [...new Set(results.filter((r) => r.status === 'fulfilled').map((r) => r.value.milerprofileid))]; + return { success: true, assigned: deliveries.length - failed.length, failed: failed.length, resolvedMilerProfileIds }; +}; +// ==============================|| createAutomationDeliveries (orders) Auto rider Assign ||============================== // +// Also part of the optimiser pipeline (routes.workolik.com / routemate.workolik.com) — untouched. +export const createAutomationDeliveries = async (variables) => { + const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : []; + const url = + variables.selectedMode.value == 1 + ? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}` + : `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`; + const body = + variables.selectedMode.value == 1 + ? { deliveries: variables.deliveries, absent_riders: absentRiders } + : { ...(variables.data || {}), absent_riders: absentRiders }; + const response = await axios.post(url, body); + return response.data; +}; + +// ==============================|| notifyRider (orders / deliveries) ||============================== // +// New API notifies by milerprofileid + title/message (server looks up the +// device token itself) instead of the caller passing an FCM token directly. +export const notifyRider = async ( + milerProfileId, + title = 'DoormileXpress', + message = 'Orders have been placed for delivery. Kindly accept and process deliveries.' +) => { + if (!milerProfileId) { + throw new Error('Invalid miler profile id'); + } + return notifyMiler(milerProfileId, title, message); +}; + +// ==============================|| fetchDeliveries (deliveries) ||============================== // +// GET /admin/consignments has zero documented response schema (see +// express-console-api.md — it's listed with no example body at all, unlike +// bookings). Rather than bind the deliveries table to an unknown shape, this +// builds the list from /admin/bookings (fully confirmed schema — see +// getBookings/fetchOrders in this same file) filtered to bookings that have +// actually been dispatched (assignedmileruserid or consignmentid set) — the +// Deliveries page is conceptually "orders that have moved past just being +// created", same underlying resource as the Orders page. +// +// Real consignment-only data (route step order, transit minutes, cumulative +// km, live delivery-leg status distinct from the booking's own status) has no +// confirmed field names and is NOT fabricated here — those cells render a +// "—" placeholder in the UI instead of invented numbers. If GET +// /admin/consignments' real shape is confirmed later, this is the function to +// extend with a join by booking.consignmentid. +// +// Each row is normalised onto the *old* jupiter field names the Deliveries +// page already reads (orderheaderid, deliverycustomer, ridername, etc.) so +// deliveries.js itself doesn't need a full rewrite — same compatibility-shim +// approach as fetchAllRiders' `{details}` wrapper. +export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { + // Every caller builds the key as + // [name, appId, userid, status, startdate, enddate, rowsPerPage, …] + // (Dispatch.js, deliveries.js, reports/ordersDetails.js, + // reports/profitability.js — all four verified). Only rowsPerPage used to be + // read here, which meant startdate/enddate were accepted and then ignored: + // changing the date on the Dispatch page rebuilt the query key and refetched, + // but issued an identical request, so the row set — and every batch count + // derived from it — was byte-identical for today, yesterday and last week. + // + // GET /admin/bookings takes only pageno/pagesize (doormileApi.js; no date + // parameter is documented in express-console-api.md and guessing one risks a + // silent 400 or, worse, a silently-ignored filter), so the range is applied + // client-side below, after the rows are normalised. + const [, , , , startdate, enddate, rowsPerPage] = queryKey; + // Unlike the 3 joins below (customers/milers/tenants — each individually + // guarded so a failed join just degrades a display field, not the whole + // page), a failed bookings call is the one thing this function can't + // recover from — the previous version let it reject the whole Promise.all + // uncaught, with no toast anywhere across the 4 pages that call this. + let bookings; + try { + bookings = await getBookings(pageParam, rowsPerPage); + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load deliveries', 'error', 2000); + return { rows: [], nextPage: undefined }; + } + // Consignments are joined for ONE reason: once a booking becomes a + // consignment, its lifecycle continues on the CONSIGNMENT record and the + // booking's own `status` stops moving. `PUT /admin/consignments/:id/status` + // — the Update Status dialog — writes there, so the write succeeded, the + // toast said so, and this page went on showing the booking's stale + // `Converted_To_Consignment` because that is the only field it read. + // + // Guarded like the other three joins: if the call fails, or the response + // carries no recognisable status, the row falls back to the booking's status + // and behaviour is exactly what it was before this join existed. + const [customers, milers, tenants, consignments] = await Promise.all([ + getAdminCustomers().catch(() => []), + getMilers().catch(() => []), + getAdminTenants().catch(() => []), + getConsignments().catch(() => []) + ]); + // The id field on a consignment record has never been captured, so both + // plausible names are indexed rather than guessing one. + const consignmentMap = new Map(); + (consignments || []).forEach((c) => { + const id = c?.consignmentid ?? c?.id; + if (id != null) consignmentMap.set(String(id), c); + }); + const customerMap = new Map((customers || []).map((c) => [c.appcustomerid ?? c.customerid ?? c.id, c])); + const milerMap = new Map((milers || []).map((m) => [m.userid ?? m.milerid, m])); + const tenantMap = new Map((tenants || []).map((t) => [t.tenantid, t])); + + const dispatched = (bookings || []).filter((b) => b.assignedmileruserid || b.consignmentid); + + const rows = dispatched.map((b) => { + const customer = customerMap.get(b.appcustomerid); + const miler = milerMap.get(b.assignedmileruserid); + const tenant = tenantMap.get(b.tenantid); + const charge = b.serviceoptions?.[0]?.estimatedprice; + return { + orderheaderid: b.bookingid, + deliveryid: b.bookingid, + orderid: b.bookingno || `#${b.bookingid}`, + consignmentid: b.consignmentid, + tenantid: b.tenantid, + tenantname: tenant?.tenantname || '', + tenantsuburb: '', + applocation: '', + tenantadress: tenant?.primaryemail || '', + locationname: tenant?.tenantname || '', + locationsuburb: '', + // Was hardcoded '' — Dispatch.js's kitchen markers read this as the + // pickup business name (`o.pickupcustomer || o.kitchen_key || 'Unknown'`, + // Dispatch.js:1660), and no booking on this API carries a `kitchen_key` + // field at all, so every kitchen pin fell through to the literal string + // 'Unknown' — rendered as a "U" marker whose hover/popup then showed + // "Unknown". The tenant IS the kitchen for a B2B booking (same value + // already used for tenantname/locationname above), so reuse it here. + pickupcustomer: tenant?.tenantname || '', + pickupcontactno: '', + Pickupaddress: b.pickupaddress || '', + pickupaddress: b.pickupaddress || '', + pickuplocation: b.pickupaddress || '', + pickupsuburb: '', + deliverycustomer: customer?.firstname || customer?.name || (b.appcustomerid ? `Customer #${b.appcustomerid}` : ''), + deliverycontactno: customer?.phone || customer?.contactno || '', + deliveryaddress: b.deliveryaddress || '', + deliverylocation: b.deliveryaddress || '', + deliverysuburb: '', + ridername: miler?.displayname || (b.assignedmileruserid ? `Rider #${b.assignedmileruserid}` : ''), + userid: b.assignedmileruserid, + // GET /admin/milers/:id/notify (and block/assign-vehicle) key off + // milerprofileid, not the userid stored on the booking — confirmed + // live (a booking's assignedmileruserid matches a miler's `userid` + // field, which 404s against /admin/milers/:id; milerprofileid is the + // real primary key of that resource). + milerprofileid: miler?.milerprofileid, + ridercontact: miler?.phone || '', + expecteddeliverytime: b.serviceoptions?.[0]?.estimateddeliveryat, + // No route-plan data source (step order/transit time/cumulative km were + // computed by the old jupiter backend from the dispatch optimiser's + // output, not stored on a booking/consignment) — left undefined so the + // UI's own "—" fallbacks render instead of a fabricated number. + transitminutes: undefined, + cumulativekms: undefined, + step: undefined, + // No road-distance field on a booking — approximated as a straight + // line between pickup and delivery coordinates. + kms: haversineKm(b.pickuplatitude, b.pickuplongitude, b.deliverylatitude, b.deliverylongitude), + pickuplatitude: b.pickuplatitude, + pickuplongitude: b.pickuplongitude, + deliverylatitude: b.deliverylatitude, + deliverylongitude: b.deliverylongitude, + deliverycharges: charge, + deliveryamt: charge, + deliveryamount: charge, + Quantity: b.parcels?.length || 0, + quantity: b.parcels?.length || 0, + collectionamt: undefined, + notes: b.notes || '', + deliverytype: customer ? 'B' : 'C', + orderdate: b.createdat, + deliverydate: b.serviceoptions?.[0]?.estimateddeliveryat || b.updatedat, + // ⚠ NOT a real assignment time. The Doormile bookings feed has no + // assignment timestamp (the true one lives on `bookingassignments`, + // reachable only per-booking via GET /admin/bookings/:id/track), so this + // is the booking's last-modified column. It moves every time ANYTHING + // touches the row — status change, parcel scan, payment, pickup-complete. + // + // **Never bucket or group by this field.** Dispatch.js and deliveries.js + // used to bucket their Morning/Afternoon/Evening batches on it, which + // meant an order re-stamped during the evening silently jumped out of the + // batch it was actually assigned to and into whichever window contained + // the current clock time — the same orders appearing under Afternoon and + // then Evening on the same day. Both now bucket on `orderdate` (this + // row's `createdat`, immutable) instead — see dispatch/CLAUDE.md §1's + // table for why `expecteddeliverytime` was also tried and rejected (it's + // the promised delivery slot, not the wave the order was placed in). It + // remains fine to DISPLAY assigntime as a "last updated" stamp, which is + // all the reports use it for. + assigntime: b.updatedat, + // The consignment's status WINS when there is one. That is the record + // the rider app and the Update Status dialog both advance; the booking's + // status is frozen at Converted_To_Consignment from pickup onwards. + // Falls back to the booking whenever the consignment is absent or carries + // nothing status-shaped — never invents a state. + orderstatus: mapBookingStatusToDeliveryStatus(consignmentStatusFor(b, consignmentMap) ?? b.status), + // Which record the status above came from, and what it said. A booking + // freezes at Converted_To_Consignment the moment it is picked up, so a + // row can legitimately read "Active" while GET /admin/bookings still says + // Converted_To_Consignment — which looks exactly like a bug unless the UI + // can say where the value came from. + consignmentstatus: consignmentStatusFor(b, consignmentMap), + statusfromconsignment: consignmentStatusFor(b, consignmentMap) != null, + droplat: b.deliverylatitude, + droplon: b.deliverylongitude + }; + }); + + // Apply the requested date range to the booking's CREATION day. This has to + // agree with what the batch bucketing reads (Dispatch.js's + // BATCH_TIME_FIELD / deliveries.js's BATCH_TIME_KEYS, both `orderdate`) — + // filtering on one field while bucketing on another is how you get a row + // that is counted for the day but belongs to no batch in it. + // + // parseDoormileTimestamp, not bare dayjs(): some Doormile timestamps carry a + // false trailing Z, and near midnight an unstripped one shifts the row +5:30 + // into the next calendar day, dropping it from the selected date. + // + // A missing/blank bound means "unbounded on that side", which preserves the + // old behaviour for any caller that doesn't pass real dates. + // + // ⛔ An "activity" basis — also admitting a row whose `assigntime` + // (== `updatedat`) falls in the window — was tried and REVERTED. It let an + // order created yesterday evening and merely touched today onto today's + // board, but batch bucketing reads `orderdate`, so that row landed in + // Evening Batch. The live result was "Evening 6" at 10:41 in the morning on a + // day with no orders created at all. Admitting a row on one timestamp while + // bucketing it on another cannot produce an honest batch count; if + // carried-over work needs to be visible it needs its own bucket, not a + // time-of-day wave it does not belong to. + const inRange = (row) => { + if (!startdate && !enddate) return true; + const t = parseDoormileTimestamp(row.orderdate); + if (!t.isValid()) return false; + const day = t.format('YYYY-MM-DD'); + if (startdate && day < String(startdate)) return false; + if (enddate && day > String(enddate)) return false; + return true; + }; + + return { + rows: rows.filter(inRange), + // Whether to fetch another page must be based on the RAW bookings page + // (bookings.length), not the post-filter `rows.length` — most bookings on + // any given page are still pending, not dispatched, so the filtered count + // almost never equals rowsPerPage. Comparing the filtered count against + // rowsPerPage (the previous logic) made pagination stop after page 1 in + // virtually every real dataset, silently hiding dispatched/assigned + // orders that live beyond the first `rowsPerPage` bookings — e.g. a + // booking just created and assigned wouldn't show on the Deliveries page + // at all once the tenant has more than one page's worth of bookings. + nextPage: (bookings || []).length === Number(rowsPerPage) ? pageParam + 1 : undefined + }; +}; + +// Backs fetchCountAPI so the chip counts are derived from the exact same +// source (and status mapping) as fetchDeliveries' rows — counting against +// the separately-guessed /admin/dashboard shape produced numbers that didn't +// match what the table actually showed. +const getDeliveryStatusCounts = async () => { + try { + const bookings = (await getBookings(1, 1000)) || []; + const dispatched = bookings.filter((b) => b.assignedmileruserid || b.consignmentid); + const counts = { total: dispatched.length }; + dispatched.forEach((b) => { + const status = mapBookingStatusToDeliveryStatus(b.status); + counts[status] = (counts[status] || 0) + 1; + }); + return counts; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load delivery counts', 'error', 2000); + return {}; + } +}; + +// ==============================|| fetchCountAPI (deliveries) ||============================== // + +export const fetchCountAPI = async () => { + const data = await getDeliveryStatusCounts(); + return { + total: data.total || 0, + uncoveredLength: data.pending || 0, + assignedLength: data.accepted || 0, + arrivedLength: data.arrived || 0, + pickedLength: data.picked || 0, + activeLength: data.active || 0, + coveredLength: data.delivered || 0, + cancelLength: data.cancelled || 0, + skippedLength: data.skipped || 0 + }; +}; + +// ==============================|| cancelDeliveryAPI (deliveries) ||============================== // + +// jupiter2doormile.md §4 confirms cancel is its own booking-level action — +// POST /admin/bookings/:id/cancel — not a consignment status PUT. cancelFeed +// isn't part of the documented cancel body; sent as a best-effort extra field +// rather than silently dropped, since it's unconfirmed whether the server +// persists it. +export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => + cancelBooking(selectedRow.orderheaderid ?? selectedRow.deliveryid, { reason: cancelFeed }); + +// ==============================|| getorderdetails (deliveries) ||============================== // +export const getorderdetails = async (orderHeaderid) => getBooking(orderHeaderid); + +// ==============================|| changeRiderAPI (deliveries) ||============================== // +// doormile-flow.md (confirmed current, authoritative) settles this: the body +// is { "mileruserid": } — the previous guess here (`milerid` +// key, `milerprofileid` value) was wrong on both counts. Admin miler +// endpoints (notify, block, etc.) key on milerprofileid; assign-miler is the +// one exception that wants userid instead — "different identity spaces on +// adjacent endpoints," per that doc's own wording. selectedRider comes +// straight from getMilers(), which carries both fields on the same object. +export const changeRiderAPI = async (selectedRider, selectedRow) => + assignMilerToBooking(selectedRow.orderheaderid ?? selectedRow.deliveryid, { mileruserid: selectedRider.userid }); + +// ==============================|| updateDeliveryAPI (deliveries) ||============================== // +// No amount/notes field exists on PUT /admin/consignments/:id/status — closest +// available write is a status update. Free-text amount/notes edits have no home +// in the new API yet. +// Target endpoint is consignment-scoped (/admin/consignments/:id/status), so +// this needs the real consignmentid, not a booking id. deliveryid on a +// deliveries-page row is always b.bookingid (see fetchDeliveries) — always +// truthy, so `deliveryid ?? consignmentid` never actually fell through to +// consignmentid even when it was present, silently calling the endpoint +// with the wrong kind of id on every Update Status submit. +// +// ---- The body --------------------------------------------------------------- +// +// This used to forward the dialog's WHOLE state object as the request body — +// the old jupiter shape (`orderstatus`, `deliveryid`, `orderheaderid`, +// `deliveryamt`, `cumulativekms`, `userid`). The endpoint wants one field +// called `status`, so every submit came back: +// +// PUT /admin/consignments/40/status → 400 {"status is required"} +// +// The status was in the payload the whole time, under the wrong name. +// +// The VALUE has to be translated too. The dialog's options are this page's own +// lifecycle keys (`delivered`, `cancelled`, …); the API speaks the booking enum +// (`Delivered`, `Cancelled`, …). Sending `delivered` where `Delivered` is +// expected is the same class of bug one layer down. +// +// Reverse of BOOKING_STATUS_TO_DELIVERY_STATUS, and deliberately NOT derived +// from it by inversion: that map is many-to-one (`pending_pickup` and +// `miler_assigned` both mean `pending`), so an automatic inversion would pick +// whichever happened to be last and silently write the wrong one. +// ⚠ These are CONSIGNMENT statuses, not booking statuses. The endpoint is +// `PUT /admin/consignments/:id/status`, and the two enums are not the same +// vocabulary even though they overlap on Delivered and Cancelled. +// +// This map used to include the booking's pre-pickup states, and they were all +// nonsense to send here: +// +// picked → 'Converted_To_Consignment' — that names the MOMENT a booking +// becomes a consignment. A consignment cannot be set to it; it +// already is one. Choosing "Picked" in the dialog therefore did +// nothing and the row kept showing whatever the consignment +// really was, which is what "I set it to picked and it still says +// Active" was. +// pending → 'Pending_Pickup' +// accepted → 'Pickup_Scheduled' — both describe a booking BEFORE +// pickup. A consignment only exists after it, so it can never go +// back to either. +// +// What a consignment can actually be set to, per doormile-flow.md §5–6: it is +// created at pickup-complete already Out_for_Delivery (hyperlocal) or routed +// via a hub, then delivered, skipped, or cancelled. +const DELIVERY_STATUS_TO_CONSIGNMENT_STATUS = { + // The dialog offers "started"; a consignment that has started IS out for + // delivery — this API has no separate state for it. + started: 'Out_for_Delivery', + active: 'Out_for_Delivery', + delivered: 'Delivered', + cancelled: 'Cancelled', + canceled: 'Cancelled' +}; + +// Why a status can't be set, when it can't. Specific beats generic: "Picked +// can't be set" is useless next to "it's already a consignment, which is what +// picked means". +const UNSETTABLE_REASON = { + picked: 'this order is already a consignment — which is exactly what "picked" means. There is no earlier state to set it back to.', + pending: + 'a consignment only exists after pickup, so it can’t go back to Pending — that describes a booking before the parcel was collected.', + accepted: + 'a consignment only exists after pickup, so it can’t go back to Accepted — that describes a booking before the parcel was collected.', + arrived: 'the rider action behind Arrived (POST /miler/bookings/:id/reached) writes no consignment status, so there is nothing to set.', + skipped: + 'Skipped is recorded by the rider (POST /miler/consignments/:id/skip) as an attempt count, not as a status this endpoint can write.' +}; + +export const updateDeliveryAPI = async (orderData) => { + const id = orderData.consignmentid ?? orderData.deliveryid; + const chosen = String(orderData.orderstatus || '').toLowerCase(); + const status = DELIVERY_STATUS_TO_CONSIGNMENT_STATUS[chosen]; + + // `arrived` and `skipped` have no booking-status equivalent at all (the rider + // actions behind them — /miler/bookings/:id/reached and + // /miler/consignments/:id/skip — write no booking status). Refusing here with + // the reason is honest; guessing a near-enough status would set the wrong one + // on a real delivery. + if (!status) { + if (!chosen) return { success: false, message: 'Choose a status first.' }; + return { + success: false, + message: + UNSETTABLE_REASON[chosen] || `"${orderData.orderstatus}" has no equivalent on the consignment API, so it can't be set from here.` + }; + } + + // Only `status` is sent. The dialog's kms / amount / notes have no field on + // this endpoint (see the note above), and this request 400s on validation — + // so posting the rest is at best ignored and at worst another rejection. + return updateConsignmentStatus(id, { status }); +}; + +// ==============================|| getalltenants (tenants) ||============================== // + +export const getalltenants = async () => { + try { + return (await getAdminTenants()) || []; + } catch (err) { + const message = err.response?.data?.message || err.message || 'Something went wrong'; + OpenToast(message); + return null; + } +}; +// ==============================|| getallpricing (clientPricing) ||============================== // +// This is the client-facing "Pricing" page (tenant pricing rules, GET +// /admin/pricing) — not GET /admin/doormile-pricing, which is Doormile's own +// internal banding with no tenant concept. Previously pointed at +// getDoormilePricing(), so the page's "Tenant"/"Tenants Priced" columns never +// had real data behind them. +export const getallpricing = async () => { + try { + return (await getAdminPricing()) || []; + } catch (err) { + const message = err.response?.data?.message || err.message || 'Something went wrong'; + OpenToast(message); + return []; + } +}; + +// ==============================|| fetchAllRiders (riders) ||============================== // +export const fetchAllRiders = async () => { + try { + const milers = await getMilers(); + return { details: milers || [], nextPage: undefined }; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000); + return { details: [], nextPage: undefined }; + } +}; +// ==============================|| getallridersummary (riders) ||============================== // +export const getallridersummary = async () => { + try { + const milers = (await getMilers()) || []; + // A miler has no `status` field — availabilitystatus is the real one + // (confirmed live: Available/Assigned/On_Pickup/Offline/Blocked). + // "Active" here means not Offline and not Blocked, matching riders.js's + // own tab filter. `available`/`onDelivery` are a KPI-card-friendly + // breakdown of that same "active" bucket — Available is free/idle, + // On Delivery covers both Assigned (accepted, not yet picked up) and + // On_Pickup (en route) — computed separately so an unrecognised status + // value still falls into "active" (unchanged behaviour) without also + // silently inflating one of the two new buckets. + const active = milers.filter((m) => !['offline', 'blocked'].includes(String(m.availabilitystatus || '').toLowerCase())).length; + const available = milers.filter((m) => String(m.availabilitystatus || '').toLowerCase() === 'available').length; + const onDelivery = milers.filter((m) => ['assigned', 'on_pickup'].includes(String(m.availabilitystatus || '').toLowerCase())).length; + return { total: milers.length, active, inactive: milers.length - active, available, onDelivery }; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load rider summary', 'error', 2000); + return { total: 0, active: 0, inactive: 0, available: 0, onDelivery: 0 }; + } +}; + +// ==============================|| getreportsummary (orders summary)||============================== // +// appId (queryKey[0]) was previously discarded here — the LocationAutocomplete +// zone picker on the Orders Summary page changed the header text but never +// the actual request. GET /admin/reports does accept a hubid param (see +// getReports in doormileApi.js) — hubs are this API's zone-equivalent (see +// LocationAutocomplete's own comment) — so pass appId through as that. +export const getreportsummary = async ({ queryKey }) => { + const [appId, tenantid, locationid, startdate, enddate] = queryKey; + try { + return (await getReports(startdate, enddate, tenantid, locationid, appId || undefined)) || {}; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load the report summary', 'error', 2000); + return {}; + } +}; + +// ==============================|| getreportlocationsummary (orders summary)||============================== // +// Confirmed against jupiter2doormile.md (Status: Done) as the replacement for +// jupiter's getlocationsummary — GET /admin/locations/summary. +export const getreportlocationsummary = async ({ queryKey }) => { + const [, tenantid, locationid, startdate, enddate] = queryKey; + try { + return (await getLocationsSummary(tenantid, locationid, startdate, enddate)) || []; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load the location summary', 'error', 2000); + return []; + } +}; + +// ==============================|| fetchRidersSummary (riders summary)||============================== // +// No per-rider performance summary endpoint in the new API. +// Confirmed against jupiter2doormile.md (Status: Done) as the replacement +// for jupiter's getridersummary — GET /admin/milers/summary. Per-rider row +// field names aren't documented anywhere; this passes the response through +// as-is and lets the page's own rendering degrade gracefully on unknown keys. +export const fetchRidersSummary = async ({ queryKey }) => { + const [, appId, startdate, enddate] = queryKey; + try { + const data = await getMilerSummary(appId || undefined, startdate, enddate); + return Array.isArray(data) ? data : data ? [data] : []; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load the riders summary', 'error', 2000); + return []; + } +}; + +// ==============================|| fetchRidersLogs (RiderLogs)||============================== // +// No rider login/checkout audit-log endpoint in the new API. +// Was a permanent [] stub ("no rider login/checkout audit-log endpoint") — +// that was true for the jupiter audit-log concept this was originally named +// after, but jupiter2doormile.md confirms (Status: Done) that +// GET /admin/milers/summary carries live position (currentlatitude/ +// currentlongitude/lastpingat per that doc) for every rider at a zone. This +// feeds Dispatch.js's live map pins and "All Active Routes" view, which were +// both silently dead with the old stub. +// +// GET /admin/milers/summary confirmed live: { milerprofileid, userid, +// displayname, phone, availabilitystatus, defaultvehicletype, hubid, +// hubname, rating, onduty, dutystartedat, currentlatitude, currentlongitude, +// lastlocationupdatedat, lastpingat, assigned, accepted, rejected, +// completed, cancelled, delivered, riderkms, ridercharges } — no `status`, +// `contactno`, or current-order-id field exists at all. `id` is exposed as +// an alias of milerprofileid, which is what every /admin/milers/:id/* route +// (notify, block, assign-vehicle, logs) actually keys off — not userid. +export const fetchRidersLogs = async ({ queryKey } = {}) => { + const [appId] = queryKey || []; + try { + const data = await getMilerSummary(appId || undefined); + const riders = Array.isArray(data) ? data : data?.riders || data?.milers || []; + return riders.map((r) => ({ + id: r.milerprofileid, + milerprofileid: r.milerprofileid, + userid: r.userid, + username: r.displayname, + status: r.availabilitystatus, + contactno: r.phone, + orderid: undefined, + logdate: r.lastpingat ?? r.lastlocationupdatedat, + latitude: r.currentlatitude, + longitude: r.currentlongitude + })); + } catch { + // Polled every 1s by Dispatch.js's live map (RIDER_LOG_POLL_MS) — a toast + // here would fire once a second on any outage. Fail silently; the map + // just stops updating rider pins until the next successful poll. + return []; + } +}; + +// ==============================|| getusers (viewProfile)||============================== // + +export const getusers = async () => { + try { + return (await getProfile()) || {}; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load profile', 'error', 2000); + return {}; + } +}; +// ==============================|| getallriders (order)||============================== // + +export const getallriders = async () => { + try { + return (await getMilers()) || []; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000); + return []; + } +}; diff --git a/src/api/seed.js b/src/api/seed.js deleted file mode 100644 index bb155ce..0000000 --- a/src/api/seed.js +++ /dev/null @@ -1,1928 +0,0 @@ -/** - * Demo dataset for the KROW Forge demo. - * - * Shapes follow the entity schemas the app was designed against, and the - * numbers are chosen so the derived dashboard figures come out to the - * reference values: 22 applicants, 5 open positions, 9 AI-scored candidates - * averaging 76, 3 hires at an average quality of 94, and a 41% standardized - * decision rate. - */ - -import { SHIFT_RECORDS } from './attendanceSeed'; - -const iso = (date) => new Date(`${date}T09:00:00.000Z`).toISOString(); - -/* ── Reference data ────────────────────────────────────────────────────── */ - -const ROLE_CATEGORIES = [ - 'Bartender', 'Server', 'Security', 'Chef', 'Picker', 'Host', 'Barback', 'Line Cook', 'Event Lead', -].map((name, i) => ({ - id: `role_${i + 1}`, - name, - created_date: iso('2026-06-01'), -})); - -const CERTIFICATIONS = [ - 'ServSafe', 'TIPS Certified', 'RBS Alcohol Server', 'Guard Card', 'CPR / First Aid', - 'Food Handler Card', 'Forklift Operator', 'OSHA 10', -].map((name, i) => ({ - id: `cert_${i + 1}`, - name, - created_date: iso('2026-06-01'), -})); - -/* ── Positions ─────────────────────────────────────────────────────────── */ - -// created_date descending drives list order on Positions and Overview. -const JOB_POSTINGS = [ - { - id: 'job_senior_server', - company: 'Fairmont San Jose', - custom_requirements: 'Fine dining or banquet service at senior level — recover a table before the client notices there was anything to recover.', - leadership_expectations: 'Set the pace for the servers on your floor', - attendance_expectations: 'Available evenings and weekends', - title: 'Senior Server – Fine Dining', - role_category: 'Server', - description: - 'An advanced service role for seated fine dining events in San Jose. You carry the standard for your section, recover a table before a client notices there was anything to recover, and set the pace for the servers around you.', - responsibilities: [ - 'Run a section at fine dining standard without supervision', - 'Recover service issues before they reach the client', - 'Set timing and pace for the servers around you', - 'Handle allergens and special requests accurately', - 'Support the captain through arrivals, toasts and close', - ], - qualifications: [ - '4+ years of fine dining or banquet service', - 'Verified advanced service capability', - 'Comfortable as the senior server on a floor', - 'Available evenings and weekends', - ], - nice_to_haves: ['Wine service knowledge', 'Captain experience', 'Bilingual'], - min_experience_years: 4, - english_required: 'fluent', - certifications_required: ['Food Handler Card'], - /* What the work actually requires, in verified skill levels. This is what - the matching engine reads — see lib/skillGraph.js. Weights say which - requirement carries the role; they are normalised, so they need not total - 100 to produce a meaningful percentage. */ - skill_requirements: [ - { skill_id: 'server', level: 'advanced', weight: 40 }, - { skill_id: 'customer_service', level: 'intermediate', weight: 25 }, - { skill_id: 'food_safety', level: 'beginner', weight: 20 }, - { skill_id: 'leadership', level: 'beginner', weight: 15 }, - ], - pay_range_min: 24, - pay_range_max: 34, - location: 'San Jose, CA', - status: 'active', - ai_generated: false, - created_date: iso('2026-08-08'), - }, - { - id: 'job_bartender', - company: 'Bay Event Staffing', - custom_requirements: 'Own your bar from setup to breakdown at high-volume private events.', - physical_requirements: 'Comfortable standing for a full shift', - title: 'Bartender', - role_category: 'Bartender', - skill_requirements: [ - { skill_id: 'bartending', level: 'intermediate', weight: 60 }, - { skill_id: 'customer_service', level: 'beginner', weight: 40 }, - ], - description: - 'We are building a bench of dependable bartenders for high-volume private events across the region. You will own your bar from setup to breakdown, pour with speed and consistency, and keep guests feeling looked after even when the room is three deep.', - responsibilities: [ - 'Set up and break down a full service bar', - 'Pour classic cocktails to spec at event pace', - 'Verify guest age and refuse service responsibly', - 'Track inventory and flag low stock before service', - 'Keep the bar clean and compliant throughout the shift', - ], - qualifications: [ - 'Prior bartending experience at events or in a busy venue', - 'Working knowledge of classic cocktail specs', - 'Comfortable standing for a full shift', - 'Valid alcohol service certification', - ], - nice_to_haves: ['Craft cocktail background', 'Bilingual', 'Own bar kit'], - min_experience_years: 2, - english_required: 'conversational', - certifications_required: ['TIPS Certified'], - pay_range_min: 0, - pay_range_max: 0, - location: '', - status: 'active', - ai_generated: true, - created_date: iso('2026-07-30'), - }, - { - id: 'job_server_fine', - company: 'Valley Catering Co.', - custom_requirements: 'Tray service and synchronized coursing experience for seated events.', - attendance_expectations: 'Available evenings and weekends', - skill_requirements: [ - { skill_id: 'server', level: 'advanced', weight: 45 }, - { skill_id: 'customer_service', level: 'intermediate', weight: 30 }, - { skill_id: 'food_safety', level: 'beginner', weight: 25 }, - ], - title: 'Event Server – Fine Dining', - role_category: 'Server', - description: - 'Fine dining service for seated corporate dinners and weddings in Silicon Valley. You will run synchronized courses, read a table without hovering, and hold a standard of polish that clients notice and rebook for.', - responsibilities: [ - 'Execute synchronized coursed service for seated events', - 'Polish and set glassware, china, and flatware', - 'Describe menus and handle allergen questions accurately', - 'Clear and reset tables between courses discreetly', - 'Support captains during guest arrivals and toasts', - ], - qualifications: [ - '2+ years of banquet or fine dining service', - 'Tray service and synchronized coursing experience', - 'Professional appearance and clear communication', - 'Available evenings and weekends', - ], - nice_to_haves: ['Wine service knowledge', 'Captain experience', 'Bilingual'], - min_experience_years: 2, - english_required: 'fluent', - certifications_required: ['Food Handler Card'], - pay_range_min: 18, - pay_range_max: 26, - location: 'Silicon Valley, CA', - status: 'active', - ai_generated: true, - created_date: iso('2026-07-29'), - }, - { - id: 'job_security', - company: 'Legendary Event Staff', - custom_requirements: 'Valid California Guard Card on file before the first shift.', - physical_requirements: 'Able to stand and patrol for extended periods', - title: 'Event Security Officer', - role_category: 'Security', - description: - 'Licensed event security for concerts, corporate activations, and private functions across Los Angeles. Presence over confrontation: you set the tone at the door and de-escalate long before anything becomes an incident.', - responsibilities: [ - 'Staff entry points and verify credentials', - 'Conduct bag checks per venue policy', - 'De-escalate guest conflicts calmly', - 'Document incidents accurately and promptly', - 'Coordinate with venue staff and local authorities', - ], - qualifications: [ - 'Valid California Guard Card', - '3+ years of event or venue security experience', - 'Clear incident reporting skills', - 'Able to stand and patrol for extended periods', - ], - nice_to_haves: ['Crowd management training', 'CPR certified', 'Bilingual'], - min_experience_years: 3, - english_required: 'conversational', - certifications_required: ['Guard Card', 'CPR / First Aid'], - pay_range_min: 20, - pay_range_max: 28, - location: 'Los Angeles, CA', - status: 'paused', - ai_generated: false, - created_date: iso('2026-07-28'), - }, - { - id: 'job_chef', - company: 'Valley Catering Co.', - custom_requirements: 'Offsite and high-volume catering experience, with food cost ownership.', - leadership_expectations: 'Lead and schedule a rotating kitchen brigade', - skill_requirements: [ - { skill_id: 'food_safety', level: 'advanced', weight: 40 }, - { skill_id: 'leadership', level: 'advanced', weight: 35 }, - { skill_id: 'customer_service', level: 'beginner', weight: 25 }, - ], - title: 'Executive Chef – Catering', - role_category: 'Chef', - description: - 'Lead the kitchen for a high-end catering operation running multiple events a week. You will design menus, cost them honestly, and run a brigade that holds quality at 400 covers the same way it does at 40.', - responsibilities: [ - 'Design and cost seasonal event menus', - 'Lead and schedule a rotating kitchen brigade', - 'Own food safety compliance across offsite kitchens', - 'Manage vendor relationships and food cost targets', - 'Execute tastings and client menu consultations', - ], - qualifications: [ - '8+ years of professional kitchen experience', - '3+ years leading a brigade at executive or sous level', - 'ServSafe certification', - 'Offsite and high-volume catering experience', - ], - nice_to_haves: ['Culinary degree', 'Plated dinner specialization', 'Bilingual kitchen leadership'], - min_experience_years: 8, - english_required: 'fluent', - certifications_required: ['ServSafe'], - pay_range_min: 45, - pay_range_max: 70, - location: 'Bay Area, CA', - status: 'active', - ai_generated: true, - created_date: iso('2026-07-27'), - }, - { - id: 'job_bartender_corp', - company: 'Bay Event Staffing', - custom_requirements: 'Corporate or executive event experience, with batched cocktail programs.', - leadership_expectations: 'Run a bar independently at corporate functions', - skill_requirements: [ - { skill_id: 'bartending', level: 'advanced', weight: 50 }, - { skill_id: 'customer_service', level: 'intermediate', weight: 30 }, - { skill_id: 'leadership', level: 'beginner', weight: 20 }, - ], - title: 'Experienced Bartender – Corporate Events', - role_category: 'Bartender', - description: - 'Corporate hospitality bartending for tech campuses and executive functions in the Bay Area. Batch cocktails, clean presentation, and the judgment to run a bar unsupervised in front of an executive audience.', - responsibilities: [ - 'Build and execute batched cocktail programs', - 'Run a bar independently at corporate functions', - 'Maintain responsible service standards', - 'Present a clean, branded bar throughout service', - 'Reconcile consumption counts after service', - ], - qualifications: [ - '5+ years of bartending experience', - 'Corporate or executive event experience', - 'RBS or equivalent alcohol service certification', - 'Reliable transportation across the Bay Area', - ], - nice_to_haves: ['Zero-proof program experience', 'Barista skills', 'Own bar kit'], - min_experience_years: 5, - english_required: 'fluent', - certifications_required: ['RBS Alcohol Server'], - pay_range_min: 22, - pay_range_max: 32, - location: 'Bay Area, CA', - status: 'active', - ai_generated: true, - created_date: iso('2026-07-26'), - }, - { - id: 'job_picker', - company: 'Oakland Event Logistics', - custom_requirements: 'Accuracy first — every mispick becomes a missing item at an event.', - physical_requirements: 'Able to lift 50 lbs repeatedly', - attendance_expectations: 'Reliable attendance for early shifts', - title: 'Picker', - role_category: 'Picker', - description: - 'Warehouse order picking for an event logistics hub in Oakland. Accuracy first, speed second: every mispick becomes a missing item at someone else’s wedding.', - responsibilities: [ - 'Pick and stage orders against pick lists', - 'Scan and verify SKUs for accuracy', - 'Stage outbound pallets for event trucks', - 'Report damaged inventory immediately', - 'Keep aisles and staging lanes clear', - ], - qualifications: [ - 'Warehouse or fulfillment experience', - 'Able to lift 50 lbs repeatedly', - 'Comfortable with handheld scanners', - 'Reliable attendance for early shifts', - ], - nice_to_haves: ['Forklift certified', 'Inventory system experience'], - min_experience_years: 1, - english_required: 'basic', - certifications_required: ['Forklift Operator'], - pay_range_min: 40, - pay_range_max: 45, - location: 'Oakland', - status: 'closed', - ai_generated: false, - created_date: iso('2026-07-20'), - }, - { - id: 'job_banquet', - company: 'Fairmont San Jose', - custom_requirements: 'Hotel banquet background and BEO fluency.', - leadership_expectations: 'Direct a floor team you have often just met that afternoon', - attendance_expectations: 'Available for early mornings and late nights', - skill_requirements: [ - { skill_id: 'server', level: 'advanced', weight: 35 }, - { skill_id: 'leadership', level: 'intermediate', weight: 35 }, - { skill_id: 'customer_service', level: 'advanced', weight: 30 }, - ], - title: 'Banquet Captain – Hotel Events', - role_category: 'Server', - description: - 'Captain banquet floors for hotel conferences and galas in San Jose. You are the bridge between the client, the kitchen, and a floor team you have often just met that afternoon.', - responsibilities: [ - 'Lead pre-shift briefings and assign sections', - 'Coordinate timing between kitchen and floor', - 'Be the client contact throughout the event', - 'Resolve service issues without escalation', - 'Close out the floor and complete event reports', - ], - qualifications: [ - '4+ years of banquet service, 1+ in a lead role', - 'Proven ability to direct a floor team', - 'Strong client-facing communication', - 'Available for early mornings and late nights', - ], - nice_to_haves: ['Hotel banquet background', 'BEO fluency', 'Bilingual'], - min_experience_years: 4, - english_required: 'fluent', - certifications_required: ['Food Handler Card'], - pay_range_min: 24, - pay_range_max: 34, - location: 'San Jose, CA', - status: 'active', - ai_generated: true, - created_date: iso('2026-07-18'), - }, - - /* ── The multi-company demand the allocation engine exists for ────────── - Three companies wanting the same skill at the same time, with different - urgency, start dates and run lengths. Total demand is 90 chefs against a - workforce that cannot cover it, which is the situation priority ordering - has to resolve — and the reason it is seeded rather than described. */ -]; - -/* ── Applications ──────────────────────────────────────────────────────── */ - -const breakdown = (over) => ({ - experience: 0, english: 0, reliability: 0, certifications: 0, availability: 0, - personality: 0, culture_fit: 0, communication_style: 0, attendance_expectations: 0, - physical_requirements: 0, leadership_expectations: 0, job_related_answers: 0, - verified_skills: 0, scenario_judgment: 0, employer_requirements: 0, - ...over, -}); - -/** An applicant who has not been screened yet — every AI field stays empty. */ -const unscreened = (over) => ({ - status: 'applied', - ai_score: 0, - certifications: [], - availability: [], - skills: [], - companies_worked: [], - client_rating: 0, - created_date: iso('2026-08-04'), - updated_date: iso('2026-08-04'), - ...over, -}); - -const JOB_APPLICATIONS = [ - /* Senior Server – Fine Dining. Two applications with no AI screening on them - at all: what the recruiter sees for these is the skill match, computed from - verified training rather than from a model's reading of a résumé. */ - { - id: 'app_arun', - applicant_name: 'Arun Kumar', - email: 'arun.kumar@example.com', - phone: '+1-408-555-0210', - years_experience: 4, - english_level: 'fluent', - certifications: ['Food Handler Card'], - availability: ['Weekdays', 'Weekends', 'Evenings'], - skills: ['Coursed service', 'Guest recovery', 'Allergen handling'], - companies_worked: ['Valley Catering Co.'], - client_rating: 4.7, - professional_summary: - 'Four years of event service in San Jose, working towards the senior server role on a fine dining floor.', - selfie_url: 'https://i.pravatar.cc/240?img=68', - job_posting_id: 'job_senior_server', - job_title: 'Senior Server – Fine Dining', - status: 'applied', - ai_score: 0, - created_date: iso('2026-08-13'), - updated_date: iso('2026-08-13'), - }, - { - id: 'app_priya_nair', - applicant_name: 'Priya Nair', - email: 'priya.nair@example.com', - phone: '+1-408-555-0211', - years_experience: 2, - english_level: 'fluent', - certifications: ['Food Handler Card'], - availability: ['Weekends', 'Evenings'], - skills: ['Table service', 'Guest relations'], - companies_worked: ['Bay Event Staffing'], - client_rating: 4.5, - professional_summary: 'Two years of event service, moving towards fine dining sections.', - selfie_url: 'https://i.pravatar.cc/240?img=47', - job_posting_id: 'job_senior_server', - job_title: 'Senior Server – Fine Dining', - status: 'applied', - ai_score: 0, - created_date: iso('2026-08-12'), - updated_date: iso('2026-08-12'), - }, - - /* Executive Chef – Catering: 2 applicants · 1 screened · 1 hired */ - { - id: 'app_antoine', - applicant_name: 'Chef Antoine Dubois', - email: 'antoine.dubois@email.com', - phone: '+1-415-555-0101', - years_experience: 12, - english_level: 'fluent', - certifications: ['ServSafe', 'CPR / First Aid'], - availability: ['Weekdays', 'Weekends', 'Evenings'], - skills: ['Menu design', 'Brigade leadership', 'Food costing', 'Plated dinners', 'Offsite catering'], - companies_worked: [], - client_rating: 4.9, - professional_summary: - 'Executive chef with 12 years across hotel banquets and high-end offsite catering. Has run brigades of 20+ and holds plate standards at 400 covers.', - selfie_url: 'https://i.pravatar.cc/240?img=45', - job_posting_id: 'job_chef', - job_title: 'Executive Chef – Catering', - status: 'hired', - ai_score: 97, - ai_match_label: 'Excellent Match', - ai_summary: - 'Exceptional fit. Twelve years of executive kitchen leadership with direct offsite catering experience and current ServSafe certification. Availability covers the full event calendar.', - ai_strengths: [ - '12 years executive chef experience', - 'ServSafe certified', - 'Proven brigade leadership at high volume', - 'Full-calendar availability', - ], - ai_gaps: ['No formal culinary degree on file'], - ai_recommendation: 'Shortlisted', - score_breakdown: breakdown({ - experience: 99, english: 95, reliability: 96, certifications: 98, availability: 94, - personality: 91, culture_fit: 93, communication_style: 92, attendance_expectations: 95, - physical_requirements: 90, leadership_expectations: 97, job_related_answers: 96, - verified_skills: 95, scenario_judgment: 94, employer_requirements: 98, - }), - created_date: iso('2026-07-24'), - updated_date: iso('2026-07-25'), - }, - unscreened({ - id: 'app_priya', - applicant_name: 'Priya Raman', - email: 'priya.raman@email.com', - phone: '+1-415-555-0110', - years_experience: 6, - english_level: 'fluent', - professional_summary: 'Sous chef moving toward executive roles in catering.', - job_posting_id: 'job_chef', - job_title: 'Executive Chef – Catering', - }), - - /* Event Security Officer: 6 applicants · 2 screened · 1 hired */ - { - id: 'app_marcus', - applicant_name: 'Marcus Williams', - email: 'marcus.w@email.com', - phone: '+1-415-555-0102', - years_experience: 5, - english_level: 'fluent', - certifications: ['Guard Card', 'CPR / First Aid'], - availability: ['Weekends', 'Evenings', 'Overnight'], - skills: ['Crowd management', 'De-escalation', 'Incident reporting', 'Access control'], - companies_worked: [], - client_rating: 4.8, - professional_summary: - 'Licensed event security officer with five years across concerts and corporate activations. Known for defusing situations before they escalate.', - selfie_url: 'https://i.pravatar.cc/240?img=12', - job_posting_id: 'job_security', - job_title: 'Event Security Officer', - status: 'hired', - ai_score: 94, - ai_match_label: 'Excellent Match', - ai_summary: - 'Strong fit. Current Guard Card and CPR certification with five years of directly relevant event security work and overnight availability.', - ai_strengths: ['Current Guard Card', 'Five years event security', 'Documented de-escalation record'], - ai_gaps: ['No formal crowd management course on file'], - ai_recommendation: 'Shortlisted', - score_breakdown: breakdown({ - experience: 92, english: 93, reliability: 95, certifications: 97, availability: 96, - personality: 90, culture_fit: 91, communication_style: 89, attendance_expectations: 96, - physical_requirements: 95, leadership_expectations: 86, job_related_answers: 93, - verified_skills: 92, scenario_judgment: 95, employer_requirements: 94, - }), - created_date: iso('2026-07-24'), - updated_date: iso('2026-07-25'), - }, - { - id: 'app_dana', - applicant_name: 'Dana Whitfield', - email: 'dana.whitfield@email.com', - phone: '+1-415-555-0111', - years_experience: 4, - english_level: 'fluent', - certifications: ['Guard Card'], - availability: ['Weekends', 'Evenings'], - skills: ['Access control', 'Incident reporting'], - companies_worked: [], - client_rating: 0, - professional_summary: 'Venue security officer transitioning into private event work.', - job_posting_id: 'job_security', - job_title: 'Event Security Officer', - status: 'interview', - ai_score: 0, - interview_id: 'int_dana', - created_date: iso('2026-08-01'), - updated_date: iso('2026-08-03'), - }, - unscreened({ - id: 'app_omar', applicant_name: 'Omar Haddad', email: 'omar.haddad@email.com', - phone: '+1-415-555-0112', years_experience: 2, english_level: 'conversational', - job_posting_id: 'job_security', job_title: 'Event Security Officer', - }), - unscreened({ - id: 'app_kevin', applicant_name: 'Kevin Boyle', email: 'kevin.boyle@email.com', - phone: '+1-415-555-0113', years_experience: 7, english_level: 'native', - job_posting_id: 'job_security', job_title: 'Event Security Officer', - }), - unscreened({ - id: 'app_rosa', applicant_name: 'Rosa Delgado', email: 'rosa.delgado@email.com', - phone: '+1-415-555-0114', years_experience: 3, english_level: 'fluent', - job_posting_id: 'job_security', job_title: 'Event Security Officer', - }), - unscreened({ - id: 'app_tyler', applicant_name: 'Tyler Nunez', email: 'tyler.nunez@email.com', - phone: '+1-415-555-0115', years_experience: 1, english_level: 'conversational', - job_posting_id: 'job_security', job_title: 'Event Security Officer', - }), - - /* Experienced Bartender – Corporate Events: 1 applicant · 1 screened · 1 hired */ - { - id: 'app_marco', - applicant_name: 'Marco Rivera', - email: 'marco.rivera@email.com', - phone: '+1-415-555-0103', - years_experience: 6, - english_level: 'fluent', - certifications: ['RBS Alcohol Server', 'TIPS Certified'], - availability: ['Weekdays', 'Evenings'], - skills: ['Batch cocktails', 'Craft cocktails', 'Bar setup', 'Inventory control'], - companies_worked: [], - client_rating: 4.7, - professional_summary: - 'Corporate event bartender with six years on Bay Area tech campuses. Builds batched programs that hold quality at volume.', - selfie_url: 'https://i.pravatar.cc/240?img=33', - job_posting_id: 'job_bartender_corp', - job_title: 'Experienced Bartender – Corporate Events', - status: 'hired', - ai_score: 92, - ai_match_label: 'Excellent Match', - ai_summary: - 'Excellent fit for corporate hospitality. Six years of directly comparable work, both required certifications current, and reliable Bay Area coverage.', - ai_strengths: ['Six years corporate bartending', 'RBS and TIPS certified', 'Batch cocktail program experience'], - ai_gaps: ['Limited zero-proof program experience'], - ai_recommendation: 'Shortlisted', - score_breakdown: breakdown({ - experience: 94, english: 92, reliability: 93, certifications: 96, availability: 88, - personality: 90, culture_fit: 92, communication_style: 91, attendance_expectations: 92, - physical_requirements: 89, leadership_expectations: 82, job_related_answers: 94, - verified_skills: 93, scenario_judgment: 90, employer_requirements: 93, - }), - created_date: iso('2026-07-24'), - updated_date: iso('2026-07-25'), - }, - - /* Event Server – Fine Dining: 3 applicants · 3 screened · 0 hired */ - { - id: 'app_sofia', - applicant_name: 'Sofia Mendez', - email: 'sofia.mendez@email.com', - phone: '+1-415-555-0104', - years_experience: 4, - english_level: 'fluent', - certifications: ['Food Handler Card'], - availability: ['Weekends', 'Evenings'], - skills: ['Synchronized service', 'Tray service', 'Wine service', 'Guest relations'], - companies_worked: [], - client_rating: 4.6, - professional_summary: - 'Fine dining event server with four years of coursed banquet service and wine knowledge.', - selfie_url: 'https://i.pravatar.cc/240?img=47', - job_posting_id: 'job_server_fine', - job_title: 'Event Server – Fine Dining', - status: 'ai_screened', - ai_score: 89, - ai_match_label: 'Excellent Match', - ai_summary: - 'Strong fine dining fit. Four years of coursed service with wine knowledge that exceeds the posting, and weekend availability aligned to the event calendar.', - ai_strengths: ['Four years fine dining service', 'Wine service knowledge', 'Strong guest relations'], - ai_gaps: ['No captain-level experience yet'], - ai_recommendation: 'Interview', - score_breakdown: breakdown({ - experience: 88, english: 92, reliability: 90, certifications: 85, availability: 87, - personality: 91, culture_fit: 90, communication_style: 93, attendance_expectations: 89, - physical_requirements: 86, leadership_expectations: 74, job_related_answers: 90, - verified_skills: 88, scenario_judgment: 87, employer_requirements: 89, - }), - created_date: iso('2026-08-02'), - updated_date: iso('2026-08-05'), - }, - { - id: 'app_tanya', - applicant_name: 'Tanya Cruz', - email: 'tanya.cruz@email.com', - phone: '+1-415-555-0105', - years_experience: 3, - english_level: 'fluent', - certifications: ['Food Handler Card'], - availability: ['Weekends'], - skills: ['Banquet service', 'Tray service', 'Table resets'], - companies_worked: [], - client_rating: 4.4, - professional_summary: 'Banquet server with three years of seated dinner service.', - selfie_url: 'https://i.pravatar.cc/240?img=44', - job_posting_id: 'job_server_fine', - job_title: 'Event Server – Fine Dining', - status: 'interview', - ai_score: 83, - ai_match_label: 'Excellent Match', - ai_summary: - 'Solid fit with room to grow. Three years of banquet service meets the requirement; weekend-only availability is the main constraint.', - ai_strengths: ['Three years banquet service', 'Reliable weekend availability', 'Clean service technique'], - ai_gaps: ['Weekend-only availability', 'No wine service training'], - ai_recommendation: 'Interview', - interview_id: 'int_tanya', - score_breakdown: breakdown({ - experience: 80, english: 90, reliability: 88, certifications: 82, availability: 70, - personality: 88, culture_fit: 86, communication_style: 87, attendance_expectations: 90, - physical_requirements: 85, leadership_expectations: 68, job_related_answers: 84, - verified_skills: 81, scenario_judgment: 82, employer_requirements: 83, - }), - created_date: iso('2026-08-01'), - updated_date: iso('2026-08-05'), - }, - { - id: 'app_nadia', - applicant_name: 'Nadia Petrova', - email: 'nadia.petrova@email.com', - phone: '+1-415-555-0116', - years_experience: 2, - english_level: 'conversational', - certifications: [], - availability: ['Weekends', 'Evenings'], - skills: ['Table service'], - companies_worked: [], - client_rating: 0, - professional_summary: 'Restaurant server moving into event work.', - job_posting_id: 'job_server_fine', - job_title: 'Event Server – Fine Dining', - status: 'ai_screened', - ai_score: 0, - created_date: iso('2026-08-03'), - updated_date: iso('2026-08-04'), - }, - - /* Bartender: 3 applicants · 3 screened · 0 hired */ - { - id: 'app_jordan', - applicant_name: 'Jordan Blake', - email: 'jordan.blake@email.com', - phone: '+1-415-555-0106', - years_experience: 3, - english_level: 'fluent', - certifications: ['TIPS Certified'], - availability: ['Weekends', 'Evenings'], - skills: ['Classic cocktails', 'Bar setup', 'Speed pouring'], - companies_worked: [], - client_rating: 4.2, - professional_summary: 'Event bartender with three years of high-volume weekend service.', - job_posting_id: 'job_bartender', - job_title: 'Bartender', - status: 'interview', - ai_score: 72, - ai_match_label: 'Good Match', - ai_summary: - 'Meets the core requirements. Three years of high-volume experience with current TIPS certification; cocktail range is narrower than ideal.', - ai_strengths: ['TIPS certified', 'High-volume service experience'], - ai_gaps: ['Limited classic cocktail range', 'No craft background'], - ai_recommendation: 'Interview', - interview_id: 'int_jordan', - score_breakdown: breakdown({ - experience: 74, english: 85, reliability: 78, certifications: 80, availability: 82, - personality: 76, culture_fit: 74, communication_style: 78, attendance_expectations: 80, - physical_requirements: 82, leadership_expectations: 58, job_related_answers: 70, - verified_skills: 68, scenario_judgment: 72, employer_requirements: 73, - }), - created_date: iso('2026-08-01'), - updated_date: iso('2026-08-05'), - }, - { - id: 'app_elena', - applicant_name: 'Elena Vasquez', - email: 'elena.vasquez@email.com', - phone: '+1-415-555-0107', - years_experience: 2, - english_level: 'conversational', - certifications: ['TIPS Certified'], - availability: ['Evenings'], - skills: ['Bar setup', 'Wine service'], - companies_worked: [], - client_rating: 4.0, - professional_summary: 'Barback stepping up to bartending at private events.', - job_posting_id: 'job_bartender', - job_title: 'Bartender', - status: 'interview', - ai_score: 68, - ai_match_label: 'Good Match', - ai_summary: - 'Meets the minimum bar. Two years of experience with certification in place; availability and cocktail depth are the open questions.', - ai_strengths: ['TIPS certified', 'Strong bar setup discipline'], - ai_gaps: ['Evenings-only availability', 'Limited independent bartending time'], - ai_recommendation: 'Interview', - interview_id: 'int_elena', - score_breakdown: breakdown({ - experience: 62, english: 72, reliability: 76, certifications: 80, availability: 58, - personality: 78, culture_fit: 72, communication_style: 70, attendance_expectations: 78, - physical_requirements: 80, leadership_expectations: 52, job_related_answers: 64, - verified_skills: 62, scenario_judgment: 66, employer_requirements: 68, - }), - created_date: iso('2026-08-02'), - updated_date: iso('2026-08-05'), - }, - { - id: 'app_sam', - applicant_name: 'Sam Okafor', - email: 'sam.okafor@email.com', - phone: '+1-415-555-0108', - years_experience: 0, - english_level: 'basic', - certifications: [], - availability: ['Weekends'], - skills: [], - companies_worked: [], - client_rating: 0, - professional_summary: 'Looking for a first hospitality role.', - job_posting_id: 'job_bartender', - job_title: 'Bartender', - status: 'ai_screened', - ai_score: 28, - ai_match_label: 'Not a Fit', - ai_summary: - 'Below the requirement for this posting. No bartending experience or alcohol service certification, and availability is limited to weekends.', - ai_strengths: ['Motivated and available weekends'], - ai_gaps: ['No bartending experience', 'No alcohol service certification', 'Basic English only'], - ai_recommendation: 'Reject', - score_breakdown: breakdown({ - experience: 10, english: 35, reliability: 45, certifications: 0, availability: 50, - personality: 60, culture_fit: 48, communication_style: 38, attendance_expectations: 55, - physical_requirements: 70, leadership_expectations: 20, job_related_answers: 18, - verified_skills: 12, scenario_judgment: 25, employer_requirements: 22, - }), - created_date: iso('2026-08-03'), - updated_date: iso('2026-08-04'), - }, - - /* Picker: 3 applicants · 2 screened · 0 hired */ - { - id: 'app_hector', - applicant_name: 'Hector Lima', - email: 'hector.lima@email.com', - phone: '+1-415-555-0109', - years_experience: 2, - english_level: 'conversational', - certifications: ['Forklift Operator'], - availability: ['Weekdays', 'Early mornings'], - skills: ['Order picking', 'Scanner operation', 'Pallet staging'], - companies_worked: [], - client_rating: 3.9, - professional_summary: 'Warehouse picker with forklift certification.', - job_posting_id: 'job_picker', - job_title: 'Picker', - status: 'ai_screened', - ai_score: 61, - ai_match_label: 'Good Match', - ai_summary: - 'Adequate fit. Forklift certified with two years of picking experience; accuracy history is not yet verified.', - ai_strengths: ['Forklift certified', 'Early morning availability'], - ai_gaps: ['Unverified pick accuracy', 'Limited scanner system exposure'], - ai_recommendation: 'Maybe', - score_breakdown: breakdown({ - experience: 60, english: 65, reliability: 68, certifications: 85, availability: 78, - personality: 70, culture_fit: 64, communication_style: 58, attendance_expectations: 72, - physical_requirements: 88, leadership_expectations: 40, job_related_answers: 55, - verified_skills: 52, scenario_judgment: 58, employer_requirements: 62, - }), - created_date: iso('2026-07-30'), - updated_date: iso('2026-08-01'), - }, - { - id: 'app_wei', - applicant_name: 'Wei Chen', - email: 'wei.chen@email.com', - phone: '+1-415-555-0117', - years_experience: 1, - english_level: 'basic', - certifications: [], - availability: ['Weekdays'], - skills: ['Order picking'], - companies_worked: [], - client_rating: 0, - professional_summary: 'Fulfillment associate seeking steady warehouse shifts.', - job_posting_id: 'job_picker', - job_title: 'Picker', - status: 'ai_screened', - ai_score: 0, - created_date: iso('2026-07-31'), - updated_date: iso('2026-08-01'), - }, - unscreened({ - id: 'app_grace', applicant_name: 'Grace Mwangi', email: 'grace.mwangi@email.com', - phone: '+1-415-555-0118', years_experience: 3, english_level: 'fluent', - job_posting_id: 'job_picker', job_title: 'Picker', - }), - - /* Banquet Captain – Hotel Events: 4 applicants · 2 screened · 0 hired */ - { - id: 'app_ana', - applicant_name: 'Ana Ruiz', - email: 'ana.ruiz@email.com', - phone: '+1-415-555-0119', - years_experience: 5, - english_level: 'fluent', - certifications: ['Food Handler Card'], - availability: ['Weekdays', 'Weekends'], - skills: ['Banquet leadership', 'BEO reading', 'Floor coordination'], - companies_worked: [], - client_rating: 0, - professional_summary: 'Banquet server with lead-shift experience in hotel events.', - job_posting_id: 'job_banquet', - job_title: 'Banquet Captain – Hotel Events', - status: 'ai_screened', - ai_score: 0, - created_date: iso('2026-08-02'), - updated_date: iso('2026-08-03'), - }, - { - id: 'app_devon', - applicant_name: 'Devon Carter', - email: 'devon.carter@email.com', - phone: '+1-415-555-0120', - years_experience: 6, - english_level: 'native', - certifications: ['Food Handler Card'], - availability: ['Weekdays', 'Weekends', 'Evenings'], - skills: ['Floor leadership', 'Client relations', 'Event reporting'], - companies_worked: [], - client_rating: 0, - professional_summary: 'Hotel banquet captain with six years directing event floors.', - job_posting_id: 'job_banquet', - job_title: 'Banquet Captain – Hotel Events', - status: 'interview', - ai_score: 0, - interview_id: 'int_devon', - created_date: iso('2026-08-01'), - updated_date: iso('2026-08-04'), - }, - unscreened({ - id: 'app_lucas', applicant_name: 'Lucas Moreau', email: 'lucas.moreau@email.com', - phone: '+1-415-555-0121', years_experience: 4, english_level: 'fluent', - job_posting_id: 'job_banquet', job_title: 'Banquet Captain – Hotel Events', - }), - unscreened({ - id: 'app_fatima', applicant_name: 'Fatima Nasser', email: 'fatima.nasser@email.com', - phone: '+1-415-555-0122', years_experience: 2, english_level: 'fluent', - job_posting_id: 'job_banquet', job_title: 'Banquet Captain – Hotel Events', - }), - -]; - -/* ── AI interviews ─────────────────────────────────────────────────────── */ - -const transcript = (candidate, role) => [ - { role: 'assistant', content: `Hi ${candidate.split(' ')[0]} — thanks for making time. Tell me about the busiest ${role} shift you've worked and how you handled it.`, timestamp: iso('2026-08-04') }, - { role: 'user', content: 'We were short two people on a 300-guest event. I re-sectioned the floor, moved the strongest server to the head tables, and pulled the rest into a runner rotation so nothing sat under the lamps.', timestamp: iso('2026-08-04'), response_time_seconds: 11 }, - { role: 'assistant', content: 'What broke first, and what did you do the moment you noticed?', timestamp: iso('2026-08-04') }, - { role: 'user', content: 'Dessert timing. I saw the pass backing up, so I pulled coffee service forward by five minutes to buy the kitchen room instead of letting plates die.', timestamp: iso('2026-08-04'), response_time_seconds: 9 }, -]; - -const AI_INTERVIEWS = [ - { - id: 'int_tanya', - application_id: 'app_tanya', - job_posting_id: 'job_server_fine', - job_title: 'Event Server – Fine Dining', - candidate_name: 'Tanya Cruz', - messages: transcript('Tanya Cruz', 'banquet'), - overall_interview_score: 72, - verdict: 'maybe', - hire_recommendation: 'Worth a trial shift on a coursed dinner before committing to the season.', - integrity_score: 100, - ai_flags: [], - category_scores: { - communication: 78, confidence: 71, experience_relevance: 74, culture_fit: 76, - problem_solving: 69, personality: 77, communication_style: 76, - attendance_expectations: 82, reliability: 80, physical_requirements: 84, - leadership_expectations: 62, scenario_judgment: 70, job_related_answers: 73, - verified_skills: 68, - }, - strengths: ['Clear, calm delivery under pressure', 'Reprioritized service timing sensibly'], - concerns: ['Weekend-only availability', 'Limited wine service vocabulary'], - best_fit_roles: ['Event Server', 'Banquet Server'], - summary: - 'Composed and specific about a real understaffed service. Judgment is sound; depth on fine dining vocabulary is still developing.', - reasoning: - 'Answers referenced concrete decisions rather than generalities, which supports the reliability signal. Scores held back by narrow availability and thin wine knowledge.', - created_date: iso('2026-08-05'), - }, - { - id: 'int_jordan', - application_id: 'app_jordan', - job_posting_id: 'job_bartender', - job_title: 'Bartender', - candidate_name: 'Jordan Blake', - messages: transcript('Jordan Blake', 'bar'), - overall_interview_score: 0, - verdict: 'maybe', - hire_recommendation: '', - integrity_score: 100, - ai_flags: [], - category_scores: {}, - strengths: [], - concerns: [], - best_fit_roles: [], - summary: '', - reasoning: '', - created_date: iso('2026-08-05'), - }, - { - id: 'int_elena', - application_id: 'app_elena', - job_posting_id: 'job_bartender', - job_title: 'Bartender', - candidate_name: 'Elena Vasquez', - messages: transcript('Elena Vasquez', 'bar'), - overall_interview_score: 0, - verdict: 'maybe', - hire_recommendation: '', - integrity_score: 100, - ai_flags: [], - category_scores: {}, - strengths: [], - concerns: [], - best_fit_roles: [], - summary: '', - reasoning: '', - created_date: iso('2026-08-05'), - }, - { - id: 'int_dana', - application_id: 'app_dana', - job_posting_id: 'job_security', - job_title: 'Event Security Officer', - candidate_name: 'Dana Whitfield', - messages: transcript('Dana Whitfield', 'door'), - overall_interview_score: 0, - verdict: 'maybe', - hire_recommendation: '', - integrity_score: 100, - ai_flags: [], - category_scores: {}, - strengths: [], - concerns: [], - best_fit_roles: [], - summary: '', - reasoning: '', - created_date: iso('2026-08-03'), - }, -]; - -/* ── Staff (hires) ─────────────────────────────────────────────────────── */ - -// created_date descending is the order Hired History renders. -const STAFF = [ - { - id: 'staff_marco', - name: 'Marco Rivera', - email: 'marco.rivera@email.com', - phone: '+1-415-555-0103', - role: 'Experienced Bartender – Corporate Events', - profile_tier: 'Skilled', - hire_date: '2026-07-25', - application_id: 'app_marco', - job_posting_id: 'job_bartender_corp', - ai_score: 92, - status: 'onboarding', - client_rating: 0, - endorsed_skills: [], - created_date: iso('2026-07-25'), - }, - { - id: 'staff_marcus', - name: 'Marcus Williams', - email: 'marcus.w@email.com', - phone: '+1-415-555-0102', - role: 'Event Security Officer', - profile_tier: 'Skilled', - hire_date: '2026-07-25', - application_id: 'app_marcus', - job_posting_id: 'job_security', - ai_score: 94, - status: 'onboarding', - client_rating: 0, - endorsed_skills: [], - created_date: iso('2026-07-25'), - }, - { - id: 'staff_antoine', - name: 'Chef Antoine Dubois', - email: 'antoine.dubois@email.com', - phone: '+1-415-555-0101', - role: 'Executive Chef – Catering', - profile_tier: 'Skilled', - hire_date: '2026-07-25', - application_id: 'app_antoine', - job_posting_id: 'job_chef', - ai_score: 97, - status: 'onboarding', - client_rating: 0, - endorsed_skills: [], - created_date: iso('2026-07-25'), - }, -]; - -/* ── Courses, badges, learning paths ───────────────────────────────────── */ - -const COURSES = [ - { - id: 'course_service_basics', - title: 'Fine Dining Service Standards', - description: - 'Coursed service, tray discipline, and the small signals that separate a polished floor from a busy one.', - category: 'Service', - difficulty: 'beginner', - xp: 120, - badge_reward: 'Service Fundamentals', - estimated_minutes: 18, - proof_skill: 'Coursed Table Service', - /* The four skill-graph fields on a record that predates them. Nothing else - about these originals changes — the ladder simply now knows where they - sit, which is what lets completing one move a worker's level. */ - skill_id: 'server', - target_level: 'advanced', - required_level: 'intermediate', - completion_criteria: ['Work through the training material', 'Submit evidence for the challenge'], - verification_criteria: ['Acknowledges without excuses', 'Offers a concrete remedy', 'Protects the table experience'], - challenge: { - type: 'roleplay', - prompt: - 'A guest at a seated dinner says their entrée is cold, and the kitchen is nine minutes behind. Handle it out loud, start to finish.', - ai_persona: 'A polite but disappointed guest at a wedding head table.', - rubric: [ - { criterion: 'Acknowledges without excuses', weight: 30 }, - { criterion: 'Offers a concrete remedy', weight: 40 }, - { criterion: 'Protects the table experience', weight: 30 }, - ], - }, - unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] }, - quiz: [ - { - question: 'A guest flags an allergy mid-service. What happens first?', - options: ['Guess from the menu', 'Stop the plate and check with the kitchen', 'Serve it anyway', 'Ask another server'], - correct_index: 1, - }, - { - question: 'When do you clear a course?', - options: ['As each guest finishes', 'When the whole table has finished', 'Before dessert only', 'At the captain’s call'], - correct_index: 1, - }, - ], - pass_score: 70, - status: 'active', - created_date: iso('2026-06-10'), - }, - { - id: 'course_responsible_service', - title: 'Responsible Alcohol Service', - description: 'Reading intoxication, refusing service without a scene, and documenting what happened.', - category: 'Bar', - difficulty: 'intermediate', - xp: 160, - badge_reward: 'Responsible Service', - estimated_minutes: 22, - proof_skill: 'Responsible Service Judgment', - skill_id: 'bartending', - target_level: 'intermediate', - required_level: 'beginner', - completion_criteria: ['Work through the training material', 'Submit evidence for the challenge'], - verification_criteria: ['Refuses clearly and kindly', 'Offers an alternative', 'Escalates appropriately'], - challenge: { - type: 'roleplay', - prompt: 'A guest who has clearly had enough asks for one more round, and their colleagues are watching. Talk it through.', - ai_persona: 'A friendly but insistent guest at a corporate holiday party.', - rubric: [ - { criterion: 'Refuses clearly and kindly', weight: 40 }, - { criterion: 'Offers an alternative', weight: 30 }, - { criterion: 'Escalates appropriately', weight: 30 }, - ], - }, - unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] }, - quiz: [ - { - question: 'A guest shows clear signs of intoxication. What do you do?', - options: ['Serve a weaker drink', 'Refuse service and notify your lead', 'Ignore it', 'Ask a coworker to serve them'], - correct_index: 1, - }, - ], - pass_score: 75, - status: 'active', - created_date: iso('2026-06-12'), - }, - { - id: 'course_kitchen_safety', - title: 'Kitchen Safety & Hazard Spotting', - description: 'Find the hazard before it finds you — temperature, cross-contamination, and traffic flow.', - category: 'Kitchen', - difficulty: 'beginner', - xp: 100, - badge_reward: 'Safety Aware', - estimated_minutes: 14, - proof_skill: 'Hazard Identification', - skill_id: 'food_safety', - target_level: 'beginner', - required_level: null, - completion_criteria: ['Work through the training material', 'Submit evidence for the challenge'], - verification_criteria: ['Identifies temperature violations', 'Spots cross-contamination risk', 'Notes obstruction hazards'], - challenge: { - type: 'photo_identify', - prompt: 'Mark every food-safety hazard you can find in this prep station.', - image_url: '', - rubric: [ - { criterion: 'Identifies temperature violations', weight: 40 }, - { criterion: 'Spots cross-contamination risk', weight: 40 }, - { criterion: 'Notes obstruction hazards', weight: 20 }, - ], - }, - unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] }, - quiz: [ - { - question: 'What is the danger zone for cold-held food?', - options: ['Below 32°F', '41°F to 135°F', 'Above 165°F', 'Any temperature'], - correct_index: 1, - }, - ], - pass_score: 70, - status: 'active', - created_date: iso('2026-06-14'), - }, - { - id: 'course_leadership', - title: 'Leading a Floor Team', - description: 'Pre-shift briefings, section assignments, and correcting a teammate without deflating them.', - category: 'Leadership', - difficulty: 'advanced', - xp: 240, - badge_reward: 'Floor Leader', - estimated_minutes: 30, - proof_skill: 'Team Leadership', - skill_id: 'leadership', - target_level: 'advanced', - required_level: 'intermediate', - completion_criteria: ['Work through the training material', 'Submit evidence for the challenge'], - verification_criteria: ['Assigns clear ownership', 'Communicates timing', 'Sets a tone people follow'], - challenge: { - type: 'video', - prompt: 'Record the pre-shift briefing you would give a floor team of eight before a 300-guest gala.', - rubric: [ - { criterion: 'Assigns clear ownership', weight: 35 }, - { criterion: 'Communicates timing', weight: 35 }, - { criterion: 'Sets a tone people follow', weight: 30 }, - ], - }, - unlock_requirements: { min_shifts: 5, min_reliability: 70, required_badges: ['Service Fundamentals'] }, - quiz: [], - pass_score: 80, - status: 'active', - created_date: iso('2026-06-16'), - }, -]; - -/* ── Training modules (the skill ladders) ──────────────────────────────── */ - -/** - * The training behind every level of every skill. - * - * Written as a table rather than 36 hand-built records: each row is one module, - * and the shape it expands to is the same Course entity the four skills above - * use, so the challenge page, the Forge library and Owliver's evaluation all - * read these without knowing they were generated. - * - * Columns: skill · level · title · minutes · proof type · description · - * what Owliver checks. `required_level` is the rung below, which is what makes - * the ladder a ladder rather than a menu. - */ -const MODULE_TABLE = [ - /* Server — the full four-rung ladder. */ - ['server', 'beginner', 'Server Fundamentals', 18, 'roleplay', 'Sequence of service, tray discipline, and the first ninety seconds at a table.', ['Greets and sets expectations', 'Follows the service sequence', 'Handles the tray safely']], - ['server', 'beginner', 'Workplace Safety', 14, 'photo_identify', 'Spot the hazard before it becomes an incident — spills, traffic, hot pass.', ['Identifies floor hazards', 'Spots obstruction risks', 'Knows when to stop service']], - ['server', 'beginner', 'Basic Guest Service', 16, 'roleplay', 'Reading a table, timing an approach, and answering what you actually know.', ['Reads the table before approaching', 'Answers accurately', 'Closes the interaction cleanly']], - - ['server', 'intermediate', 'Advanced Guest Service', 20, 'roleplay', 'Difficult tables, competing demands, and keeping a section moving anyway.', ['Prioritises across tables', 'Keeps guests informed', 'Recovers timing without panic']], - ['server', 'intermediate', 'Table Management', 18, 'roleplay', 'Running a section: turns, holds, and the order you touch tables in.', ['Sequences the section', 'Manages holds and turns', 'Communicates with the pass']], - ['server', 'intermediate', 'Order Accuracy', 15, 'roleplay', 'Modifiers, allergens, and repeating back what you are about to send.', ['Confirms modifiers', 'Flags allergens correctly', 'Verifies before firing']], - - ['server', 'advanced', 'Guest Recovery', 22, 'roleplay', 'Turning a failed experience around without giving away the room.', ['Acknowledges without excuses', 'Offers a concrete remedy', 'Protects the table experience']], - ['server', 'advanced', 'High-Volume Service Operations', 25, 'video', 'Three hundred covers, one pass, and a plan that survives contact.', ['Plans the run', 'Holds the standard at volume', 'Adjusts without dropping tables']], - - ['server', 'expert', 'Floor Leadership', 30, 'video', 'Owning the floor: assignments, pace, and the call nobody else will make.', ['Assigns clear ownership', 'Sets and holds the pace', 'Makes the decision on time']], - ['server', 'expert', 'Service Coaching', 25, 'roleplay', 'Correcting a teammate mid-service without deflating them.', ['Names the behaviour, not the person', 'Gives an actionable correction', 'Confirms understanding']], - ['server', 'expert', 'Operations Excellence', 28, 'roleplay', 'Reading a BEO, staffing to it, and defending the standard to a client.', ['Reads the event brief accurately', 'Staffs to the requirement', 'Holds the standard with the client']], - - /* Bartending. */ - ['bartending', 'beginner', 'Bar Fundamentals', 16, 'roleplay', 'Station setup, glassware, and pouring to spec under a clock.', ['Sets up a working station', 'Pours to spec', 'Keeps the bar clean during service']], - ['bartending', 'beginner', 'Responsible Service Basics', 14, 'roleplay', 'Checking age, reading a guest, and knowing where the line is.', ['Verifies age correctly', 'Reads intoxication signs', 'Knows the escalation path']], - - ['bartending', 'intermediate', 'Cocktail Specs & Speed', 20, 'video', 'Classic specs from memory, built at event pace without losing accuracy.', ['Builds to spec', 'Maintains pace', 'Recovers from a mistake cleanly']], - - ['bartending', 'advanced', 'High-Volume Bar Operations', 24, 'video', 'Running a bar three deep: batching, sequencing, and restock timing.', ['Batches sensibly', 'Sequences the rail', 'Restocks before it hurts']], - ['bartending', 'advanced', 'Guest Judgment Behind the Bar', 20, 'roleplay', 'The refusal, the regular, and the colleague watching you make the call.', ['Refuses clearly and kindly', 'Protects the guest and the room', 'Documents what happened']], - - ['bartending', 'expert', 'Bar Program Leadership', 28, 'roleplay', 'Menu, training, and holding a team of bartenders to one standard.', ['Sets a standard others can follow', 'Trains rather than corrects', 'Owns the program end to end']], - ['bartending', 'expert', 'Inventory & Cost Control', 24, 'roleplay', 'Par levels, variance, and explaining a number to an operator.', ['Tracks variance accurately', 'Explains the number', 'Proposes a real fix']], - - /* Leadership. */ - ['leadership', 'beginner', 'Shift Communication', 14, 'roleplay', 'Saying the necessary thing, once, so the room hears it.', ['States the message clearly', 'Confirms it landed', 'Keeps it short']], - ['leadership', 'beginner', 'Giving & Taking Direction', 16, 'roleplay', 'Taking a correction well, and giving one that can be acted on.', ['Receives correction without friction', 'Gives an actionable instruction', 'Follows up']], - - ['leadership', 'intermediate', 'Pre-Shift Briefings', 18, 'video', 'Two minutes that decide how the next six hours go.', ['Covers ownership and timing', 'Sets the tone', 'Ends with questions answered']], - ['leadership', 'intermediate', 'Handling Conflict on the Floor', 20, 'roleplay', 'Two staff, one section, and a room that must not notice.', ['De-escalates in private', 'Resolves rather than defers', 'Protects service']], - - ['leadership', 'advanced', 'Section Assignment & Coverage', 22, 'roleplay', 'Matching people to sections, and covering the gap when someone drops.', ['Assigns to strength', 'Plans for absence', 'Rebalances mid-service']], - - ['leadership', 'expert', 'Developing Your Team', 26, 'roleplay', 'Turning a good server into the person who runs the floor next year.', ['Identifies real potential', 'Gives progressive responsibility', 'Follows through over time']], - ['leadership', 'expert', 'Operations Under Pressure', 28, 'video', 'The gala that goes wrong, and the twenty minutes that decide it.', ['Triages correctly', 'Communicates under load', 'Owns the outcome']], - - /* Customer Service. */ - ['customer_service', 'beginner', 'Guest First Impressions', 12, 'roleplay', 'The greeting, the eye contact, and the tone that sets the whole visit.', ['Opens warmly', 'Sets expectations', 'Hands off cleanly']], - ['customer_service', 'beginner', 'Reading the Room', 14, 'roleplay', 'Knowing when a table wants attention and when it wants to be left alone.', ['Reads guest signals', 'Times the approach', 'Adjusts to the table']], - - ['customer_service', 'intermediate', 'Handling Complaints', 18, 'roleplay', 'Hearing the complaint behind the complaint, and fixing that one.', ['Listens before solving', 'Identifies the real issue', 'Resolves within authority']], - ['customer_service', 'intermediate', 'Service Recovery Basics', 16, 'roleplay', 'What you can offer, what you cannot, and how to escalate fast.', ['Offers a concrete remedy', 'Stays within policy', 'Escalates without stalling']], - - ['customer_service', 'advanced', 'VIP & High-Stakes Guests', 20, 'roleplay', 'The head table, the client, and the guest everyone is watching.', ['Handles scrutiny calmly', 'Anticipates needs', 'Protects discretion']], - ['customer_service', 'advanced', 'Client Relations', 22, 'roleplay', 'The conversation with the person who books the next event.', ['Represents the operation', 'Handles pushback', 'Secures the follow-up']], - - /* Food Safety. */ - ['food_safety', 'beginner', 'Personal Hygiene Standards', 10, 'photo_identify', 'Handwashing, gloves, and the habits an inspector looks for first.', ['Follows hand hygiene', 'Uses gloves correctly', 'Presents to standard']], - - ['food_safety', 'intermediate', 'Temperature Control & HACCP', 18, 'photo_identify', 'Danger zone, hold times, and the log that proves you held it.', ['Knows the danger zone', 'Records holds accurately', 'Acts on a violation']], - ['food_safety', 'intermediate', 'Allergen Management', 16, 'roleplay', 'Stopping the plate, checking the ticket, and saying it out loud.', ['Stops the plate', 'Verifies with the kitchen', 'Communicates to the guest']], - - ['food_safety', 'advanced', 'Food Safety Auditing', 22, 'photo_identify', 'Walking a kitchen the way an inspector does, and writing it up.', ['Finds the violations', 'Ranks by severity', 'Writes an actionable report']], - ['food_safety', 'advanced', 'Incident Response & Reporting', 18, 'roleplay', 'A suspected foodborne incident, in the hour it matters.', ['Isolates the product', 'Documents accurately', 'Notifies the right people']], -]; - -const SKILL_CATEGORY = { - server: 'Service', - bartending: 'Bar', - leadership: 'Leadership', - customer_service: 'Service', - food_safety: 'Kitchen', -}; - -const LEVEL_ORDER = ['beginner', 'intermediate', 'advanced', 'expert']; - -/** Level → the rung below it, which is what a module requires before it opens. */ -const requiredLevelFor = (level) => LEVEL_ORDER[LEVEL_ORDER.indexOf(level) - 1] || null; - -/** Difficulty is the existing field; level drives it so the two never disagree. */ -const DIFFICULTY_FOR = { beginner: 'beginner', intermediate: 'intermediate', advanced: 'advanced', expert: 'advanced' }; - -const XP_FOR = { beginner: 100, intermediate: 150, advanced: 220, expert: 300 }; - -const PROMPT_FOR = { - roleplay: (title) => `Work through a ${title.toLowerCase()} scenario with Owliver, start to finish.`, - video: (title) => `Record yourself demonstrating ${title.toLowerCase()} as you would on a live shift.`, - photo_identify: (title) => `Mark everything a ${title.toLowerCase()} check should catch in this photo.`, -}; - -const slug = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, ''); - -const TRAINING_MODULES = MODULE_TABLE.map( - ([skill_id, target_level, title, minutes, type, description, criteria], i) => ({ - id: `mod_${skill_id}_${slug(title)}`, - title, - description, - category: SKILL_CATEGORY[skill_id], - difficulty: DIFFICULTY_FOR[target_level], - xp: XP_FOR[target_level], - estimated_minutes: minutes, - proof_skill: title, - /* The four fields that make a course a rung on a ladder. */ - skill_id, - target_level, - required_level: requiredLevelFor(target_level), - completion_criteria: ['Work through the training material', 'Submit evidence for the challenge'], - verification_criteria: criteria, - challenge: { - type, - prompt: PROMPT_FOR[type](title), - ai_persona: type === 'roleplay' ? 'A guest or teammate in a live service situation.' : undefined, - rubric: (Array.isArray(criteria) ? criteria : []).map((criterion, n) => ({ criterion, weight: n === 0 ? 40 : 30 })), - }, - unlock_requirements: { min_shifts: 0, min_reliability: 0, required_badges: [] }, - quiz: [], - pass_score: 70, - status: 'active', - created_date: iso('2026-06-20'), - _order: i, - }) -); - -/** Every course the platform holds — the four originals plus the ladders. */ -const COURSE_LIBRARY = [...COURSES, ...TRAINING_MODULES]; - -/** A module id from its skill and title, so profiles read like the ladder does. */ -const mod = (skill, title) => `mod_${skill}_${slug(title)}`; - -/** - * Completed training on a worker profile. - * - * Titles are looked up rather than repeated, so a renamed module cannot leave a - * profile claiming a course that no longer goes by that name. - */ -const completions = (ids, date, score = 88) => - ids.map((id) => ({ - course_id: id, - title: COURSE_LIBRARY.find((c) => c.id === id)?.title || id, - score, - completed_date: iso(date), - })); - -/* The ladders, as id lists — profiles below compose them. */ -const SERVER_BEGINNER = ['Server Fundamentals', 'Workplace Safety', 'Basic Guest Service'].map((t) => mod('server', t)); -const SERVER_INTERMEDIATE = ['Advanced Guest Service', 'Table Management', 'Order Accuracy'].map((t) => mod('server', t)); -const SERVER_ADVANCED = ['course_service_basics', mod('server', 'Guest Recovery'), mod('server', 'High-Volume Service Operations')]; -const CS_BEGINNER = ['Guest First Impressions', 'Reading the Room'].map((t) => mod('customer_service', t)); -const CS_INTERMEDIATE = ['Handling Complaints', 'Service Recovery Basics'].map((t) => mod('customer_service', t)); -const CS_ADVANCED = ['VIP & High-Stakes Guests', 'Client Relations'].map((t) => mod('customer_service', t)); -const FOOD_BEGINNER = ['course_kitchen_safety', mod('food_safety', 'Personal Hygiene Standards')]; -const LEAD_BEGINNER = ['Shift Communication', 'Giving & Taking Direction'].map((t) => mod('leadership', t)); -const LEAD_INTERMEDIATE = ['Pre-Shift Briefings', 'Handling Conflict on the Floor'].map((t) => mod('leadership', t)); -const LEAD_ADVANCED = ['course_leadership', mod('leadership', 'Section Assignment & Coverage')]; - -const BADGES = [ - { id: 'badge_service', name: 'Service Fundamentals', description: 'Verified coursed table service.', level: 'bronze', requirements: 'Complete Fine Dining Service Standards', verification_status: 'verified', created_date: iso('2026-06-10') }, - { id: 'badge_responsible', name: 'Responsible Service', description: 'Verified responsible alcohol service judgment.', level: 'silver', requirements: 'Complete Responsible Alcohol Service', expiration_months: 24, verification_status: 'verified', created_date: iso('2026-06-12') }, - { id: 'badge_safety', name: 'Safety Aware', description: 'Verified hazard identification.', level: 'bronze', requirements: 'Complete Kitchen Safety & Hazard Spotting', verification_status: 'verified', created_date: iso('2026-06-14') }, - { id: 'badge_leader', name: 'Floor Leader', description: 'Verified floor team leadership.', level: 'gold', requirements: 'Complete Leading a Floor Team', verification_status: 'verified', created_date: iso('2026-06-16') }, -]; - -const LEARNING_PATHS = [ - { - id: 'path_server', - name: 'Event Server Track', - target_role: 'Event Server', - description: 'From first shift to a floor a captain can trust.', - difficulty: 'beginner', - steps: [ - { order: 1, course_id: 'course_service_basics', course_title: 'Fine Dining Service Standards' }, - { order: 2, course_id: 'course_kitchen_safety', course_title: 'Kitchen Safety & Hazard Spotting' }, - { order: 3, course_id: 'course_leadership', course_title: 'Leading a Floor Team' }, - ], - created_date: iso('2026-06-18'), - }, - { - id: 'path_bartender', - name: 'Bartender Track', - target_role: 'Bartender', - description: 'Speed, specs, and the judgment to run a bar alone.', - difficulty: 'intermediate', - steps: [ - { order: 1, course_id: 'course_responsible_service', course_title: 'Responsible Alcohol Service' }, - { order: 2, course_id: 'course_service_basics', course_title: 'Fine Dining Service Standards' }, - ], - created_date: iso('2026-06-18'), - }, -]; - -/* ── Worker profiles (Talent Pool) ─────────────────────────────────────── */ - -/** krow_score maps to the 300–850 career score: 94 → 817, 12 → 366, 0 → 300. */ -const WORKER_PROFILES = [ - { - id: 'wp_maria', - full_name: 'Maria Gonzalez', - email: 'maria.gonzalez@example.com', - phone: '+1-415-555-0201', - address: 'San Jose, CA', - selfie_url: 'https://i.pravatar.cc/240?img=31', - languages: ['English', 'Spanish'], - availability: ['Weekdays', 'Weekends', 'Evenings'], - transportation: 'Own vehicle', - certifications: ['Food Handler Card', 'ServSafe'], - experience: [ - { company: 'Legendary Event Staff', role: 'Banquet Captain', years: 4 }, - { company: 'Fairmont San Jose', role: 'Event Server', years: 3 }, - ], - experience_years: 7, - current_position: 'Banquet Captain', - desired_position: 'Event Lead', - career_goals: 'Run event operations for a hotel group within three years.', - skills: ['Floor leadership', 'Coursed service', 'Client relations', 'BEO reading', 'Scheduling'], - industries: ['Hospitality', 'Events'], - personality: 'Steady and direct. Sets expectations early and holds them without friction.', - strengths: ['Calm under pressure', 'Trains new staff well', 'Client-facing polish'], - weaknesses: ['Takes on too much rather than delegating'], - communication_style: 'Concise and warm; confirms understanding before moving on.', - salary_expectations: '$30–$36/hr', - leadership_potential: 92, - ai_interview_score: 93, - krow_score: 94, - reliability_score: 95, - profile_completion: 100, - xp: 1480, - /* Seven years on the floor, as a completed ladder: Server and Leadership to - Advanced, Customer Service to Advanced, Food Safety to Beginner. */ - completed_courses: completions( - [ - ...SERVER_BEGINNER, ...SERVER_INTERMEDIATE, ...SERVER_ADVANCED, - ...CS_BEGINNER, ...CS_INTERMEDIATE, ...CS_ADVANCED, - ...FOOD_BEGINNER, - ...LEAD_BEGINNER, ...LEAD_INTERMEDIATE, ...LEAD_ADVANCED, - ], - '2026-07-14', - 93 - ), - earned_badges: [ - { name: 'Service Fundamentals', level: 'bronze', course_id: 'course_service_basics', earned_date: iso('2026-06-22') }, - { name: 'Safety Aware', level: 'bronze', course_id: 'course_kitchen_safety', earned_date: iso('2026-07-01') }, - { name: 'Floor Leader', level: 'gold', course_id: 'course_leadership', earned_date: iso('2026-07-14') }, - ], - capabilities: [ - { skill: 'Coursed Table Service', level: 'Verified', evidence_id: 'ev_maria_service', status: 'verified', verified_date: iso('2026-06-22') }, - { skill: 'Team Leadership', level: 'Verified', evidence_id: 'ev_maria_leadership', status: 'verified', verified_date: iso('2026-07-14') }, - ], - shifts_completed: 64, - attendance_score: 98, - performance_score: 93, - client_rating: 4.9, - supervisor_rating: 4.8, - status: 'active', - created_date: iso('2026-06-20'), - }, - { - id: 'wp_iliana', - full_name: 'Iliana Ortega', - email: 'iortega@legendaryeventstaff.com', - phone: '+1-415-555-0202', - address: 'Oakland, CA', - selfie_url: 'https://i.pravatar.cc/240?img=26', - languages: ['English', 'Spanish'], - availability: ['Weekends'], - transportation: 'Public transit', - certifications: ['Food Handler Card'], - experience: [{ company: 'Legendary Event Staff', role: 'Event Server', years: 1 }], - experience_years: 1, - current_position: 'Event Server', - desired_position: 'Bartender', - career_goals: 'Move behind the bar and get certified this year.', - skills: ['Table service', 'Guest relations'], - industries: ['Hospitality'], - leadership_potential: 24, - ai_interview_score: 0, - krow_score: 12, - reliability_score: 38, - profile_completion: 62, - xp: 120, - /* One year in: the Beginner rung of Server, and one Intermediate module - started — so her card reads "Beginner, advancing" rather than a flat - label that hides the work in progress. */ - completed_courses: completions( - [...SERVER_BEGINNER, mod('server', 'Advanced Guest Service'), ...CS_BEGINNER], - '2026-07-28', - 79 - ), - earned_badges: [ - { name: 'Service Fundamentals', level: 'bronze', course_id: 'course_service_basics', earned_date: iso('2026-07-28') }, - ], - capabilities: [], - shifts_completed: 6, - attendance_score: 92, - performance_score: 0, - client_rating: 0, - supervisor_rating: 0, - status: 'active', - created_date: iso('2026-07-22'), - }, - /** - * Arun, Priya and Rahul are the same position read three ways. - * - * All three are measured against Senior Server – Fine Dining, and the only - * thing that separates them is verified training: Arun is one module short of - * qualifying, Priya is a level behind on the skill that carries the role, and - * Rahul has the ground floor but nothing above it. Nothing about their match - * is stored — completing training is what moves it. - */ - { - id: 'wp_arun', - full_name: 'Arun Kumar', - email: 'arun.kumar@example.com', - phone: '+1-408-555-0210', - address: 'San Jose, CA', - selfie_url: 'https://i.pravatar.cc/240?img=68', - languages: ['English', 'Tamil'], - availability: ['Weekdays', 'Weekends', 'Evenings'], - transportation: 'Own vehicle', - certifications: ['Food Handler Card'], - experience: [{ company: 'Valley Catering Co.', role: 'Event Server', years: 4 }], - experience_years: 4, - current_position: 'Event Server', - desired_position: 'Senior Server', - career_goals: 'Run a fine dining section as the senior server on the floor.', - skills: ['Coursed service', 'Guest recovery', 'Allergen handling'], - industries: ['Hospitality', 'Events'], - personality: 'Unflappable. Notices the table nobody else is watching.', - strengths: ['Calm at volume', 'Accurate under pressure'], - weaknesses: ['Quiet in briefings'], - communication_style: 'Measured; confirms detail before acting.', - salary_expectations: '$26–$32/hr', - leadership_potential: 68, - ai_interview_score: 86, - krow_score: 82, - reliability_score: 91, - profile_completion: 94, - xp: 1180, - /* Server through Intermediate, plus two of the three Advanced modules — - Fine Dining Service Standards is the one thing between him and Advanced, - and therefore between him and this role. */ - completed_courses: completions( - [ - ...SERVER_BEGINNER, ...SERVER_INTERMEDIATE, - mod('server', 'Guest Recovery'), mod('server', 'High-Volume Service Operations'), - ...CS_BEGINNER, ...CS_INTERMEDIATE, - ...FOOD_BEGINNER, - ...LEAD_BEGINNER, - ], - '2026-08-02', - 89 - ), - earned_badges: [], - capabilities: [], - shifts_completed: 38, - attendance_score: 96, - performance_score: 88, - client_rating: 4.7, - supervisor_rating: 4.6, - status: 'active', - created_date: iso('2026-07-05'), - }, - { - id: 'wp_priya', - full_name: 'Priya Nair', - email: 'priya.nair@example.com', - phone: '+1-408-555-0211', - address: 'Santa Clara, CA', - selfie_url: 'https://i.pravatar.cc/240?img=47', - languages: ['English', 'Hindi'], - availability: ['Weekends', 'Evenings'], - transportation: 'Own vehicle', - certifications: ['Food Handler Card'], - experience: [{ company: 'Bay Event Staffing', role: 'Event Server', years: 2 }], - experience_years: 2, - current_position: 'Event Server', - desired_position: 'Senior Server', - career_goals: 'Move up to fine dining sections.', - skills: ['Table service', 'Guest relations'], - industries: ['Hospitality'], - leadership_potential: 45, - ai_interview_score: 74, - krow_score: 68, - reliability_score: 84, - profile_completion: 80, - xp: 760, - completed_courses: completions( - [...SERVER_BEGINNER, ...SERVER_INTERMEDIATE, ...CS_BEGINNER, ...CS_INTERMEDIATE, ...FOOD_BEGINNER], - '2026-07-30', - 85 - ), - earned_badges: [], - capabilities: [], - shifts_completed: 21, - attendance_score: 93, - performance_score: 81, - client_rating: 4.5, - supervisor_rating: 4.4, - status: 'active', - created_date: iso('2026-07-12'), - }, - { - id: 'wp_rahul', - full_name: 'Rahul Menon', - email: 'rahul.menon@example.com', - phone: '+1-408-555-0212', - address: 'Milpitas, CA', - selfie_url: 'https://i.pravatar.cc/240?img=59', - languages: ['English'], - availability: ['Weekends'], - transportation: 'Public transit', - certifications: [], - experience: [], - experience_years: 0, - current_position: '', - desired_position: 'Event Server', - career_goals: 'Get on a floor and learn the work properly.', - skills: [], - industries: ['Hospitality'], - leadership_potential: 20, - ai_interview_score: 0, - krow_score: 41, - reliability_score: 72, - profile_completion: 58, - xp: 320, - completed_courses: completions( - [...SERVER_BEGINNER, ...CS_BEGINNER, ...FOOD_BEGINNER, ...LEAD_BEGINNER], - '2026-08-05', - 76 - ), - earned_badges: [], - capabilities: [], - shifts_completed: 4, - attendance_score: 100, - performance_score: 0, - client_rating: 0, - supervisor_rating: 0, - status: 'active', - created_date: iso('2026-07-28'), - }, - { - id: 'wp_aravind', - full_name: 'Aravind Kumar', - email: 'aravind@tenext.in', - phone: '+1-415-555-0203', - address: 'Fremont, CA', - selfie_url: 'https://i.pravatar.cc/240?img=15', - languages: ['English', 'Tamil', 'Hindi'], - availability: ['Weekdays', 'Evenings'], - transportation: 'Own vehicle', - certifications: [], - experience: [], - experience_years: 0, - current_position: '', - desired_position: 'Barback', - career_goals: 'Get a first hospitality shift and build from there.', - skills: [], - industries: ['Hospitality'], - leadership_potential: 0, - ai_interview_score: 0, - krow_score: 0, - reliability_score: 0, - profile_completion: 30, - xp: 0, - completed_courses: [], - earned_badges: [], - capabilities: [], - shifts_completed: 0, - attendance_score: 100, - performance_score: 0, - client_rating: 0, - supervisor_rating: 0, - status: 'active', - created_date: iso('2026-08-02'), - }, - { - id: 'wp_demo', - full_name: 'New Talent Demo', - email: 'new.demo2@example.com', - phone: '+1-415-555-0204', - address: 'San Francisco, CA', - selfie_url: 'https://i.pravatar.cc/240?img=9', - languages: ['English'], - availability: [], - transportation: '', - certifications: [], - experience: [], - experience_years: 0, - current_position: '', - desired_position: '', - career_goals: '', - skills: [], - industries: [], - leadership_potential: 0, - ai_interview_score: 0, - krow_score: 0, - reliability_score: 0, - profile_completion: 10, - xp: 0, - completed_courses: [], - earned_badges: [], - capabilities: [], - shifts_completed: 0, - attendance_score: 100, - performance_score: 0, - client_rating: 0, - supervisor_rating: 0, - status: 'active', - created_date: iso('2026-08-05'), - }, - { - id: 'wp_jamal', - full_name: 'Jamal Whitaker', - email: 'jamal.whitaker@example.com', - phone: '+1-415-555-0205', - address: 'Berkeley, CA', - selfie_url: 'https://i.pravatar.cc/240?img=52', - languages: ['English'], - availability: ['Weekends', 'Evenings'], - transportation: 'Own vehicle', - certifications: [], - experience: [], - experience_years: 0, - current_position: '', - desired_position: 'Event Server', - career_goals: 'Work weekends around school.', - skills: [], - industries: ['Events'], - leadership_potential: 0, - ai_interview_score: 0, - krow_score: 0, - reliability_score: 0, - profile_completion: 25, - xp: 0, - completed_courses: [], - earned_badges: [], - capabilities: [], - shifts_completed: 0, - attendance_score: 100, - performance_score: 0, - client_rating: 0, - supervisor_rating: 0, - status: 'active', - created_date: iso('2026-08-06'), - }, - { - id: 'wp_lin', - full_name: 'Lin Zhao', - email: 'lin.zhao@example.com', - phone: '+1-415-555-0206', - address: 'Daly City, CA', - selfie_url: 'https://i.pravatar.cc/240?img=20', - languages: ['English', 'Mandarin'], - availability: ['Weekdays'], - transportation: 'Public transit', - certifications: [], - experience: [], - experience_years: 0, - current_position: '', - desired_position: 'Picker', - career_goals: 'Steady weekday warehouse work.', - skills: [], - industries: ['Logistics'], - leadership_potential: 0, - ai_interview_score: 0, - krow_score: 0, - reliability_score: 0, - profile_completion: 20, - xp: 0, - completed_courses: [], - earned_badges: [], - capabilities: [], - shifts_completed: 0, - attendance_score: 100, - performance_score: 0, - client_rating: 0, - supervisor_rating: 0, - status: 'active', - created_date: iso('2026-08-06'), - }, -]; - -/* ── Evidence (Proving Ground) ─────────────────────────────────────────── */ - -const EVIDENCE = [ - { - id: 'ev_maria_service', - course_id: 'course_service_basics', - course_title: 'Fine Dining Service Standards', - skill: 'Coursed Table Service', - worker_email: 'maria.gonzalez@example.com', - worker_name: 'Maria Gonzalez', - type: 'roleplay', - media_url: '', - transcript: - 'Acknowledged the cold entrée without excuses, offered to re-fire immediately with a comped course, and kept the rest of the table on timing.', - ai_verdict: 'verified', - ai_score: 96, - ai_rubric: { 'Acknowledges without excuses': 95, 'Offers a concrete remedy': 98, 'Protects the table experience': 94 }, - ai_feedback: 'Owned the problem in one sentence and moved straight to a remedy. Exactly the instinct this role needs.', - supervisor_verified: true, - supervisor_name: 'Luis Ortega', - verified_date: iso('2026-06-23'), - created_date: iso('2026-06-22'), - }, - { - id: 'ev_maria_leadership', - course_id: 'course_leadership', - course_title: 'Leading a Floor Team', - skill: 'Team Leadership', - worker_email: 'maria.gonzalez@example.com', - worker_name: 'Maria Gonzalez', - type: 'video', - media_url: '', - transcript: 'Pre-shift briefing for eight servers covering sections, coursing timing, and the VIP head table.', - ai_verdict: 'verified', - ai_score: 89, - ai_rubric: { 'Assigns clear ownership': 92, 'Communicates timing': 90, 'Sets a tone people follow': 85 }, - ai_feedback: 'Assignments were unambiguous and the timing was concrete. Could invite more questions at the close.', - supervisor_verified: true, - supervisor_name: 'Luis Ortega', - verified_date: iso('2026-07-15'), - created_date: iso('2026-07-14'), - }, - { - id: 'ev_iliana_service', - course_id: 'course_service_basics', - course_title: 'Fine Dining Service Standards', - skill: 'Coursed Table Service', - worker_email: 'iortega@legendaryeventstaff.com', - worker_name: 'Iliana Ortega', - type: 'roleplay', - media_url: '', - transcript: 'Apologized, offered to check with the kitchen, and returned with a timing update.', - ai_verdict: 'verified', - ai_score: 78, - ai_rubric: { 'Acknowledges without excuses': 82, 'Offers a concrete remedy': 74, 'Protects the table experience': 78 }, - ai_feedback: 'Good instinct to go straight to the kitchen. Commit to a specific remedy rather than only an update.', - supervisor_verified: false, - created_date: iso('2026-07-28'), - }, -]; - -/* ── Activity log ──────────────────────────────────────────────────────── */ - -const ACTIVITY_EVENTS = [ - ['create_position', 'demo@krow.app', 'Alex Rivera', 'employer', 'Created Banquet Captain – Hotel Events', '2026-07-18'], - ['create_position', 'demo@krow.app', 'Alex Rivera', 'employer', 'Created Executive Chef – Catering', '2026-07-27'], - ['apply_job', 'antoine.dubois@email.com', 'Chef Antoine Dubois', 'talent', 'Applied to Executive Chef – Catering', '2026-07-24'], - ['apply_job', 'marcus.w@email.com', 'Marcus Williams', 'talent', 'Applied to Event Security Officer', '2026-07-24'], - ['apply_job', 'marco.rivera@email.com', 'Marco Rivera', 'talent', 'Applied to Experienced Bartender – Corporate Events', '2026-07-24'], - ['screen_candidate', 'demo@krow.app', 'Alex Rivera', 'employer', 'AI screened 3 candidates', '2026-07-24'], - ['hire_candidate', 'demo@krow.app', 'Alex Rivera', 'employer', 'Hired Chef Antoine Dubois', '2026-07-25'], - ['hire_candidate', 'demo@krow.app', 'Alex Rivera', 'employer', 'Hired Marcus Williams', '2026-07-25'], - ['hire_candidate', 'demo@krow.app', 'Alex Rivera', 'employer', 'Hired Marco Rivera', '2026-07-25'], - ['apply_job', 'sofia.mendez@email.com', 'Sofia Mendez', 'talent', 'Applied to Event Server – Fine Dining', '2026-08-02'], - ['screen_candidate', 'demo@krow.app', 'Alex Rivera', 'employer', 'AI screened Sofia Mendez', '2026-08-05'], - ['start_interview', 'tanya.cruz@email.com', 'Tanya Cruz', 'talent', 'Completed AI interview', '2026-08-05'], - ['login', 'demo@krow.app', 'Alex Rivera', 'employer', '', '2026-08-06'], - ['signup', 'new.demo2@example.com', 'New Talent Demo', 'talent', '', '2026-08-05'], - ['login', 'maria.gonzalez@example.com', 'Maria Gonzalez', 'talent', '', '2026-08-06'], -]; - -const USER_ACTIVITY = ACTIVITY_EVENTS.map(([event_type, user_email, user_name, account_type, details, date], i) => ({ - id: `act_${i + 1}`, - event_type, - user_email, - user_name, - account_type, - details, - created_date: iso(date), -})); - -/* ── Signed-in demo user ───────────────────────────────────────────────── */ - -export const DEMO_USER = { - id: 'user_demo', - full_name: 'Alex Rivera', - email: 'demo@krow.app', - role: 'admin', - account_type: 'employer', - created_date: iso('2026-06-01'), - /* Product preferences travel with the account rather than in a store of their - own, so there is one record to persist and one thing to read. */ - preferences: { - owliverDefault: true, - compactDensity: false, - emailDigest: true, - }, -}; - -/* ── Assignments ─────────────────────────────────────────────────────────── - Who is on which position, and until when — the record that turns a hire into - workforce allocation, and the one that says who is therefore not free for - anything else. - - Empty by design. This deployment has no assignment records yet: they are - written by the assignment flow, not shipped as seed. The engine reads an - empty set correctly — every position simply reports nobody assigned — and - the UI omits the demand figures rather than inventing coverage that does - not exist. */ -const ASSIGNMENTS = []; - -/* Attendance lives in its own module: it is generated rather than written out, - and its dates are anchored to now rather than to this file's fixed calendar. - See the note at the top of `attendanceSeed.js`. */ -export const seedData = { - JobPosting: JOB_POSTINGS, - JobApplication: JOB_APPLICATIONS, - AIInterview: AI_INTERVIEWS, - Staff: STAFF, - WorkerProfile: WORKER_PROFILES, - Course: COURSE_LIBRARY, - Badge: BADGES, - LearningPath: LEARNING_PATHS, - Certification: CERTIFICATIONS, - RoleCategory: ROLE_CATEGORIES, - UserActivity: USER_ACTIVITY, - Assignment: ASSIGNMENTS, - ShiftRecord: SHIFT_RECORDS, - Evidence: EVIDENCE, - User: [DEMO_USER], -}; diff --git a/src/api/store.js b/src/api/store.js deleted file mode 100644 index 3010c0f..0000000 --- a/src/api/store.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * In-memory entity store backing the KROW demo. - * - * Mirrors the Base44 entity API the app was built against - * (`list` / `filter` / `get` / `create` / `update` / `delete`) so every hook, - * page and component consumes data exactly as it did against the real backend. - * Records live in memory and are mirrored to localStorage, so demo edits — - * screening a candidate, hiring, creating a position — survive a reload. - */ - -const STORAGE_KEY = 'krow_demo_db'; -const STORAGE_VERSION = 8; - -/** Simulated network latency, in ms, so loading states are real. */ -const LATENCY = { read: 140, write: 220 }; - -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -/** Structured clone with a JSON fallback for older Safari. */ -const clone = (value) => - typeof structuredClone === 'function' - ? structuredClone(value) - : JSON.parse(JSON.stringify(value)); - -let _counter = 0; -export function makeId(prefix = 'rec') { - _counter += 1; - return `${prefix}_${Date.now().toString(36)}${_counter.toString(36).padStart(3, '0')}`; -} - -/* ── Persistence ───────────────────────────────────────────────────────── */ - -let db = {}; - -function persist() { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify({ version: STORAGE_VERSION, db })); - } catch { - // Private browsing / quota — the demo still works from memory. - } -} - -function readPersisted() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return null; - const parsed = JSON.parse(raw); - // A seed-data change bumps STORAGE_VERSION, which invalidates stale copies. - return parsed?.version === STORAGE_VERSION ? parsed.db : null; - } catch { - return null; - } -} - -/** - * Loads the store: persisted snapshot if one matches the current seed version, - * otherwise the seed itself. - */ -export function initStore(seed) { - db = readPersisted() || clone(seed); - // A seed that gained an entity after the snapshot was written still resolves. - for (const [name, records] of Object.entries(seed)) { - if (!db[name]) db[name] = clone(records); - } - persist(); -} - -/** Drops all local edits and restores the shipped demo data. */ -export function resetStore(seed) { - db = clone(seed); - persist(); -} - -/* ── Query helpers ─────────────────────────────────────────────────────── */ - -/** - * Applies a Base44-style sort string: `'-ai_score'` descending, - * `'created_date'` ascending. Unknown fields leave order untouched. - */ -function applySort(records, sort) { - if (!sort) return records; - const desc = sort.startsWith('-'); - const field = desc ? sort.slice(1) : sort; - return [...records].sort((a, b) => { - const av = a?.[field]; - const bv = b?.[field]; - if (av === bv) return 0; - if (av === undefined || av === null) return 1; - if (bv === undefined || bv === null) return -1; - const result = typeof av === 'number' && typeof bv === 'number' - ? av - bv - : String(av).localeCompare(String(bv)); - return desc ? -result : result; - }); -} - -/** Shallow equality match across every key of `query`, array fields included. */ -function matches(record, query) { - return Object.entries(query).every(([key, want]) => { - const got = record?.[key]; - if (Array.isArray(want)) return want.includes(got); - return got === want; - }); -} - -/* ── Entity API ────────────────────────────────────────────────────────── */ - -/** - * Builds the client surface for one entity. Every method is async and returns - * cloned records, so callers can never mutate the store by reference. - */ -export function createEntity(name) { - const table = () => (db[name] ||= []); - - return { - entityName: name, - - async list(sort = '-created_date', limit = 100) { - await delay(LATENCY.read); - return clone(applySort(table(), sort).slice(0, limit)); - }, - - async filter(query = {}, sort = '-created_date', limit = 100) { - await delay(LATENCY.read); - const found = table().filter((record) => matches(record, query)); - return clone(applySort(found, sort).slice(0, limit)); - }, - - async get(id) { - await delay(LATENCY.read); - const found = table().find((record) => record.id === id); - if (!found) throw new Error(`${name} ${id} not found`); - return clone(found); - }, - - async create(data) { - await delay(LATENCY.write); - const now = new Date().toISOString(); - const record = { - id: makeId(name.toLowerCase()), - created_date: now, - updated_date: now, - ...data, - }; - table().unshift(record); - persist(); - return clone(record); - }, - - async update(id, data) { - await delay(LATENCY.write); - const index = table().findIndex((record) => record.id === id); - if (index === -1) throw new Error(`${name} ${id} not found`); - const next = { ...table()[index], ...data, updated_date: new Date().toISOString() }; - table()[index] = next; - persist(); - return clone(next); - }, - - async delete(id) { - await delay(LATENCY.write); - db[name] = table().filter((record) => record.id !== id); - persist(); - return { id }; - }, - - async bulkCreate(records = []) { - const created = []; - for (const record of records) created.push(await this.create(record)); - return created; - }, - }; -} diff --git a/src/assets/brand.js b/src/assets/brand.js index 83a5fe3..3e4ef3e 100644 --- a/src/assets/brand.js +++ b/src/assets/brand.js @@ -1,9 +1,16 @@ /** - * Krow brand assets, served from `public/` so the dashboard has no remote image - * dependency. Referenced through this module rather than inline strings so a - * swap is a one-line change. + * Doormile brand assets, served from `public/` so the console has no remote + * image dependency. Referenced through this module rather than as inline + * strings, so swapping an asset is a one-line change. * - * Owliver is a separate product and owns its own assets — nothing here. + * `doormile-logo.png` is the white wordmark — it is invisible on the console's + * light chrome, so the circular red mark is what the header and the sign-in + * page use. Reach for the wordmark only on a dark ground. */ -export const KROW_LOGO_URL = '/krow-logo.webp'; +/** The circular red D — the brand mark, legible on light surfaces. */ +export const DOORMILE_MARK_URL = '/doormile-mark.png'; + +/** The white wordmark. Dark backgrounds only. */ +export const DOORMILE_WORDMARK_URL = '/doormile-logo.png'; + diff --git a/src/assets/images/doormile-mark.png b/src/assets/images/doormile-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..f64351844eb8674eeada1cad25774313d5258af3 GIT binary patch literal 83551 zcmeFZ^;=Zm7dA|Tgn%F+4T>TSQj!V?NC`+u!%)&79fKfU(hZVIcgN5T1JWQ}(hN1^ z%zMVq^FDvXbA9H_}KMDL#0{@f1|0MALrUXtDm(WN_N#|ddPZ7;Uvi*ybpy3y2B&4KlQMJmO4b%00 z4EPU$=;_AyFGiYGsI$UFe*; zH`^Y1d8Wn#MBG!tf6>+|*T|Zj5D2hhn5gO0?Ox4S7(&l$v|j+vXiB4E??*o6%dP7d znUIww=@@bLdhla|_s-x7f^`6XcK2;FutHB8TWiE$$D}g}y>X|Y*9`Mk- z8tNhIf(MWUGHeWH<~T2WG#QzvoUhiB-{(pJ6RQ9BMjY$CI;SAQK${A9{Mo-xxd|%Y z6fCe20^g4N7yU)kSnrsjDtg#5GN7OT<`oTKy=Pv0R{|gkP{_X#Gue2**s1~^VCKFA zlsM+;)6OFfXz@{xw6FoPj4dlrfXG|_o7XNEwWs=Tz%0A@sCVj9Q15thrCmiN#Ki7v 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ℑLH^vV6!ltU%*7=%^zH&!eaFX1 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 V!zj{wQHMGNsHWkAiu;al{txy&3PS(@ literal 0 HcmV?d00001 diff --git a/src/components/CircularLoader.jsx b/src/components/CircularLoader.jsx new file mode 100644 index 0000000..e441520 --- /dev/null +++ b/src/components/CircularLoader.jsx @@ -0,0 +1,29 @@ +import { Center } from '@astryxdesign/core/Center'; + +import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; +import { DT } from 'themes/dt/tokens'; + +// ==============================|| FULL-SCREEN BRANDED LOADER ||============================== // +// Centered branded ring on a translucent scrim. The ring itself is +// LoaderWithImage — this component only supplies the fixed-position overlay, +// so the two loaders can't drift apart visually. + +const CircularLoader = () => ( +

+ +
+); + +export default CircularLoader; diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx index 6018bc6..10cec83 100644 --- a/src/components/ProtectedRoute.jsx +++ b/src/components/ProtectedRoute.jsx @@ -1,7 +1,5 @@ -import { useEffect } from 'react'; -import { Outlet } from 'react-router-dom'; +import { Navigate, Outlet, useLocation } from 'react-router-dom'; import { useAuth } from '@/lib/AuthContext'; -import UserNotRegisteredError from '@/components/UserNotRegisteredError'; const DefaultFallback = () => (
@@ -9,28 +7,21 @@ const DefaultFallback = () => (
); -export default function ProtectedRoute({ fallback = , unauthenticatedElement }) { - const { isAuthenticated, isLoadingAuth, authChecked, authError, checkUserAuth } = useAuth(); +/** + * The console's only gate: a Doormile token, or the login page. + * + * The attempted path travels along in location state, so signing in returns you + * to where you were going rather than dropping you on Dispatch — which is what + * a bookmarked deep link needs. + */ +export default function ProtectedRoute({ fallback = }) { + const location = useLocation(); + const { isAuthenticated, isLoadingAuth, authChecked } = useAuth(); - useEffect(() => { - if (!authChecked && !isLoadingAuth) { - checkUserAuth(); - } - }, [authChecked, isLoadingAuth, checkUserAuth]); - - if (isLoadingAuth || !authChecked) { - return fallback; - } - - if (authError) { - if (authError.type === 'user_not_registered') { - return ; - } - return unauthenticatedElement; - } + if (isLoadingAuth || !authChecked) return fallback; if (!isAuthenticated) { - return unauthenticatedElement; + return ; } return ; diff --git a/src/components/UserNotRegisteredError.jsx b/src/components/UserNotRegisteredError.jsx deleted file mode 100644 index ede5274..0000000 --- a/src/components/UserNotRegisteredError.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; - -const UserNotRegisteredError = () => { - return ( -
-
-
-
- - - -
-

Access Restricted

-

- You are not registered to use this application. Please contact the app administrator to request access. -

-
-

If you believe this is an error, you can:

-
    -
  • Verify you are logged in with the correct account
  • -
  • Contact the app administrator for access
  • -
  • Try logging out and back in again
  • -
-
-
-
-
- ); -}; - -export default UserNotRegisteredError; diff --git a/src/components/admin/PageShell.jsx b/src/components/admin/PageShell.jsx deleted file mode 100644 index 98efa86..0000000 --- a/src/components/admin/PageShell.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import * as React from 'react'; -import { ChevronRight } from 'lucide-react'; -import { cn } from '@/lib/utils'; - -/** - * Shared page furniture for the Admin product. - * - * Admin pages are dense and numerous, so the header, the section labels and the - * vertical rhythm are defined once here. That is what makes seven independently - * built pages read as one product rather than seven takes on a dashboard. - */ - -/** - * AdminPage — the standard page frame: title, subtitle, right-aligned controls, - * then stacked sections. - * - * Controls live in the page rather than in the global chrome, which is why the - * Admin header only needs one 56px bar. - */ -/** @param {any} props */ -export function AdminPage({ title, subtitle, meta, actions, tabs, children, className }) { - return ( -
-
-
-
-

{title}

- {/* A count of whatever the page is a list of, stated beside its - name rather than only inside the list. */} - {meta && {meta}} - - - Live System - -
- {subtitle &&

{subtitle}

} -
- {actions &&
{actions}
} -
- - {tabs} - - {children} -
- ); -} - -/** - * SectionTitle — the label above a region. - * - * Sentence-case and small rather than a heavy all-caps overline: on a page with - * four or five sections, loud labels compete with the data they introduce. - */ -/** @param {any} props */ -export function SectionTitle({ id, title, meta, action, className }) { - return ( -
-
-

{title}

- {meta && {meta}} -
- - {action && ( - - )} -
- ); -} - -/** - * Toolbar — the controls row above a table or list. - * - * Search takes the remaining space, filters keep their natural width, and the - * primary action is pushed to the end, so every management page has the same - * control geometry. - */ -/** @param {any} props */ -export function Toolbar({ search = null, filters = null, actions = null, meta = null, className = '' }) { - return ( -
-
- {search &&
{search}
} - {filters &&
{filters}
} - {actions &&
{actions}
} -
- {meta &&

{meta}

} -
- ); -} diff --git a/src/components/admin/RoleGlyph.jsx b/src/components/admin/RoleGlyph.jsx deleted file mode 100644 index 351834f..0000000 --- a/src/components/admin/RoleGlyph.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - Briefcase, ChefHat, ConciergeBell, HardHat, PackageSearch, ShieldCheck, Sparkles, Truck, Wine, -} from 'lucide-react'; -import { cn } from '@/lib/utils'; - -/** - * RoleGlyph — the visual identity on a position card. - * - * Positions in this dataset have no photograph, and there is nothing to - * photograph anyway: a role is not a person. The two obvious fallbacks are both - * worse than this one — two-letter initials ("BA", "SE") read as a placeholder - * someone forgot to replace, and stock photography attaches an invented face to a - * job requisition, which is misleading on a page about hiring real people. - * - * So the glyph is the role's own trade: a chef's hat, a wine glass, a hard hat. - * It is drawn from the existing icon language and the existing brand tint, it is - * stable for a given category, and it tells you what kind of work the position is - * from across the room — which is the actual job of an image in a card grid. - */ - -/** Category to icon. Matched loosely, so a new "Head Chef" still gets the hat. */ -/** @type {[RegExp, React.ComponentType][]} */ -const GLYPHS = [ - [/chef|cook|kitchen/i, ChefHat], - [/bartend|bar|mixolog/i, Wine], - [/server|wait|host|banquet|concierge/i, ConciergeBell], - [/security|guard|officer/i, ShieldCheck], - [/pick|warehouse|stock|inventory/i, PackageSearch], - [/driver|delivery|logistic/i, Truck], - [/construct|labour|labor|site/i, HardHat], -]; - -export function iconForRole(category, title = '') { - const subject = `${category || ''} ${title || ''}`; - const match = GLYPHS.find(([pattern]) => pattern.test(subject)); - return match ? match[1] : Briefcase; -} - -const SIZES = { - sm: { box: 'h-9 w-9 rounded-lg', icon: 'h-4 w-4' }, - md: { box: 'h-11 w-11 rounded-xl', icon: 'h-5 w-5' }, - lg: { box: 'h-14 w-14 rounded-xl', icon: 'h-6 w-6' }, -}; - -/** @param {any} props */ -export function RoleGlyph({ category, title, size = 'md', aiGenerated = false, className = '' }) { - const Icon = iconForRole(category, title); - const s = SIZES[size] || SIZES.md; - - return ( - - ); -} - -export default RoleGlyph; diff --git a/src/components/agents/AddSkillsModal.jsx b/src/components/agents/AddSkillsModal.jsx deleted file mode 100644 index 6bf4890..0000000 --- a/src/components/agents/AddSkillsModal.jsx +++ /dev/null @@ -1,180 +0,0 @@ -import * as React from 'react'; -import { Check } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { Button, Modal, SearchInput } from '@/components/ds'; -import { allSkills, skillsWithFacet } from '@/lib/skills/registry'; -import { surfaceFor } from '@/lib/skills/surfaces'; - -/** - * Attaching skills to an agent. - * - * Reads **the one skill registry** — the same `allSkills` the Skills page and - * Owliver itself read. There is deliberately no separate list for agents: a - * second one would drift, and an agent would end up offering a skill the - * runtime does not have. - * - * Categories are derived rather than written down. A skill's own `category` - * field when it declares one, and the pages it attaches to otherwise, so a - * filter can never offer a grouping that matches nothing — and a skill added - * tomorrow appears under its own category without this file being edited. - */ - -const ALL = 'all'; - -/** The groupings the registry actually contains, in a stable order. */ -function categoriesFor(skills) { - const named = new Set(); - for (const skill of skills) { - if (skill.category) named.add(skill.category); - } - return [ALL, ...[...named].sort()]; -} - -/** What this skill contributes, in the terms the reader is choosing between. */ -function skillSummary(skill) { - const pages = skill.pages.map((p) => surfaceFor(p)?.label || p); - const capabilities = skill.owliver?.capabilities || []; - return [ - pages.length ? pages.join(', ') : 'No pages', - capabilities.length - ? `${capabilities.length} capabilit${capabilities.length === 1 ? 'y' : 'ies'}` - : (skill.actions || []).length ? `${skill.actions.length} action${skill.actions.length === 1 ? '' : 's'}` : null, - ].filter(Boolean).join(' · '); -} - -/** @param {any} props */ -export function AddSkillsModal({ open, onOpenChange, attached = [], customSkills = [], onAdd }) { - const [query, setQuery] = React.useState(''); - const [category, setCategory] = React.useState(ALL); - const [picked, setPicked] = React.useState([]); - - /* A fresh sheet each time, so a previous selection is not still ticked. */ - React.useEffect(() => { - if (open) { setQuery(''); setCategory(ALL); setPicked([]); } - }, [open]); - - /* Owliver skills only. A workforce training path is something a person - learns, not something an agent can be asked to do, and offering one here - would promise behaviour that does not exist. */ - const available = React.useMemo( - () => skillsWithFacet(allSkills(customSkills), 'owliver') - .filter((s) => s.status === 'active' && !attached.includes(s.id)), - [customSkills, attached] - ); - - const categories = React.useMemo(() => categoriesFor(available), [available]); - - const visible = React.useMemo(() => { - const q = query.trim().toLowerCase(); - return available - .filter((s) => category === ALL || s.category === category) - .filter((s) => !q - || s.name.toLowerCase().includes(q) - || s.description.toLowerCase().includes(q) - || s.id.includes(q)); - }, [available, category, query]); - - const toggle = (id) => - setPicked((current) => (current.includes(id) - ? current.filter((x) => x !== id) - : [...current, id])); - - return ( - -

- {picked.length - ? `${picked.length} skill${picked.length === 1 ? '' : 's'} selected` - : 'Nothing selected yet'} -

- - - - } - > -
- - - {categories.length > 1 && ( -
- {categories.map((id) => ( - - ))} -
- )} - -
- {visible.map((skill) => { - const chosen = picked.includes(skill.id); - return ( - - ); - })} - - {!visible.length && ( -

- {available.length - ? 'No skills match that.' - : 'Every available skill is already attached to this agent.'} -

- )} -
-
-
- ); -} - -export default AddSkillsModal; diff --git a/src/components/agents/AgentCanvas.jsx b/src/components/agents/AgentCanvas.jsx deleted file mode 100644 index f9f3686..0000000 --- a/src/components/agents/AgentCanvas.jsx +++ /dev/null @@ -1,516 +0,0 @@ -import * as React from 'react'; -import { ChevronRight, X } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { agentIconFor } from './icons'; -import OwliverAvatar from '@/components/krow/OwliverAvatar'; - -/** - * The Agent Configure workspace, as parts. - * - * The screen this builds is an *authoring* surface, and it is the only one in - * the Admin console that is. The eight operational pages report; this one is - * written in. That is the whole reason it is allowed to look different from - * them, and the difference is composition rather than colour: every token here - * — `border-border`, `surface-subtle`, `krow-blue-tint`, the ink scale, the - * duration and easing variables — is the same Control Tower vocabulary the rest - * of the console uses. - * - * Three ideas carry the layout: - * - * 1. **One document, not a stack of cards.** Four equal cards make four - * unrelated settings groups. One surface divided by hairlines makes a thing - * being edited, which is what an agent definition is. - * - * 2. **A spine on the left.** The rail lists what an agent is made of — - * Identity, Instructions, Capabilities, Behavior, and what is inside each — - * with its current value beside it. It is the tree reading of the - * configuration, and it is what lets someone understand the page before - * reading a single field. It is navigation, never a second copy of state. - * - * 3. **Values on the surface.** A row states what it currently holds while - * closed. Nothing has to be opened to be read. - * - * Motion is CSS only. `prefers-reduced-motion` is handled once, globally, in - * `index.css`, so a component that animates with transitions inherits that and - * a component that animates in JavaScript would not. - */ - -/* ── The surface ─────────────────────────────────────────────────────────── */ - -/** - * The workspace: rail beside document, both inside one border. - * - * The rail only becomes a column when there is genuinely room for one. Owliver - * holds a ~380px track on the right of every Admin page, so below `xl` the - * remaining width belongs to the editor and the rail becomes a strip above it. - */ -export function Workspace({ rail, children, className }) { - return ( -
-
- {rail} -
-
{children}
-
- ); -} - -/* ── Motion ──────────────────────────────────────────────────────────────── */ - -/** - * Open and close, in height. - * - * `grid-template-rows: 0fr → 1fr` is the one way to transition to an unknown - * height without measuring it in JavaScript. Content stays mounted, which is - * what keeps a section addressable by the rail while it is closed. - */ -/** @param {any} props */ -export function Collapse({ open, children, className }) { - return ( -
- {/* `inert` rather than `aria-hidden`: the content stays mounted so a - closed section is still addressable, and a mounted control that cannot - be seen must not be reachable by Tab either. `aria-hidden` alone would - hide it from a screen reader while leaving it in the tab order, which - is the worse of both. */} -
- {children} -
-
- ); -} - -/* ── The rail ────────────────────────────────────────────────────────────── */ - -/** - * The spine. - * - * Provides clear section navigation and leaf anchor shortcuts across the - * configuration workspace. - */ -/** @param {any} props */ -export function Rail({ sections, activeId, onSelect }) { - const activeSection = sections.find((s) => s.id === activeId); - - return ( - - ); -} - -/* ── The document ────────────────────────────────────────────────────────── */ - -/** - * One section of the document. - */ -/** @param {any} props */ -export function DocSection({ - id, icon: Icon, title, description, meta, open, onToggle, emphasis = false, last = false, - children, -}) { - return ( -
- - - -
- {children} -
-
-
- ); -} - -/** A labelled field in the document body. */ -/** @param {any} props */ -export function DocField({ id, label, hint, children, className }) { - const reactId = React.useId(); - const controlId = id ? `${id}-control` : reactId; - - return ( -
- - {React.isValidElement(children) - ? React.cloneElement(children, { id: children.props.id || controlId }) - : children} - {hint &&

{hint}

} -
- ); -} - -/** - * The head of a group inside a section — Pages, Skills, Knowledge. - * - * One notch quieter than a section title and one louder than a row, which is - * exactly the level of the thing it names. - */ -/** @param {any} props */ -export function GroupHead({ id, icon: Icon, title, count, action, className }) { - return ( -
- {Icon &&
- ); -} - -/** - * A thing attached to the agent — a skill, a subagent — with a way to detach it. - * - * A row, not a card. Five attached skills as five cards is five borders - * competing with the section that holds them; as rows they read as a list, - * which is what they are. - */ -/** @param {any} props */ -export function ItemRow({ icon: Icon, title, detail, warning, onRemove, removeLabel }) { - return ( -
  • - {Icon && ( -
  • - ); -} - -/** - * A behaviour setting: what it is, what it is set to, and the way to change it. - * - * Two shapes, one row. A setting whose control fits on the row carries it - * there — a switch, a segmented choice — and is not a button, because wrapping - * a switch in a button makes one press mean two things. A setting that needs - * space gets a chevron and opens underneath. - */ -/** @param {any} props */ -export function SettingRow({ - id, icon: Icon, title, value, description, control, children, open, onToggle, last = false, -}) { - const expandable = Boolean(children); - - const body = ( - <> - {Icon &&