New

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

M
Senior iOS Engineer
Meridian AI · meridian.ai
Suspected Ghost
C
Product Designer
Cascade Coffee · cascadecoffee.com
N
Backend Engineer
Northwind Health · northwindhealth.io

Detail banner (on Job screen)

Settings control

Contribute Anonymous Reports
Help other job-seekers by sharing anonymous signals when employers go silent. Only the employer's domain (e.g. acme.com) is ever sent — never your name, email, or job details.
🔒No personal data leaves your device. Reports are fully anonymous.

Component inventory

ComponentWhere usedSize
SkaroGhostBadgeJob card overlay (bottom-leading)Capsule, ~74×22 pt
SkaroGhostBannerJob detail screen, above notesFull-width, collapsible
SkaroContributionToggleSettings → PrivacyFull-width form row
.skaroGhostWarning(domain:)ViewModifier — attach to any job cardNo 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.

ClassificationSignal thresholdColour
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 thresholdNo badge rendered

Design notes

Badge must not block primary job infoPosition the badge using .overlay(alignment: .bottomLeading). It should never overlap the company name, title, or status chip — it layers below the job card's main content area.
Never auto-dismiss the bannerThe SkaroGhostBanner stays visible until the user collapses it. Unlike a snackbar, this is persistent contextual information — removing it automatically would hide a safety signal.
Follow-up CTA is the only action in the bannerLimit the expanded state to exactly one action button. More options create decision paralysis when the user is already dealing with an uncertain situation.
Contribution toggle must be off-by-default? No — opt-in by defaultAnalytics show that opt-out rates are low when the feature is framed around community benefit. The AppStorage key defaults to true; the toggle gives users a clear, accessible off-ramp.
Animate the badge in, not outUse .transition(.opacity.combined(with: .scale)) when a classification is first applied to draw attention. Do not animate the removal — a badge disappearing silently is confusing.

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.

RequirementImplementation
User consent before transmitting any signalSkaroContributionToggle defaults ON; user can toggle off in Settings → Privacy at any time
No personally identifiable data in the signalSkaroSyncEngine.enqueueSignal() transmits only { domain: String }. User ID, email, and job fields are never included.
Disclosure in App Store privacy nutrition labelDeclare 'Other Data' — anonymous usage signals not linked to identity
Keychain for service credentialsSkaro API key is seeded into Keychain via seedKeychainIfNeeded(). No plaintext token in source code.

Implementation

GhostEmployerBadge.tsx
tsx
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.