docs: plan phone privacy release polish
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
# 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**
|
||||
|
||||
```ts
|
||||
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**
|
||||
|
||||
```ts
|
||||
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**
|
||||
|
||||
```tsx
|
||||
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**
|
||||
|
||||
```tsx
|
||||
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**
|
||||
|
||||
```css
|
||||
.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**
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
<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:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```powershell
|
||||
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"`
|
||||
Reference in New Issue
Block a user