feat(web): protect phone data and polish release UI

This commit is contained in:
Codex
2026-07-30 20:16:50 +08:00
parent 6ec8e4f6a6
commit d60cff1dd8
19 changed files with 800 additions and 396 deletions
+110 -15
View File
@@ -102,8 +102,20 @@ async function builtAssetHandler(request, response) {
phoneNumbers: [`+852 5550 10${Math.max(0, fixtureIndex)}`],
};
}
if (FLEET_FIXTURE && /^\/api\/v1\/instances\/[^/]+\/messages$/u.test(pathname))
body = { messages: [] };
if (FLEET_FIXTURE && /^\/api\/v1\/instances\/[^/]+\/messages$/u.test(pathname)) {
const fixtureIndex = FLEET_FIXTURE_ITEMS.findIndex((item) => pathname.includes(item.id));
body = {
messages: [
{
id: `sms-${Math.max(0, fixtureIndex)}`,
direction: 'incoming',
phoneNumber: `1390013900${Math.max(0, fixtureIndex)}`,
content: 'status',
timestamp: '2026-07-30T08:30:00.000Z',
},
],
};
}
if (
pathname === '/api/v1/jobs' ||
pathname === '/api/v1/audit' ||
@@ -134,7 +146,10 @@ async function builtAssetHandler(request, response) {
response.end(JSON.stringify({ error: 'Not Found', statusCode: 404 }));
return;
}
const relative = pathname.startsWith('/assets/') ? pathname.slice(1) : 'index.html';
const relative =
pathname.startsWith('/assets/') || pathname === '/favicon.svg'
? pathname.slice(1)
: 'index.html';
try {
const asset = await readFile(join(DIST, relative));
response.statusCode = 200;
@@ -144,6 +159,7 @@ async function builtAssetHandler(request, response) {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.svg': 'image/svg+xml; charset=utf-8',
}[extname(relative)] ?? 'application/octet-stream',
);
response.end(asset);
@@ -396,9 +412,15 @@ async function fleetLayout(cdp) {
const firstResource = document.querySelector('.fleet-resource-row');
const telemetry = document.querySelector('.fleet-card-telemetry');
const footer = document.querySelector('.fleet-card-footer');
const footerNumber = document.querySelector('.fleet-card-footer-primary > span:last-child');
const menuTrigger = document.querySelector('.fleet-card-menu-trigger');
const menuPanel = document.querySelector('.fleet-card-menu-panel');
const checkbox = document.querySelector('.fleet-card-select');
const hardware = document.querySelector('.fleet-card-hardware');
const phoneField = document.querySelector('.fleet-card-phone');
const phoneValue = document.querySelector('.fleet-card-phone-value');
const phoneToggle = document.querySelector('.fleet-phone-privacy-toggle');
const temperatureField = document.querySelector('.fleet-card-hardware > div:not(.fleet-card-phone)');
const bounds = (element) => {
if (!element) return null;
const rect = element.getBoundingClientRect();
@@ -406,12 +428,14 @@ async function fleetLayout(cdp) {
};
const hardwareEntries = [...document.querySelectorAll('.fleet-card-hardware > div')].map((item) => {
const label = item.querySelector('dt');
const value = item.querySelector('dd');
const value = item.querySelector('.fleet-card-phone-value') ?? item.querySelector('dd');
return {
labelHeight: label?.getBoundingClientRect().height ?? 0,
labelFits: label ? label.scrollWidth <= label.clientWidth : false,
valueHeight: value?.getBoundingClientRect().height ?? 0,
valueFits: value ? value.scrollWidth <= value.clientWidth : false,
valueClientWidth: value?.clientWidth ?? 0,
valueScrollWidth: value?.scrollWidth ?? 0,
};
});
const navigation = [...document.querySelectorAll('nav[aria-label="全局导航"] a')].map((item) => {
@@ -438,9 +462,16 @@ async function fleetLayout(cdp) {
firstResource: bounds(firstResource),
telemetry: bounds(telemetry),
footer: bounds(footer),
footerNumber: bounds(footerNumber),
footerNumberFits: footerNumber ? footerNumber.scrollWidth <= footerNumber.clientWidth : false,
menuTrigger: bounds(menuTrigger),
menuPanel: bounds(menuPanel),
checkbox: bounds(checkbox),
hardware: bounds(hardware),
phoneField: bounds(phoneField),
phoneValue: bounds(phoneValue),
phoneToggle: bounds(phoneToggle),
temperatureField: bounds(temperatureField),
hardwareEntries,
navigation,
};
@@ -575,6 +606,31 @@ try {
`document.querySelector('.fleet-card-version')?.textContent.trim() === 'SimAdmin 2.4.0'`,
`Fleet SimAdmin version did not synchronize at ${viewport.width}px`,
);
await eventually(
cdp,
`document.querySelector('.fleet-card-footer-primary')?.textContent.includes('139 •••• 9000')`,
`Fleet SMS phone did not load privately at ${viewport.width}px`,
);
if (viewport.width === 390) {
const branding = await evaluate(
cdp,
`(() => ({ title: document.title, favicon: new URL(document.querySelector('link[rel="icon"]').href).pathname, theme: document.querySelector('meta[name="theme-color"]').content }))()`,
);
assert.deepEqual(branding, {
title: 'SimAdmin Control · 多节点控制台',
favicon: '/favicon.svg',
theme: '#f3f0e7',
});
}
const privacyText = await evaluate(
cdp,
`(() => ({ text: document.body.textContent, toggleCount: document.querySelectorAll('.fleet-phone-privacy-toggle').length }))()`,
);
assert.equal(privacyText.toggleCount, FLEET_FIXTURE_ITEMS.length);
assert.match(privacyText.text, /\+852 •••• 0100/u);
assert.match(privacyText.text, /139 •••• 9000/u);
assert.doesNotMatch(privacyText.text, /\+852 5550 100/u);
assert.doesNotMatch(privacyText.text, /13900139000/u);
const layout = await fleetLayout(cdp);
assert.equal(
layout.scrollWidth <= layout.viewport,
@@ -585,7 +641,11 @@ try {
assert.equal(layout.groupDisplay, 'flex', `${viewport.width}px tag groups must use flex`);
assert.equal(layout.resourceDisplay, 'grid', `${viewport.width}px resource rows must use grid`);
assert.equal(layout.telemetryDisplay, 'grid', `${viewport.width}px telemetry must use grid`);
assert.equal(layout.footerDisplay, 'flex', `${viewport.width}px SMS footer must use flex`);
assert.equal(
layout.footerDisplay,
viewport.width >= 1280 ? 'grid' : 'flex',
`${viewport.width}px SMS footer layout`,
);
assert.equal(
layout.workspaceDisplay,
'grid',
@@ -604,7 +664,13 @@ try {
layout.firstResource &&
layout.telemetry &&
layout.footer &&
layout.menuTrigger,
layout.footerNumber &&
layout.menuTrigger &&
layout.hardware &&
layout.phoneField &&
layout.phoneValue &&
layout.phoneToggle &&
layout.temperatureField,
`${viewport.width}px Fleet information hierarchy must render`,
);
for (const [name, bounds] of [
@@ -651,16 +717,30 @@ try {
`${viewport.width}px card action trigger is too small: ${JSON.stringify(layout.menuTrigger)}`,
);
assert.equal(
layout.hardwareEntries.length === FLEET_FIXTURE_ITEMS.length * 2 &&
layout.hardwareEntries.every(
(entry) =>
entry.labelFits &&
entry.valueFits &&
entry.labelHeight <= 20 &&
entry.valueHeight <= 20,
),
layout.footerNumberFits,
true,
`${viewport.width}px hardware facts wrap or clip: ${JSON.stringify(layout.hardwareEntries)}`,
`${viewport.width}px latest SMS phone must remain fully visible`,
);
assert.equal(
Math.abs(layout.phoneToggle.width - 40) <= 0.5 &&
Math.abs(layout.phoneToggle.height - 40) <= 0.5,
true,
`${viewport.width}px privacy toggle must remain 40px: ${JSON.stringify(layout.phoneToggle)}`,
);
assert.equal(
layout.phoneField.left >= layout.hardware.left - 0.5 &&
layout.temperatureField.right <= layout.hardware.right + 0.5 &&
layout.phoneValue.right <= layout.phoneToggle.left + 0.5 &&
layout.phoneToggle.right <= layout.phoneField.right + 0.5 &&
layout.phoneField.right < layout.temperatureField.left,
true,
`${viewport.width}px phone and temperature geometry overlaps: ${JSON.stringify(layout)}`,
);
assert.equal(
layout.hardwareEntries.length === FLEET_FIXTURE_ITEMS.length * 2 &&
layout.hardwareEntries.every((entry) => entry.labelFits && entry.valueFits),
true,
`${viewport.width}px hardware facts overflow: ${JSON.stringify(layout.hardwareEntries)}`,
);
if (viewport.sidebarMode === 'left') {
assert.equal(
@@ -705,6 +785,21 @@ try {
await captureScreenshot(cdp, 'warm-fleet-390-cards.png');
}
if (viewport.width === 1440) {
await evaluate(cdp, `document.querySelector('.fleet-phone-privacy-toggle').click()`);
await eventually(
cdp,
`document.querySelector('.fleet-card')?.textContent.includes('+852 5550 100') && document.querySelector('.fleet-card')?.textContent.includes('13900139000')`,
'Fleet phone disclosure did not reveal both phone locations',
);
const disclosedText = await evaluate(
cdp,
`[...document.querySelectorAll('.fleet-card')].map((card) => card.textContent)`,
);
assert.match(disclosedText[0], /\+852 5550 100/u);
assert.match(disclosedText[0], /13900139000/u);
assert.doesNotMatch(disclosedText[1], /\+852 5550/u);
assert.doesNotMatch(disclosedText[1], /1390013900/u);
await evaluate(cdp, `document.querySelector('.fleet-phone-privacy-toggle').click()`);
await evaluate(cdp, `document.querySelector('.fleet-card-menu-trigger').click()`);
await eventually(
cdp,