diff --git a/docs/designs/ui04-implementation/actual/server/server-desktop-heart.png b/docs/designs/ui04-implementation/actual/server/server-desktop-heart.png
new file mode 100644
index 0000000..51d0afb
Binary files /dev/null and b/docs/designs/ui04-implementation/actual/server/server-desktop-heart.png differ
diff --git a/docs/designs/ui04-implementation/actual/server/server-desktop-widget.png b/docs/designs/ui04-implementation/actual/server/server-desktop-widget.png
new file mode 100644
index 0000000..61a4ab5
Binary files /dev/null and b/docs/designs/ui04-implementation/actual/server/server-desktop-widget.png differ
diff --git a/docs/designs/ui04-implementation/server-verify.spec.ts b/docs/designs/ui04-implementation/server-verify.spec.ts
new file mode 100644
index 0000000..1014012
--- /dev/null
+++ b/docs/designs/ui04-implementation/server-verify.spec.ts
@@ -0,0 +1,137 @@
+// 临时服务器验证规格(UI-04 部署后真机验证,采集后删除):
+// 本地浏览器 → 测试服务器(219.157.255.213)真实链路:
+// · v2 loader/iframe/ov feed/OSS 图片全部来自服务器与真实 OSS;
+// · 覆盖:195×280 固定窗、照片渲染、连续上滚、收起展开、点击深链弹窗、
+// 真实点赞(ov 公开 API)→ WS 经 OpenResty 32006 代理 → 心形气泡、
+// viewer 相册页与管理后台登录页可达。
+import { createServer, type Server } from 'node:http';
+import { expect, test, type Page } from '@playwright/test';
+
+const HOST_PORT = 3997;
+const SERVER_VIEWER = 'http://219.157.255.213:32006';
+const SERVER_ADMIN = 'http://219.157.255.213:32005';
+const SLOT_TOKEN = 'dg0mo4i5gov39g1vv0wgisgb';
+const ALBUM_TOKEN = 'nzmk2kr4trzemiwmv65uwxva';
+const OUT = 'docs/designs/ui04-implementation/actual/server';
+
+const server: Server = createServer((_req, res) => {
+ res.setHeader('content-type', 'text/html; charset=utf-8');
+ res.end(`
+
+ UI-04 server host
+
+ Host page (server-deployed v2 widget)
+
+ `);
+});
+
+async function openHost(page: Page): Promise {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ await page.goto(`http://127.0.0.1:${HOST_PORT}`);
+}
+
+test.describe.serial('UI-04 server deployment verification', () => {
+ test.beforeAll(async () => {
+ await new Promise((resolve) => {
+ server.once('error', () => resolve()); // 残留进程容错
+ server.listen(HOST_PORT, '127.0.0.1', () => resolve());
+ });
+ });
+ test.afterAll(() => server.close());
+
+ test('entries reachable: viewer album page + admin login + loader artifact', async ({ page, request }) => {
+ const viewer = await page.goto(`${SERVER_VIEWER}/a/${ALBUM_TOKEN}`);
+ expect(viewer?.status()).toBe(200);
+ const loader = await request.get(`${SERVER_VIEWER}/embed/v2/loader.js`);
+ expect(loader.status()).toBe(200);
+ expect((await loader.text()).length).toBeGreaterThan(1000);
+ const admin = await page.goto(`${SERVER_ADMIN}/admin/`);
+ expect(admin?.status()).toBe(200);
+ });
+
+ test('v2 widget on server: 195x280, real photos render, auto scrolls', async ({ page }) => {
+ await openHost(page);
+ const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
+ await frame.waitFor({ timeout: 15_000 });
+ const wrapper = frame.locator('..');
+ await expect
+ .poll(async () => Math.round((await wrapper.boundingBox())?.width ?? 0), { timeout: 10_000 })
+ .toBe(195);
+ await expect
+ .poll(async () => Math.round((await wrapper.boundingBox())?.height ?? 0))
+ .toBe(280);
+ const embedFrame = page.frames().find((f) => f.url().includes('v=2'));
+ expect(embedFrame).toBeDefined();
+ // 真实 OSS 图片加载出真实尺寸(等待首图 complete)。
+ await expect
+ .poll(
+ async () =>
+ embedFrame!.locator('.stream-photo__img').first().evaluate((el) => (el as HTMLImageElement).complete),
+ { timeout: 20_000 },
+ )
+ .toBe(true);
+ // 自动上滚:2s 位移 ~32px(transform 更负)。
+ await page.waitForTimeout(1_500);
+ const t1 = await embedFrame!.locator('.stream-track').evaluate((el) => el.style.transform);
+ await page.waitForTimeout(2_000);
+ const t2 = await embedFrame!.locator('.stream-track').evaluate((el) => el.style.transform);
+ expect(t2).not.toBe(t1);
+ await page.screenshot({ path: `${OUT}/server-desktop-widget.png` });
+ });
+
+ test('collapse via in-iframe button, expand via host handle', async ({ page }) => {
+ await openHost(page);
+ const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
+ await frame.waitFor({ timeout: 15_000 });
+ const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]');
+ await inside.getByRole('button', { name: /收起照片窗|Collapse photo window/ }).click();
+ await expect(frame).toBeHidden();
+ await expect(page.locator('[role="button"]').first()).toBeVisible();
+ await page.locator('[role="button"]').first().click();
+ await expect(frame).toBeVisible();
+ });
+
+ test('photo click opens server viewer deep link', async ({ page }) => {
+ await openHost(page);
+ const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
+ await frame.waitFor({ timeout: 15_000 });
+ const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]');
+ const box = await frame.boundingBox();
+ await page.mouse.move((box!.x ?? 0) + 90, (box!.y ?? 0) + 140); // 悬停冻结
+ await page.waitForTimeout(500);
+ const target = inside.locator('a.stream-photo').first();
+ const photoId = await target.getAttribute('data-photo-id');
+ const popupPromise = page.waitForEvent('popup', { timeout: 10_000 });
+ await target.click();
+ const popup = await popupPromise;
+ expect(popup.url()).toContain(`219.157.255.213:32006/a/${ALBUM_TOKEN}/p/${photoId}`);
+ await popup.close();
+ });
+
+ test('real like via ov API → WS through 32006 proxy → heart in widget', async ({ page }) => {
+ await openHost(page);
+ const frame = page.locator('iframe[src*="/embed/"][src*="v=2"]');
+ await frame.waitFor({ timeout: 15_000 });
+ const inside = page.frameLocator('iframe[src*="/embed/"][src*="v=2"]');
+ const box = await frame.boundingBox();
+ await page.mouse.move((box!.x ?? 0) + 90, (box!.y ?? 0) + 140);
+ await page.waitForTimeout(800);
+ const embedFrame = page.frames().find((f) => f.url().includes('v=2'))!;
+ const photoId = await embedFrame.locator('a.stream-photo').first().getAttribute('data-photo-id');
+ const like = await page.request.post(
+ `${SERVER_VIEWER}/v1/public/albums/${ALBUM_TOKEN}/photos/${photoId}/likes`,
+ {
+ headers: { 'content-type': 'application/json' },
+ data: {
+ sessionId: `ui04-server-${Date.now()}`,
+ batchId: `batch-server-${Date.now()}`,
+ delta: 2,
+ },
+ },
+ );
+ expect(like.status()).toBeLessThan(300);
+ // WS 经 OpenResty 32006 反代(升级头)到达 iframe → 真实 burst → 心形。
+ await expect(inside.locator('.heart').first()).toBeVisible({ timeout: 8_000 });
+ await page.screenshot({ path: `${OUT}/server-desktop-heart.png` });
+ });
+});