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 info — Position 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 banner — The 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 banner — Limit 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 default — Analytics 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 out — Use .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.