Files
multi-simadmin/docs/superpowers/plans/2026-07-30-phone-privacy-release-polish.md

14 KiB

Phone Privacy And Release Polish Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make node phone data private by default, align the phone field, unify browser branding, and remove the approved redundant release copy across the application.

Architecture: Add one pure phone-display formatter beside the Fleet feature, then keep per-instance disclosure state inside FleetPage. Reuse the existing icon component and card DOM rather than introducing a new component system. Treat favicon and release-copy changes as static branding/content changes with source-level and browser-level regression coverage.

Tech Stack: React 19, TypeScript, Vitest, Testing Library, CSS, Vite, SVG, Chrome DevTools Protocol E2E.

Global Constraints

  • Both the card's own phone number and latest-SMS phone number are masked by default.
  • One per-card eye button reveals or hides both phone locations for only that instance.
  • Disclosure state is memory-only and resets after a page reload.
  • Masked international example: +852 •••• 0100; masked local example: 138 •••• 5678.
  • The eye button has a stable 40-by-40-pixel hit target and cannot move the temperature field.
  • The favicon uses the warm yellow and brown header-brand language.
  • The release is non-commercial; animal-island-ui attribution and CC BY-NC 4.0 remain visible and linked.
  • Do not change APIs, card column counts, navigation, the left Fleet sidebar position, instance-detail phone displays, or SMS conversation displays.

Task 1: Pure Phone Masking

Files:

  • Create: apps/web/src/fleet/phone-privacy.ts
  • Create: apps/web/src/fleet/phone-privacy.test.ts

Interfaces:

  • Produces: maskPhoneNumber(phoneNumber: string): string.

  • Produces: formatPhoneNumbers(phoneNumbers: readonly string[], revealed: boolean): string.

  • Step 1: Write failing formatter tests

expect(maskPhoneNumber('+852 5550 0100')).toBe('+852 •••• 0100');
expect(maskPhoneNumber('13812345678')).toBe('138 •••• 5678');
expect(formatPhoneNumbers(['+852 5550 0100', '13812345678'], false)).toBe(
  '+852 •••• 0100、138 •••• 5678',
);
expect(formatPhoneNumbers(['+852 5550 0100'], true)).toBe('+852 5550 0100');
  • Step 2: Verify the tests fail because the module does not exist

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/fleet/phone-privacy.test.ts

Expected: FAIL because phone-privacy.ts or its exports are missing.

  • Step 3: Implement the minimal pure formatter
const MASK = '••••';

export function maskPhoneNumber(phoneNumber: string): string {
  const trimmed = phoneNumber.trim();
  const digits = trimmed.replace(/\D/gu, '');
  if (digits.length <= 4) return MASK;
  const suffix = digits.slice(-4);
  if (trimmed.startsWith('+')) {
    const groupedPrefix = /^\+\d{1,3}(?=[\s-])/u.exec(trimmed)?.[0];
    const prefix = groupedPrefix ?? `+${digits.slice(0, 3)}`;
    return `${prefix} ${MASK} ${suffix}`;
  }
  return `${digits.slice(0, 3)} ${MASK} ${suffix}`;
}

export function formatPhoneNumbers(
  phoneNumbers: readonly string[],
  revealed: boolean,
): string {
  return phoneNumbers.map((number) => (revealed ? number : maskPhoneNumber(number))).join('、');
}
  • Step 4: Run the formatter tests and verify they pass

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/fleet/phone-privacy.test.ts

Expected: PASS.

  • Step 5: Commit the formatter

Run: git add apps/web/src/fleet/phone-privacy.ts apps/web/src/fleet/phone-privacy.test.ts && git commit -m "feat(web): add phone privacy formatter"

Task 2: Per-Card Disclosure And Stable Phone Layout

Files:

  • Modify: apps/web/src/fleet/fleet-page.test.tsx
  • Modify: apps/web/src/fleet/fleet-page.tsx
  • Modify: apps/web/src/ui/icon.tsx
  • Modify: apps/web/src/styles.css

Interfaces:

  • Consumes: formatPhoneNumbers(phoneNumbers, revealed) from Task 1.

  • Produces: per-instance revealedPhoneIds: Set<string> state in FleetPage.

  • Produces: IconName values eye and eye-off through the existing icon component.

  • Step 1: Write failing card behavior tests

expect(within(firstCard).getByText('+852 •••• 0100')).toBeTruthy();
expect(within(firstCard).queryByText('+852 5550 0100')).toBeNull();
expect(within(firstCard).getByText('+852 •••• 0199')).toBeTruthy();

await user.click(within(firstCard).getByRole('button', { name: /显示.*手机号/u }));
expect(within(firstCard).getByText('+852 5550 0100')).toBeTruthy();
expect(within(firstCard).getByText('+852 5550 0199')).toBeTruthy();
expect(within(secondCard).queryByText('+852 5550 0200')).toBeNull();

