Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@ MinIO 上の Parquet レイク(+ 最新値は NATS KV)にストア、REST +

## 👀 見た目と最初の1コマンド

<!--
📸 スクリーンショット / デモ GIF プレースホルダ(#156)
実 UI のキャプチャ(推奨3点: /resources ツリー・/points/{id} 時系列・制御ダイアログ)を
docs/images/ 配下に追加し、ここに ![説明](docs/images/xxx.png) で差し込んでください。
※ 本リポジトリの自動化環境では実 UI を起動できないため画像は未添付です(メンテナ対応)。
-->

> 📸 **スクリーンショット準備中** — 中心となる画面は `/resources`(設備ツリー)→ `/points/{id}`(最新値+履歴グラフ)
> → 制御ダイアログ の3つです。ローカルで `make local-up-oss` 後にブラウザで確認できます。
運用者ホーム(鮮度サマリ+要対応ポイント)・リソースエクスプローラ(設備ツリー+詳細)・ポイント詳細
(最新値+履歴グラフ)の3画面:

![運用者ホーム — テレメトリ鮮度サマリ(最新/鮮度切れ/欠測)と要対応ポイント一覧](docs/screenshots/operator-home.png)

![リソースエクスプローラ — 建物→フロア→空間→機器→ポイントの設備ツリーと詳細ペイン](docs/screenshots/resource-explorer.png)

![ポイント詳細 — 最新値・鮮度バッジ・テレメトリ履歴グラフ(期間/粒度セレクタ付き)](docs/screenshots/point-detail.png)

> 上記は route-mock した実 UI をブラウザ(Playwright)で撮影したものです(サンプルツイン相当のデータ、
> `web-client/e2e/capture-readme-screenshots.spec.ts` で再生成可能)。実データでの動作は `make demo`
> 後にブラウザで確認できます。

**5分で動かす(最短・1コマンド):**

Expand Down
Binary file added docs/screenshots/operator-home.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/point-detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/resource-explorer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions web-client/e2e/capture-readme-screenshots.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { test, type Page } from "@playwright/test";
import { loginAs } from "./support/auth";
import {
fulfillJson,
isoSecondsAgo,
mockBuildings,
mockDevices,
mockFloors,
mockLatestTelemetry,
mockPoints,
mockSpaces,
} from "./support/mock";

// Capture the README screenshots (#156) from the real app driven against a route-mocked API — no
// backend required. Run explicitly: npx playwright test e2e/capture-readme-screenshots.spec.ts
// PNGs land in ../docs/screenshots/. Kept in-repo so the images can be regenerated on UI changes.
test.use({ viewport: { width: 1280, height: 860 } });

const SHOTS = "../docs/screenshots";

/** Hide the Next.js dev-mode overlay button so it doesn't appear in the README captures. */
async function hideDevOverlay(page: Page): Promise<void> {
await page.addStyleTag({
content:
"nextjs-portal, [data-nextjs-toast], #__next-build-watcher { display: none !important; }",
});
}

const HOME_POINTS = [
{ dtId: "pt-1", id: "pt-1", name: "室温" },
{ dtId: "pt-2", id: "pt-2", name: "CO2 濃度" },
{ dtId: "pt-3", id: "pt-3", name: "消費電力" },
];

test("capture: operator home", async ({ context, page }) => {
await loginAs(context, "operator");
await mockBuildings(page);
await mockFloors(page);
await mockSpaces(page);
await mockDevices(page);
await mockPoints(page, HOME_POINTS);
await mockLatestTelemetry(page, {
"pt-1": isoSecondsAgo(12), // fresh
"pt-2": isoSecondsAgo(1200), // stale
// pt-3 omitted → missing
});
await page.goto("/home");
await page.getByTestId("home-attention-list").waitFor();
await hideDevOverlay(page);
await page.screenshot({ path: `${SHOTS}/operator-home.png` });
});

test("capture: resource explorer", async ({ context, page }) => {
await loginAs(context, "admin");
await mockBuildings(page);
await mockFloors(page);
// Detail pane fetches resource metadata; stub it so the page has no failed request.
await page.route("**/metadata**", (route) =>
fulfillJson(route, { identifiers: {}, customTags: {} }),
);
await page.goto("/resources");
// Single root building auto-expands and loads its floors (#135).
await page.getByText("1F オフィス").waitFor();
await hideDevOverlay(page);
await page.screenshot({ path: `${SHOTS}/resource-explorer.png` });
});

async function mockPointDetail(page: Page): Promise<void> {
await page.route("**/point-details/**", (route) =>
fulfillJson(route, {
point: { dtId: "SOS-PT-001", id: "SOS-PT-001", name: "室温", unit: "℃" },
}),
);
// latest (latest=true) + warm history both hit /telemetries/query; return a day of hourly samples
// so the chart draws a realistic curve.
await page.route("**/telemetries/query*", (route) => {
const now = Date.now();
const series = Array.from({ length: 24 }, (_, i) => ({
datetime: new Date(now - (23 - i) * 3600_000).toISOString(),
value: Math.round((22 + Math.sin(i / 3) * 2.5) * 10) / 10,
}));
return fulfillJson(route, series);
});
}

test("capture: point detail", async ({ context, page }) => {
await loginAs(context, "operator");
await mockPointDetail(page);
await page.goto("/points/SOS-PT-001");
await page.getByTestId("point-detail").waitFor();
await hideDevOverlay(page);
// Let the chart animation settle.
await page.waitForTimeout(600);
await page.screenshot({ path: `${SHOTS}/point-detail.png`, fullPage: true });
});
Loading