Ghost Employer Tracking
The Skaro system surfaces employers with a history of going silent after interviews. A compact badge overlays job cards; an expandable banner on the detail screen provides context and a follow-up CTA. An opt-in toggle in Settings lets users contribute anonymous domain signals to the shared registry.
Preview
Detail banner (on Job screen)
Settings control
Component inventory
| Component | Where used | Size |
|---|---|---|
| SkaroGhostBadge | Job card overlay (bottom-leading) | Capsule, ~74×22 pt |
| SkaroGhostBanner | Job detail screen, above notes | Full-width, collapsible |
| SkaroContributionToggle | Settings → Privacy | Full-width form row |
| .skaroGhostWarning(domain:) | ViewModifier — attach to any job card | No visual footprint when domain is clean |
Classification system
The local SkaroStore holds a keyed dictionary of domain → classification. The registry is populated by periodic background fetches from the Skaro API and stored in a SwiftData entity. No personal data is ever part of the domain record.
| Classification | Signal threshold | Colour |
|---|---|---|
| suspected | ≥ 3 applicants went silent in 90 days | --status-warning (amber) |
| confirmedGhost | ≥ 10 applicants confirmed ghost in 30 days | --status-danger (red) |
| (none) | No data or below threshold | No badge rendered |
Design notes
Privacy & App Store compliance
Skaro must comply with App Store Guideline 5.1.1 (data collection and use). The outbound signal contains only the employer's domain name — no user identifier, job ID, or personal data is ever transmitted.
| Requirement | Implementation |
|---|---|
| User consent before transmitting any signal | SkaroContributionToggle defaults ON; user can toggle off in Settings → Privacy at any time |
| No personally identifiable data in the signal | SkaroSyncEngine.enqueueSignal() transmits only { domain: String }. User ID, email, and job fields are never included. |
| Disclosure in App Store privacy nutrition label | Declare 'Other Data' — anonymous usage signals not linked to identity |
| Keychain for service credentials | Skaro API key is seeded into Keychain via seedKeychainIfNeeded(). No plaintext token in source code. |
Implementation
import { useState } from "react";
type GhostClassification = "suspected" | "confirmed";
const GHOST_META = {
suspected: {
label: "Suspected Ghost",
detail: "This employer has gone silent on multiple applicants. Consider following up or deprioritising.",
colorVar: "var(--status-warning)",
},
confirmed: {
label: "Confirmed Ghost",
detail: "Community reports confirm this employer consistently stops responding after first-round interviews.",
colorVar: "var(--status-danger)",
},
};
// Compact capsule — overlay on job cards
export function GhostBadge({ cls }: { cls: GhostClassification }) {
const m = GHOST_META[cls];
return (
<span
role="status"
aria-label={`Ghost employer warning: ${m.label}`}
style={{
display: "inline-flex", alignItems: "center", gap: 4,
padding: "3px 8px", borderRadius: 99, fontSize: 11, fontWeight: 700,
background: `rgb(${m.colorVar} / 0.12)`,
color: `rgb(${m.colorVar})`,
border: `0.5px solid rgb(${m.colorVar} / 0.3)`,
}}
>
⚠ {m.label}
</span>
);
}
// Expandable banner — detail screen
export function GhostBanner({
cls,
onSetDeadline,
}: {
cls: GhostClassification;
onSetDeadline?: () => void;
}) {
const [expanded, setExpanded] = useState(false);
const m = GHOST_META[cls];
return (
<div
role="region"
aria-label={`Ghost employer warning: ${m.label}`}
style={{
borderRadius: 12,
border: `0.5px solid rgb(${m.colorVar} / 0.3)`,
background: `rgb(${m.colorVar} / 0.06)`,
overflow: "hidden",
}}
>
<button
aria-expanded={expanded}
onClick={() => setExpanded(v => !v)}
style={{
width: "100%", display: "flex", alignItems: "center", gap: 8,
padding: "10px 14px", background: "transparent", border: "none",
cursor: "pointer", color: `rgb(${m.colorVar})`,
fontSize: 12, fontWeight: 700,
}}
>
⚠ <span style={{ flex: 1, textAlign: "left" }}>{m.label}</span>
<span style={{ fontSize: 10, opacity: 0.6 }}>{expanded ? "▲" : "▼"}</span>
</button>
{expanded && (
<div style={{ padding: "0 14px 14px", borderTop: `0.5px solid rgb(${m.colorVar} / 0.3)` }}>
<p style={{ fontSize: 12, color: "rgb(var(--text-secondary))", margin: "10px 0 10px", lineHeight: 1.6 }}>
{m.detail}
</p>
{onSetDeadline && (
<button onClick={onSetDeadline} style={{
display: "flex", alignItems: "center", gap: 6, padding: "5px 12px",
borderRadius: 8, border: "none",
background: `rgb(${m.colorVar} / 0.1)`,
color: `rgb(${m.colorVar})`,
fontSize: 12, fontWeight: 600, cursor: "pointer",
}}>
📅 Set 14-day follow-up reminder
</button>
)}
</div>
)}
</div>
);
}
// Settings opt-in toggle
export function GhostContributionToggle({
enabled,
onChange,
}: {
enabled: boolean;
onChange: (v: boolean) => void;
}) {
return (
<div style={{ display: "flex", alignItems: "flex-start", gap: 12, padding: "14px 0" }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: "rgb(var(--text-primary))", marginBottom: 4 }}>
Contribute Anonymous Reports
</div>
<div style={{ fontSize: 12, color: "rgb(var(--text-secondary))", lineHeight: 1.6, marginBottom: 8 }}>
Help other job-seekers by sharing anonymous signals when employers go silent.
Only the employer's domain (e.g. acme.com) is ever sent.
</div>
<div style={{ fontSize: 11, color: "rgb(var(--text-tertiary))", display: "flex", alignItems: "center", gap: 4 }}>
🔒 No personal data leaves your device.
</div>
</div>
<input type="checkbox" checked={enabled} onChange={e => onChange(e.target.checked)}
style={{ marginTop: 4, accentColor: "rgb(var(--accent))", width: 18, height: 18, cursor: "pointer" }}
aria-label="Contribute anonymous ghost employer reports"
/>
</div>
);
}Accessibility
- →The badge requires an accessibilityLabel combining both 'Ghost employer warning' and the classification label — never rely on colour alone, which is invisible to colour-blind users and screen readers.
- →The GhostBanner expand/collapse button requires aria-expanded (web) or isExpanded in accessibilityElement (iOS). VoiceOver must announce the state change on toggle.
- →The 'Set 14-day follow-up' button should post an accessibility announcement confirming the reminder was set (UIAccessibility.post notification on iOS).
- →The contribution toggle must have an aria-label / accessibilityLabel that names the feature: 'Contribute anonymous ghost employer reports'. The description text may be rendered as accessibilityHint.
- →Do not remove the badge on scroll — it must remain visible whenever the job card is on screen so users who navigate by VoiceOver swipe can encounter it in document order.