await user.click(within(firstCard).getByRole('button', { name: /隐藏.*手机号/u }));
expect(within(firstCard).queryByText('+852 5550 0100')).toBeNull();

Also assert that a card without either phone source has no disclosure button.

  • Step 2: Run the Fleet test and verify privacy assertions fail

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/fleet/fleet-page.test.tsx

Expected: FAIL because raw numbers are still rendered and no disclosure button exists.

  • Step 3: Add eye icons and per-instance disclosure state
const [revealedPhoneIds, setRevealedPhoneIds] = useState<ReadonlySet<string>>(
  () => new Set(),
);

function togglePhoneVisibility(instanceId: string): void {
  setRevealedPhoneIds((current) => {
    const next = new Set(current);
    if (next.has(instanceId)) next.delete(instanceId);
    else next.add(instanceId);
    return next;
  });
}

Render one .fleet-card-phone-toggle button in the hardware phone field when either phone source exists. Give it dynamic aria-label, aria-pressed, title, and eye/eye-off icon. Use the same revealed boolean for the card hardware value and latest-SMS phone text.

  • Step 4: Stabilize the phone and temperature grid
.fleet-card-hardware {
  grid-template-columns: minmax(0, 1.35fr) minmax(5rem, 0.65fr);
}

.fleet-card-phone-value {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 2.5rem;
  align-items: center;
  gap: 0.35rem;
}

.fleet-card-phone-toggle {
  inline-size: 2.5rem;
  block-size: 2.5rem;
}

Keep text within min-width: 0, use tabular numerals, and prevent the button from shrinking.

  • Step 5: Run Fleet behavior tests and verify they pass

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/fleet/phone-privacy.test.ts src/fleet/fleet-page.test.tsx

Expected: PASS.

  • Step 6: Commit card privacy behavior

Run: git add apps/web/src/fleet/fleet-page.tsx apps/web/src/fleet/fleet-page.test.tsx apps/web/src/ui/icon.tsx apps/web/src/styles.css && git commit -m "feat(web): protect fleet phone numbers"

Task 3: Browser Tab Branding

Files:

  • Create: apps/web/public/favicon.svg
  • Create: apps/web/src/app-branding.test.ts
  • Modify: apps/web/index.html
  • Modify: scripts/real-browser-e2e.mjs

Interfaces:

  • Produces: /favicon.svg with a 64-by-64 warm signal badge.

  • Produces: title SimAdmin Control · 多节点控制台 and theme color #f3f0e7.

  • Step 1: Write failing branding source and browser assertions

expect(indexHtml).toContain('href="/favicon.svg"');
expect(indexHtml).toContain('content="#f3f0e7"');
expect(indexHtml).toContain('<title>SimAdmin Control · 多节点控制台</title>');
expect(faviconSvg).toContain('#f6c95f');
expect(faviconSvg).toContain('#725d42');

Add E2E assertions for document.title, the favicon link pathname, and the theme-color value.

  • Step 2: Run branding tests and verify they fail

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/app-branding.test.ts

Expected: FAIL on the old inline blue favicon, dark theme color, and corrupted title.

  • Step 3: Add the static warm favicon and update index.html

Create a compact SVG with a warm yellow rounded square, brown border, and three ascending rounded bars. Replace the data-URI favicon, theme color, and title with the exact values above.

  • Step 4: Run branding tests and verify they pass

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/app-branding.test.ts

Expected: PASS.

  • Step 5: Commit browser branding

Run: git add apps/web/index.html apps/web/public/favicon.svg apps/web/src/app-branding.test.ts scripts/real-browser-e2e.mjs && git commit -m "feat(web): align browser branding"

Task 4: Release Copy Cleanup

Files:

  • Modify: apps/web/src/app-shell.integration.test.tsx
  • Modify: apps/web/src/app-shell.tsx
  • Modify: apps/web/src/fleet/fleet-page.test.tsx
  • Modify: apps/web/src/fleet/fleet-page.tsx
  • Modify: apps/web/src/automation/automation-page.test.tsx
  • Modify: apps/web/src/automation/automation-page.tsx
  • Modify: apps/web/src/settings/instance-settings-page.test.tsx
  • Modify: apps/web/src/settings/instance-settings-page.tsx
  • Modify: apps/web/src/auth/console-auth.test.tsx
  • Modify: apps/web/src/auth/console-auth.tsx
  • Modify: apps/web/src/auth/console-auth-settings.tsx
  • Modify: apps/web/src/styles.css

Interfaces:

  • Consumes: the exact removal and retention lists in the design specification.

  • Produces: a shorter linked footer attribution that still names animal-island-ui and CC BY-NC 4.0.

  • Step 1: Add failing content assertions

Assert that the approved redundant strings are absent and that required copy remains:

