ui changes
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { AlertTriangle, Eye, Gauge, Lightbulb, ScrollText, TrendingUp } from 'lucide-react';
|
||||
import { actions, answer, doc, heading, insights, kpis, note, status, table, text } from '../blocks';
|
||||
import {
|
||||
Activity, AlertTriangle, ArrowLeftRight, BellRing, Eye, Filter, Gauge, GitBranch,
|
||||
Lightbulb, ScrollText, ShieldAlert, TrendingUp, Users,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
actions, answer, doc, funnel, heading, insights, kpis, list, meters, note, status, table,
|
||||
text, timeline,
|
||||
} from '../blocks';
|
||||
import { plural, verb } from '../insights';
|
||||
|
||||
/**
|
||||
@@ -68,6 +74,199 @@ const platformHealth = (f) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Workforce summary — the platform's scale in one read.
|
||||
*
|
||||
* Answers "how big is this thing and what is it made of", which is a different
|
||||
* question from "is it healthy": no checks, no verdicts, just the composition.
|
||||
*/
|
||||
const workforceSummary = (f) => doc(
|
||||
heading('Workforce summary', `${plural(f.postings.length, 'position')} · ${plural(f.profiles.length, 'worker')}`),
|
||||
kpis([
|
||||
{ label: 'Open roles', value: f.openPositions.length, sub: `of ${f.postings.length}` },
|
||||
{ label: 'Applicants', value: f.total, sub: `${f.scored.length} scored` },
|
||||
{ label: 'On staff', value: f.staff.length, sub: `${f.hired.length} hired here` },
|
||||
{ label: 'Talent pool', value: f.profiles.length, sub: `${f.elite.length} Elite` },
|
||||
]),
|
||||
heading('Where the people are'),
|
||||
meters([
|
||||
{ label: 'In the pipeline', value: f.total, max: f.total + f.profiles.length },
|
||||
{ label: 'In the talent pool', value: f.profiles.length, max: f.total + f.profiles.length },
|
||||
{ label: 'Placed on staff', value: f.staff.length, max: f.total + f.profiles.length, tone: 'success' },
|
||||
]),
|
||||
heading('Demand by role category'),
|
||||
table(
|
||||
[
|
||||
{ key: 'category', label: 'Category' },
|
||||
{ key: 'roles', label: 'Roles', align: 'right' },
|
||||
{ key: 'applied', label: 'Applicants', align: 'right' },
|
||||
],
|
||||
Object.values(f.byRole.reduce((acc, r) => {
|
||||
acc[r.category] ||= { category: r.category, roles: 0, applied: 0 };
|
||||
acc[r.category].roles += 1;
|
||||
acc[r.category].applied += r.applied;
|
||||
return acc;
|
||||
}, {})).sort((a, b) => b.applied - a.applied)
|
||||
),
|
||||
text(
|
||||
f.profiles.length > f.total
|
||||
? `The talent pool is larger than the live pipeline — ${f.profiles.length} registered workers against ${f.total} applications. Most of the supply is not applying to anything.`
|
||||
: 'Most of the platform\'s people are attached to a live application rather than sitting in the pool.'
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Hiring operations — throughput and speed.
|
||||
*
|
||||
* The operational question is whether work is moving, so this leads with rate
|
||||
* and duration rather than totals.
|
||||
*/
|
||||
const hiringOperations = (f) => {
|
||||
const perRole = f.byRole
|
||||
.slice()
|
||||
.sort((a, b) => b.applied - a.applied)
|
||||
.map((r) => ({
|
||||
title: r.title,
|
||||
applied: r.applied,
|
||||
qualified: r.qualified,
|
||||
hired: r.hired || '—',
|
||||
rate: { value: `${r.applied ? Math.round((r.qualified / r.applied) * 100) : 0}%`, tone: r.applied && r.qualified / r.applied >= 0.5 ? 'success' : 'warning' },
|
||||
}));
|
||||
|
||||
return doc(
|
||||
heading('Hiring operations'),
|
||||
kpis([
|
||||
{ label: 'Applicants', value: f.total },
|
||||
{ label: 'Hires', value: f.hired.length, tone: 'success', sub: `${f.hireRate}% of applicants` },
|
||||
{ label: 'Time to hire', value: f.timeToHire ? `${f.timeToHire}d` : '—' },
|
||||
{ label: 'Screening', value: `${f.standardizedPct}%`, tone: f.standardizedPct >= 80 ? 'success' : 'warning', sub: 'coverage' },
|
||||
]),
|
||||
heading('Throughput by role'),
|
||||
table(
|
||||
[
|
||||
{ key: 'title', label: 'Role' },
|
||||
{ key: 'applied', label: 'Applied', align: 'right' },
|
||||
{ key: 'qualified', label: '70+', align: 'right' },
|
||||
{ key: 'hired', label: 'Hired', align: 'right' },
|
||||
{ key: 'rate', label: 'Qualified', align: 'right' },
|
||||
],
|
||||
perRole
|
||||
),
|
||||
insights([
|
||||
f.starvedPositions.length && {
|
||||
tone: 'warning',
|
||||
title: `${plural(f.starvedPositions.length, 'role')} attracting nobody`,
|
||||
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. Throughput cannot improve on a role with no input.`,
|
||||
},
|
||||
f.timeToHire > 0 && {
|
||||
tone: f.timeToHire <= 7 ? 'success' : 'warning',
|
||||
title: `Hires close in ${plural(f.timeToHire, 'day')}`,
|
||||
body: f.timeToHire <= 7
|
||||
? 'Fast enough that strong candidates are unlikely to be lost to a competing offer.'
|
||||
: 'Long enough that the best candidates may accept elsewhere first.',
|
||||
},
|
||||
]),
|
||||
note('Time-to-hire is measured from application to decision, so it reflects this platform\'s part of the process only.')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pipeline health — the funnel and its worst step.
|
||||
*
|
||||
* A funnel block rather than a table: the shape is the finding, and the single
|
||||
* weakest transition is what someone can actually act on.
|
||||
*/
|
||||
const pipelineHealth = (f) => {
|
||||
if (!f.total) return answer('Nothing has entered the pipeline yet, so there is no funnel to read.');
|
||||
|
||||
const worst = f.bottleneck;
|
||||
|
||||
return doc(
|
||||
heading('Pipeline health', `${plural(f.total, 'candidate')} entered`),
|
||||
funnel(f.funnel.map((stage, i) => ({
|
||||
label: stage.label,
|
||||
count: stage.count,
|
||||
rate: i === 0 ? null : f.transitions[i - 1]?.rate,
|
||||
lost: i === 0 ? 0 : f.transitions[i - 1]?.lost,
|
||||
}))),
|
||||
worst && worst.lost > 0
|
||||
? insights([{
|
||||
tone: worst.rate < 50 ? 'risk' : 'warning',
|
||||
title: `${worst.from} → ${worst.to} is the bottleneck`,
|
||||
body: `Only ${worst.rate}% pass through, losing ${plural(worst.lost, 'candidate')}. Every other step performs better, so this is where recovered conversion is cheapest.`,
|
||||
}])
|
||||
: insights([{
|
||||
tone: 'success',
|
||||
title: 'No single step is losing candidates',
|
||||
body: 'Conversion is even across the funnel — there is no cheap win to recover here.',
|
||||
}]),
|
||||
heading('Stage-to-stage rates'),
|
||||
table(
|
||||
[
|
||||
{ key: 'step', label: 'Transition' },
|
||||
{ key: 'rate', label: 'Pass', align: 'right' },
|
||||
{ key: 'lost', label: 'Lost', align: 'right' },
|
||||
],
|
||||
f.transitions.map((t) => ({
|
||||
step: `${t.from} → ${t.to}`,
|
||||
rate: { value: `${t.rate}%`, tone: t.rate < 60 ? 'warning' : 'success' },
|
||||
lost: t.lost ? { value: `−${t.lost}`, tone: 'risk' } : '0',
|
||||
}))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Attention required — the triage queue, ordered by cost of inaction.
|
||||
*
|
||||
* The same items the Control Center lists, because the panel must never rank
|
||||
* things differently from the page beside it.
|
||||
*/
|
||||
const attentionRequired = (f) => {
|
||||
const items = [
|
||||
f.starvedPositions.length && {
|
||||
tone: 'risk',
|
||||
title: `${plural(f.starvedPositions.length, 'role')} with no candidate supply`,
|
||||
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. Live and attracting nobody — usually a pay band or distribution problem.`,
|
||||
},
|
||||
f.stalled.length && {
|
||||
tone: 'risk',
|
||||
title: `${plural(f.stalled.length, 'strong candidate')} not actioned`,
|
||||
body: `${f.stalled.map((a) => a.applicant_name).join(', ')} scored 80 or above and ${verb(f.stalled.length, 'is', 'are')} still sitting at AI Screened. The most expensive kind of delay.`,
|
||||
},
|
||||
f.unscreened.length && {
|
||||
tone: 'warning',
|
||||
title: `${plural(f.unscreened.length, 'applicant')} waiting on screening`,
|
||||
body: 'Each unscored application is a decision made without a standard.',
|
||||
},
|
||||
f.unscoredProfiles.length && {
|
||||
tone: 'warning',
|
||||
title: `${plural(f.unscoredProfiles.length, 'talent profile')} without a score`,
|
||||
body: 'Invisible to matching, so employers cannot find them even when they fit.',
|
||||
},
|
||||
f.unratedStaff.length && {
|
||||
tone: 'info',
|
||||
title: `${plural(f.unratedStaff.length, 'hire')} awaiting review`,
|
||||
body: 'Employer ratings are the only verified performance signal feeding scores.',
|
||||
},
|
||||
f.flagged.length && {
|
||||
tone: 'warning',
|
||||
title: `${plural(f.flagged.length, 'interview')} flagged for integrity`,
|
||||
body: 'Worth clearing before those candidates progress.',
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
if (!items.length) {
|
||||
return answer('Nothing needs intervention. Every open role has candidates, every applicant is scored, and no interview is flagged.');
|
||||
}
|
||||
|
||||
return doc(
|
||||
heading(`${plural(items.length, 'item')} need attention`, 'Ordered by cost of leaving it'),
|
||||
insights(items),
|
||||
text(`Start with ${items[0].title.toLowerCase()} — it is the one where waiting costs the most.`)
|
||||
);
|
||||
};
|
||||
|
||||
const recommendations = (f) => {
|
||||
const items = [
|
||||
f.unscoredProfiles.length && {
|
||||
@@ -197,6 +396,195 @@ const candidateRiskAnalysis = (f) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Screening gaps — where the record is incomplete.
|
||||
*
|
||||
* Distinct from risk: risk is what the data says about a candidate, this is what
|
||||
* the data fails to say at all. An unscored applicant is not a weak applicant,
|
||||
* and conflating the two is how good people get passed over.
|
||||
*/
|
||||
const screeningGaps = (f) => {
|
||||
const gaps = [
|
||||
{
|
||||
label: 'Unscored applications',
|
||||
count: f.unscreened.length,
|
||||
note: 'Cannot be ranked or compared against anyone.',
|
||||
},
|
||||
{
|
||||
label: 'Scored without credentials',
|
||||
count: f.missingCredentials.length,
|
||||
note: 'No certifications on file to verify against a role requirement.',
|
||||
},
|
||||
{
|
||||
label: 'No recorded experience',
|
||||
count: f.scored.filter((a) => !a.years_experience).length,
|
||||
note: 'Scored on other dimensions, unverifiable on history.',
|
||||
},
|
||||
{
|
||||
label: 'Interviews started, not scored',
|
||||
count: f.interviews.length - f.completedInterviews.length,
|
||||
note: 'Abandoned before a verdict, so the interview signal is missing.',
|
||||
},
|
||||
{
|
||||
label: 'Availability not set',
|
||||
count: f.narrowAvailability.length,
|
||||
note: 'One slot or none recorded, so scheduling is guesswork.',
|
||||
},
|
||||
].filter((g) => g.count > 0);
|
||||
|
||||
if (!gaps.length) {
|
||||
return answer('No screening gaps. Every applicant is scored, credentialed and has availability recorded, and every interview reached a verdict.');
|
||||
}
|
||||
|
||||
return doc(
|
||||
heading('Screening gaps', `${plural(gaps.length, 'kind')} of missing data`),
|
||||
table(
|
||||
[
|
||||
{ key: 'label', label: 'Gap' },
|
||||
{ key: 'count', label: 'Records', align: 'right' },
|
||||
],
|
||||
gaps.map((g) => ({ label: g.label, count: { value: g.count, tone: 'warning' } }))
|
||||
),
|
||||
heading('Coverage'),
|
||||
status([
|
||||
{
|
||||
label: 'Screening coverage',
|
||||
value: `${f.standardizedPct}%`,
|
||||
ok: f.standardizedPct >= 80,
|
||||
note: f.standardizedPct >= 80
|
||||
? 'Enough of the pool is scored for comparisons to mean something.'
|
||||
: `${plural(f.unscreened.length, 'applicant')} outside the standard.`,
|
||||
},
|
||||
{
|
||||
label: 'Interview completion',
|
||||
value: `${f.interviewCompletion}%`,
|
||||
ok: f.interviewCompletion >= 80,
|
||||
note: f.interviewCompletion >= 80
|
||||
? 'Most interviews reach a verdict.'
|
||||
: 'A meaningful share of interviews stop before scoring.',
|
||||
},
|
||||
]),
|
||||
list(gaps.map((g) => `**${g.label}** — ${g.note}`)),
|
||||
note('A gap is missing evidence, not a negative finding. These candidates are unassessed, not unsuitable.')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hiring recommendations — who to move on, and in what order.
|
||||
*
|
||||
* Named people with a stated reason. A recommendation that does not name anyone
|
||||
* is an observation, and the Admin already has plenty of those.
|
||||
*/
|
||||
const hiringRecommendations = (f) => {
|
||||
if (!f.scored.length) {
|
||||
return answer('Nothing is scored yet, so there is no basis for a recommendation. Screen the pipeline first.');
|
||||
}
|
||||
|
||||
const advance = f.ranked.filter((a) => a.ai_score >= 80 && a.status === 'ai_screened').slice(0, 4);
|
||||
const interview = f.ranked.filter((a) => a.ai_score >= 60 && a.ai_score < 80 && a.status === 'ai_screened').slice(0, 4);
|
||||
const review = f.unscreened.slice(0, 4);
|
||||
|
||||
const rows = [
|
||||
...advance.map((a) => ({
|
||||
name: a.applicant_name,
|
||||
role: a.job_title,
|
||||
score: { value: a.ai_score, tone: 'success' },
|
||||
action: { badge: 'Advance', tone: 'success' },
|
||||
})),
|
||||
...interview.map((a) => ({
|
||||
name: a.applicant_name,
|
||||
role: a.job_title,
|
||||
score: { value: a.ai_score, tone: 'info' },
|
||||
action: { badge: 'Interview', tone: 'info' },
|
||||
})),
|
||||
...review.map((a) => ({
|
||||
name: a.applicant_name,
|
||||
role: a.job_title,
|
||||
score: '—',
|
||||
action: { badge: 'Screen', tone: 'warning' },
|
||||
})),
|
||||
];
|
||||
|
||||
return doc(
|
||||
heading('Recommended next actions', `${plural(rows.length, 'candidate')}`),
|
||||
table(
|
||||
[
|
||||
{ key: 'name', label: 'Candidate' },
|
||||
{ key: 'role', label: 'Role' },
|
||||
{ key: 'score', label: 'Score', align: 'right' },
|
||||
{ key: 'action', label: 'Action', align: 'right' },
|
||||
],
|
||||
rows
|
||||
),
|
||||
actions([
|
||||
advance.length && {
|
||||
title: `Advance ${plural(advance.length, 'candidate')} scoring 80 or above`,
|
||||
body: `${advance.map((a) => a.applicant_name).join(', ')}. Already screened and waiting — the delay, not the assessment, is what loses these people.`,
|
||||
},
|
||||
review.length && {
|
||||
title: `Screen ${plural(review.length, 'waiting applicant')}`,
|
||||
body: 'Until they are scored they cannot be compared with anyone already in the funnel.',
|
||||
},
|
||||
f.singleCandidateRoles.length && {
|
||||
title: `Widen supply on ${plural(f.singleCandidateRoles.length, 'role')}`,
|
||||
body: `${f.singleCandidateRoles.map((e) => e.posting.title).join(', ')} ${verb(f.singleCandidateRoles.length, 'rests', 'rest')} on one viable candidate. One withdrawal reopens the search.`,
|
||||
},
|
||||
]),
|
||||
note('Ranked on screening data only. Nothing here accounts for a conversation you have had that the platform has not seen.')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Top candidates side by side.
|
||||
*
|
||||
* A comparison is a table — the whole point is reading the same dimension across
|
||||
* people, which prose makes impossible.
|
||||
*/
|
||||
const topCandidates = (f) => {
|
||||
const top = f.ranked.slice(0, 5);
|
||||
if (!top.length) return answer('No scored candidates yet, so there is nobody to compare.');
|
||||
|
||||
const DIMENSIONS = ['experience', 'english', 'reliability', 'certifications', 'availability'];
|
||||
|
||||
return doc(
|
||||
heading('Strongest candidates', `Top ${top.length} by KROW score`),
|
||||
table(
|
||||
[
|
||||
{ key: 'name', label: 'Candidate' },
|
||||
{ key: 'role', label: 'Applied for' },
|
||||
{ key: 'score', label: 'Score', align: 'right' },
|
||||
{ key: 'stage', label: 'Stage', align: 'right' },
|
||||
],
|
||||
top.map((a) => ({
|
||||
name: a.applicant_name,
|
||||
role: a.job_title,
|
||||
score: { value: a.ai_score, tone: a.ai_score >= 80 ? 'success' : 'info' },
|
||||
stage: { badge: String(a.status).replace(/_/g, ' '), tone: a.status === 'hired' ? 'success' : 'neutral' },
|
||||
}))
|
||||
),
|
||||
heading('Score breakdown', top[0].applicant_name),
|
||||
meters(DIMENSIONS.map((d) => ({
|
||||
label: d[0].toUpperCase() + d.slice(1),
|
||||
value: top[0].score_breakdown?.[d] ?? 0,
|
||||
max: 100,
|
||||
}))),
|
||||
insights([
|
||||
top.length >= 2 && {
|
||||
tone: 'info',
|
||||
title: `${top[0].applicant_name} leads by ${top[0].ai_score - top[1].ai_score} points`,
|
||||
body: top[0].ai_score - top[1].ai_score <= 5
|
||||
? `Effectively level with ${top[1].applicant_name} — close enough that the choice should come down to a conversation, not the score.`
|
||||
: `A clear enough margin over ${top[1].applicant_name} to act on.`,
|
||||
},
|
||||
f.stalled.length && {
|
||||
tone: 'warning',
|
||||
title: `${plural(f.stalled.length, 'candidate')} at 80+ waiting on a decision`,
|
||||
body: f.stalled.map((a) => a.applicant_name).join(', '),
|
||||
},
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
/* ── Activity ───────────────────────────────────────────────────────────── */
|
||||
|
||||
const auditSummary = (f) => {
|
||||
@@ -294,26 +682,231 @@ const userActivityInsights = (f) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Privileged actions carry more audit weight than reads. Kept in step with the
|
||||
* Activity page's own severity table — one definition of "privileged", so the
|
||||
* panel and the log can never disagree about what matters.
|
||||
*/
|
||||
const HIGH_SEVERITY = ['hire_candidate', 'create_position'];
|
||||
const MEDIUM_SEVERITY = ['screen_candidate', 'start_interview'];
|
||||
|
||||
/**
|
||||
* Unusual activity — what is out of pattern, stated as a pattern.
|
||||
*
|
||||
* Deliberately not "suspicious". These are deviations from the platform's own
|
||||
* baseline, and on a real deployment most of them turn out to be an integration
|
||||
* or a busy afternoon. Saying otherwise would manufacture alarm.
|
||||
*/
|
||||
const unusualActivity = (f) => {
|
||||
if (!f.activity.length) return answer('No activity logged yet, so there is no baseline to deviate from.');
|
||||
|
||||
const perUser = f.activity.reduce((acc, e) => {
|
||||
acc[e.user_email] ||= { name: e.user_name, count: 0, privileged: 0, types: new Set() };
|
||||
acc[e.user_email].count += 1;
|
||||
acc[e.user_email].types.add(e.event_type);
|
||||
if (HIGH_SEVERITY.includes(e.event_type)) acc[e.user_email].privileged += 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const accounts = Object.entries(perUser);
|
||||
const busiest = accounts.slice().sort((a, b) => b[1].count - a[1].count)[0];
|
||||
const share = busiest ? Math.round((busiest[1].count / f.activity.length) * 100) : 0;
|
||||
|
||||
/* Bursts: more than three events from one account inside an hour. */
|
||||
const byHour = f.activity.reduce((acc, e) => {
|
||||
const key = `${e.user_email}|${String(e.created_date).slice(0, 13)}`;
|
||||
acc[key] = (acc[key] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
const bursts = Object.entries(byHour).filter(([, n]) => n > 3);
|
||||
|
||||
const privileged = f.activity.filter((e) => HIGH_SEVERITY.includes(e.event_type));
|
||||
const offHours = f.activity.filter((e) => {
|
||||
const hour = new Date(e.created_date).getHours();
|
||||
return hour < 6 || hour >= 22;
|
||||
});
|
||||
|
||||
const patterns = [
|
||||
share >= 50 && {
|
||||
tone: 'warning',
|
||||
title: `One account is ${share}% of all activity`,
|
||||
body: `${busiest[1].name || busiest[0]} accounts for ${busiest[1].count} of ${f.activity.length} events. Worth confirming that is a person and not an integration running under someone's credentials.`,
|
||||
},
|
||||
bursts.length && {
|
||||
tone: 'warning',
|
||||
title: `${plural(bursts.length, 'burst')} of rapid activity`,
|
||||
body: 'More than three actions from one account inside a single hour. Normal for batch work, worth a glance if it is not expected.',
|
||||
},
|
||||
offHours.length && {
|
||||
tone: 'info',
|
||||
title: `${plural(offHours.length, 'event')} outside working hours`,
|
||||
body: 'Logged before 06:00 or after 22:00 local time. Not a problem on its own; a pattern of privileged actions at those hours would be.',
|
||||
},
|
||||
f.activity24h.length === 0 && {
|
||||
tone: 'warning',
|
||||
title: 'Nothing in the last 24 hours',
|
||||
body: 'Expected for a seeded dataset. On a live platform, silence is an ingestion question rather than a quiet day.',
|
||||
},
|
||||
privileged.length > f.activity.length * 0.3 && {
|
||||
tone: 'warning',
|
||||
title: `Privileged actions are ${Math.round((privileged.length / f.activity.length) * 100)}% of the log`,
|
||||
body: 'A log dominated by hires and position changes usually means routine reads are not being captured, which weakens the audit trail.',
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
if (!patterns.length) {
|
||||
return doc(
|
||||
heading('Nothing out of pattern'),
|
||||
kpis([
|
||||
{ label: 'Events', value: f.activity.length },
|
||||
{ label: 'Accounts', value: accounts.length },
|
||||
{ label: 'Busiest share', value: `${share}%`, tone: 'success' },
|
||||
]),
|
||||
text('Activity is spread across accounts, there are no bursts, and privileged actions are a normal share of the log.')
|
||||
);
|
||||
}
|
||||
|
||||
return doc(
|
||||
heading(`${plural(patterns.length, 'pattern')} worth attention`),
|
||||
insights(patterns),
|
||||
heading('Per account'),
|
||||
table(
|
||||
[
|
||||
{ key: 'name', label: 'Account' },
|
||||
{ key: 'events', label: 'Events', align: 'right' },
|
||||
{ key: 'privileged', label: 'Privileged', align: 'right' },
|
||||
],
|
||||
accounts
|
||||
.sort((a, b) => b[1].count - a[1].count)
|
||||
.slice(0, 6)
|
||||
.map(([email, d]) => ({
|
||||
name: d.name || email,
|
||||
events: { value: d.count, tone: d.count === busiest[1].count ? 'info' : undefined },
|
||||
privileged: d.privileged ? { value: d.privileged, tone: 'warning' } : '0',
|
||||
}))
|
||||
),
|
||||
note('Deviations from this platform\'s own baseline, not accusations. Each one is a question to ask, and most have a dull answer.')
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Security insights — the audit trail judged as a control, not as a feed.
|
||||
*
|
||||
* The question a security reviewer actually asks is whether the log would stand
|
||||
* up: are privileged actions attributable, is coverage complete, is anything
|
||||
* unaccounted for. So this reports on the trail itself.
|
||||
*/
|
||||
const securityInsights = (f) => {
|
||||
const privileged = f.activity.filter((e) => HIGH_SEVERITY.includes(e.event_type));
|
||||
const attributed = privileged.filter((e) => e.user_email && e.user_name);
|
||||
const named = f.activity.filter((e) => e.user_email);
|
||||
const hires = f.activity.filter((e) => e.event_type === 'hire_candidate').length;
|
||||
|
||||
const checks = [
|
||||
{
|
||||
label: 'Privileged actions attributable',
|
||||
value: `${attributed.length}/${privileged.length}`,
|
||||
ok: privileged.length === attributed.length,
|
||||
note: privileged.length === attributed.length
|
||||
? 'Every hire and position change traces to a named account.'
|
||||
: `${privileged.length - attributed.length} cannot be traced to a named person.`,
|
||||
},
|
||||
{
|
||||
label: 'Events carry an actor',
|
||||
value: `${named.length}/${f.activity.length}`,
|
||||
ok: named.length === f.activity.length,
|
||||
note: named.length === f.activity.length
|
||||
? 'No anonymous entries in the log.'
|
||||
: `${f.activity.length - named.length} entries have no account attached.`,
|
||||
},
|
||||
{
|
||||
label: 'Hires reconcile with staff',
|
||||
value: `${hires}/${f.staff.length}`,
|
||||
ok: hires === f.staff.length,
|
||||
note: hires === f.staff.length
|
||||
? 'Every person on staff has a matching hire event.'
|
||||
: `${Math.abs(f.staff.length - hires)} on staff without a corresponding hire event, or the reverse.`,
|
||||
},
|
||||
{
|
||||
label: 'Recent coverage',
|
||||
value: `${f.activity24h.length} in 24h`,
|
||||
ok: f.activity24h.length > 0,
|
||||
note: f.activity24h.length
|
||||
? 'The log is being written to.'
|
||||
: 'No events in the last day — on a live platform, check ingestion before concluding it was quiet.',
|
||||
},
|
||||
{
|
||||
label: 'Interview integrity',
|
||||
value: `${f.interviews.length - f.flagged.length}/${f.interviews.length}`,
|
||||
ok: f.flagged.length === 0,
|
||||
note: f.flagged.length
|
||||
? `${plural(f.flagged.length, 'interview')} below full integrity — usually implausibly fast responses.`
|
||||
: 'No interview flagged for integrity.',
|
||||
},
|
||||
];
|
||||
|
||||
const passing = checks.filter((c) => c.ok).length;
|
||||
|
||||
return doc(
|
||||
heading(`Security posture: ${passing}/${checks.length} checks passing`),
|
||||
status(checks),
|
||||
heading('Privileged action log', `${plural(privileged.length, 'event')}`),
|
||||
privileged.length
|
||||
? timeline(
|
||||
privileged.slice(0, 6).map((e) => ({
|
||||
title: String(e.event_type).replace(/_/g, ' '),
|
||||
description: `${e.user_name || e.user_email}${e.details ? ` — ${e.details}` : ''}`,
|
||||
timestamp: new Date(e.created_date).toLocaleString(undefined, {
|
||||
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
|
||||
}),
|
||||
tone: MEDIUM_SEVERITY.includes(e.event_type) ? 'warning' : 'risk',
|
||||
}))
|
||||
)
|
||||
: text('No privileged actions in this log.'),
|
||||
text(
|
||||
passing === checks.length
|
||||
? 'The trail would hold up to review: every privileged action is attributable and the records reconcile.'
|
||||
: 'The failing checks are completeness problems. They do not mean something happened — they mean you could not prove it had not.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/* ── Free-text responders ───────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Free-text routing.
|
||||
*
|
||||
* Keyword matching, ordered most specific first, because a question mentioning
|
||||
* both "risk" and "candidates" is a risk question. The ordering *is* the logic
|
||||
* here, so each responder reads top to bottom as a priority list.
|
||||
*/
|
||||
const has = (q, ...words) => words.some((w) => q.includes(w));
|
||||
|
||||
export const respondControlCenter = (question, f) => {
|
||||
const q = question.toLowerCase();
|
||||
if (has(q, 'recommend', 'should', 'action', 'fix', 'improve')) return recommendations(f);
|
||||
if (has(q, 'unusual', 'anomal', 'suspicious')) return userActivityInsights(f);
|
||||
if (has(q, 'attention', 'urgent', 'blocked', 'stuck', 'wrong')) return attentionRequired(f);
|
||||
if (has(q, 'bottleneck', 'funnel', 'pipeline', 'conversion', 'drop')) return pipelineHealth(f);
|
||||
if (has(q, 'recommend', 'should', 'fix', 'improve', 'risk', 'next')) return recommendations(f);
|
||||
if (has(q, 'usage', 'summary', 'summarize', 'workforce', 'scale', 'how many')) return workforceSummary(f);
|
||||
if (has(q, 'operation', 'velocity', 'speed', 'throughput', 'time to hire', 'hiring')) return hiringOperations(f);
|
||||
if (has(q, 'unusual', 'anomal', 'suspicious')) return unusualActivity(f);
|
||||
return platformHealth(f);
|
||||
};
|
||||
|
||||
export const respondAdminCandidates = (question, f) => {
|
||||
const q = question.toLowerCase();
|
||||
if (has(q, 'gap', 'missing', 'incomplete', 'coverage', 'unscored')) return screeningGaps(f);
|
||||
if (has(q, 'risk', 'flag', 'concern', 'compliance')) return candidateRiskAnalysis(f);
|
||||
if (has(q, 'compare', 'top', 'best', 'strongest', 'shortlist')) return topCandidates(f);
|
||||
if (has(q, 'recommend', 'should', 'next', 'action', 'hire', 'who')) return hiringRecommendations(f);
|
||||
return recruitmentInsights(f);
|
||||
};
|
||||
|
||||
export const respondActivity = (question, f) => {
|
||||
const q = question.toLowerCase();
|
||||
if (has(q, 'user', 'who', 'active', 'behaviour', 'behavior', 'account', 'unusual')) return userActivityInsights(f);
|
||||
if (has(q, 'security', 'breach', 'attributab', 'compliance', 'integrity', 'trace')) return securityInsights(f);
|
||||
if (has(q, 'unusual', 'anomal', 'suspicious', 'odd', 'spike', 'burst')) return unusualActivity(f);
|
||||
if (has(q, 'user', 'who', 'account', 'behaviour', 'behavior', 'busiest')) return userActivityInsights(f);
|
||||
return auditSummary(f);
|
||||
};
|
||||
|
||||
@@ -321,15 +914,24 @@ export const respondActivity = (question, f) => {
|
||||
|
||||
export const CONTROL_CENTER_CAPABILITIES = [
|
||||
{ id: 'platform-health', label: 'Platform Health', icon: Gauge, run: platformHealth },
|
||||
{ id: 'workforce-summary', label: 'Workforce Summary', icon: Users, run: workforceSummary },
|
||||
{ id: 'hiring-operations', label: 'Hiring Operations', icon: Activity, run: hiringOperations },
|
||||
{ id: 'pipeline-health', label: 'Pipeline Health', icon: GitBranch, run: pipelineHealth },
|
||||
{ id: 'attention-required', label: 'Attention Required', icon: BellRing, run: attentionRequired },
|
||||
{ id: 'recommendations', label: 'Recommendations', icon: Lightbulb, run: recommendations },
|
||||
];
|
||||
|
||||
export const ADMIN_CANDIDATE_CAPABILITIES = [
|
||||
{ id: 'candidate-risk', label: 'Candidate Risk', icon: AlertTriangle, run: candidateRiskAnalysis },
|
||||
{ id: 'recruitment-insights', label: 'Recruitment Insights', icon: TrendingUp, run: recruitmentInsights },
|
||||
{ id: 'candidate-risk', label: 'Candidate Risk Analysis', icon: AlertTriangle, run: candidateRiskAnalysis },
|
||||
{ id: 'screening-gaps', label: 'Screening Gaps', icon: Filter, run: screeningGaps },
|
||||
{ id: 'hiring-recommendations', label: 'Hiring Recommendations', icon: Lightbulb, run: hiringRecommendations },
|
||||
{ id: 'top-candidates', label: 'Compare Top Candidates', icon: ArrowLeftRight, run: topCandidates },
|
||||
];
|
||||
|
||||
export const ACTIVITY_CAPABILITIES = [
|
||||
{ id: 'audit-summary', label: 'Audit Summary', icon: ScrollText, run: auditSummary },
|
||||
{ id: 'user-activity', label: 'User Activity Insights', icon: Eye, run: userActivityInsights },
|
||||
{ id: 'user-activity', label: 'User Activity', icon: Eye, run: userActivityInsights },
|
||||
{ id: 'unusual-activity', label: 'Unusual Activity', icon: AlertTriangle, run: unusualActivity },
|
||||
{ id: 'security-insights', label: 'Security Insights', icon: ShieldAlert, run: securityInsights },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user