expect(screen.queryByText('NODE DIRECTORY')).toBeNull();
expect(screen.queryByText(/LIVE NODE MATRIX/u)).toBeNull();
expect(screen.queryByText(/按北京时间(UTC\+8)统一调度/u)).toBeNull();
expect(screen.queryByText('SYSTEM SETTINGS')).toBeNull();
expect(screen.queryByText('MULTI SIMADMIN')).toBeNull();
expect(screen.getByText(/HTTP/u)).toBeTruthy();
expect(screen.getByRole('link', { name: 'animal-island-ui' })).toBeTruthy();
expect(screen.getByRole('link', { name: 'CC BY-NC 4.0' })).toBeTruthy();
  • Step 2: Run the affected component tests and verify they fail

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/app-shell.integration.test.tsx src/fleet/fleet-page.test.tsx src/automation/automation-page.test.tsx src/settings/instance-settings-page.test.tsx src/auth/console-auth.test.tsx

Expected: FAIL while the redundant strings remain or the compact footer is absent.

  • Step 3: Remove only the approved visible copy

Delete the approved header descriptions, English eyebrow labels, duplicate Fleet counts and status explanations, Automation empty-state descriptions and drawer eyebrow, Settings descriptions, and login eyebrow/description. Keep loading, error, risk, security, timezone-near-Cron, SMS preservation, and empty-state titles.

Replace the footer with two explicit links:

<p>
  UI: <a href="https://github.com/guokaigdg/animal-island-ui">animal-island-ui</a>
  <span aria-hidden="true"> · </span>
  <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC 4.0</a>
</p>
  • Step 4: Remove styles made unused by this change

Remove selectors only when their final DOM owner was deleted, including the obsolete brand subtitle and removed eyebrow-specific declarations. Do not refactor unrelated style cascades.

  • Step 5: Run affected tests and verify they pass

Run: corepack pnpm --filter @multi-simadmin/web exec vitest run src/app-shell.integration.test.tsx src/fleet/fleet-page.test.tsx src/automation/automation-page.test.tsx src/settings/instance-settings-page.test.tsx src/auth/console-auth.test.tsx

Expected: PASS.

  • Step 6: Commit release copy cleanup

Run: git add apps/web/src/app-shell.tsx apps/web/src/app-shell.integration.test.tsx apps/web/src/fleet/fleet-page.tsx apps/web/src/fleet/fleet-page.test.tsx apps/web/src/automation/automation-page.tsx apps/web/src/automation/automation-page.test.tsx apps/web/src/settings/instance-settings-page.tsx apps/web/src/settings/instance-settings-page.test.tsx apps/web/src/auth/console-auth.tsx apps/web/src/auth/console-auth.test.tsx apps/web/src/auth/console-auth-settings.tsx apps/web/src/styles.css && git commit -m "refactor(web): trim release copy"

Task 5: Responsive Browser Verification And Release Gates

Files:

  • Modify: scripts/real-browser-e2e.mjs

Interfaces:

  • Consumes: .fleet-card-phone-value, .fleet-card-phone-toggle, page title, favicon, and the existing five viewport fixtures.

  • Produces: regression assertions and screenshots for 390, 768, 1024, 1440, and 1920 pixels.

  • Step 1: Extend E2E geometry and privacy assertions

For every viewport, assert that raw phone fixtures are absent before disclosure, the masked value is present, the toggle is 40 by 40 pixels, and the phone value/toggle/temperature rectangles remain inside the hardware panel without overlap. At 1440 pixels, click the first card toggle and assert both raw numbers appear only in that card.

  • Step 2: Run Chrome E2E and correct only failures caused by this feature

Run: $env:E2E_SCREENSHOT_DIR='C:\Users\86135\Downloads\multi-simadmin\artifacts\phone-privacy-release'; corepack pnpm run test:e2e:browser

Expected: PASS across all five viewports.

  • Step 3: Rebuild the production web bundle after E2E cleanup

Run: corepack pnpm --filter @multi-simadmin/web build

Expected: Vite build succeeds and recreates apps/web/dist.

  • Step 4: Run changed-scope and static quality gates

Run:

corepack pnpm --filter @multi-simadmin/web test
corepack pnpm run typecheck
corepack pnpm run lint
corepack pnpm run format:check
git diff --check

Expected: Web tests, typecheck, lint, formatting, and diff checks pass.

  • Step 5: Verify the running local service

Run:

Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8789/
Invoke-WebRequest -UseBasicParsing http://127.0.0.1:8789/api/v1/instances

Expected: both requests return HTTP 200.

  • Step 6: Commit E2E coverage after all gates pass

Run: git add scripts/real-browser-e2e.mjs docs/superpowers/plans/2026-07-30-phone-privacy-release-polish.md && git commit -m "test(web): verify phone privacy release polish"