Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c9655a9
docs(signage): revise spec for YouTube IFrame video + OS Spotify audio
ut42tech May 30, 2026
f6063d1
docs(signage): revise implementation plan for YouTube video layer
ut42tech May 30, 2026
9068306
feat(shared): add activity-cycle (50/10 schedule + chime events)
ut42tech May 30, 2026
64d89c9
feat(shared): add YouTube Data API wrapper and signage playlist schema
ut42tech May 30, 2026
b0b58ba
feat(api): add GET /api/signage/playlist (YouTube Data API, cached)
ut42tech May 30, 2026
e620aef
feat(signage): scaffold Next.js app on port 3002
ut42tech May 30, 2026
c1b6937
feat(signage): add mentor-whitelist auth (auth-client, MeProvider she…
ut42tech May 30, 2026
017b59e
feat(signage): add time utils (now override, useNow, jst/mmss) and wa…
ut42tech May 30, 2026
4d20a60
feat(signage): synthesize chime tones via Web Audio
ut42tech May 30, 2026
abcf43e
feat(signage): drift-free chime scheduler with per-boundary dedupe
ut42tech May 30, 2026
ca92f29
feat(signage): poll /api/sessions/today and derive per-term counts
ut42tech May 30, 2026
d271612
feat(signage): YouTube IFrame self-queue player, playlist fetch, mute…
ut42tech May 30, 2026
0aae018
feat(signage): info-bar, break/idle screens, tap-to-start overlay
ut42tech May 30, 2026
ece4737
feat(signage): wire phase state machine (YouTube/break/idle + chimes …
ut42tech May 30, 2026
1b62414
style(signage): apply biome formatting
ut42tech May 30, 2026
e549253
docs(signage): add app CLAUDE.md/AGENTS.md and architecture entry
ut42tech May 30, 2026
1bda4de
fix(signage): rescue YouTube queue when playlist arrives before onReady
ut42tech May 30, 2026
7940974
feat(signage): add ?debug=1 preview mode (pseudo-clock, force-active,…
ut42tech May 30, 2026
590646b
feat(shared): add attendance liveliness classifier
ut42tech May 30, 2026
8325fd9
feat(signage): carry video titles and current-track through playlist/…
ut42tech May 30, 2026
c766a48
feat(signage): redesign viewer screens as a broadcast layout
ut42tech May 30, 2026
49c0379
feat(signage): align login + shell with broadcast theme; document layout
ut42tech May 30, 2026
9840b84
feat(shared): calibrate liveliness to a 25-person same-time capacity
ut42tech May 30, 2026
aded081
feat(signage): animate countdown digits with an odometer roll
ut42tech May 30, 2026
192202f
feat(signage): modernize the info ticker (stories progress, clipped s…
ut42tech May 30, 2026
3e05d2f
feat(signage): enrich the chime rail and refine the video panel
ut42tech May 30, 2026
09ff83b
refactor(signage): reword status, soften copy, add entrance motion
ut42tech May 30, 2026
4fa641e
feat(shared): summarize previous-event stays by unique participant
ut42tech May 30, 2026
5e709b7
feat(api): add signage previous-summary and health endpoints
ut42tech May 30, 2026
3bcc4ca
feat(signage): ticker shows previous-event stats, system health, and …
ut42tech May 30, 2026
70890f9
refactor(signage): drop the status badge ripple (sonar) animation
ut42tech May 30, 2026
1a9bfe5
feat(signage): style the follow call-out as an Instagram card with av…
ut42tech May 30, 2026
c31db78
fix(signage): widen Instagram follow card so the handle no longer clips
ut42tech May 30, 2026
6123156
revert(signage): restore the simple Instagram follow card
ut42tech May 30, 2026
e4d2e59
chore(signage): add comment
ut42tech May 30, 2026
adeded1
fix(signage): address code-review findings
ut42tech May 30, 2026
efda9bb
Merge pull request #37 from ut42tech/feat/signage-youtube
ut42tech May 30, 2026
6acad2b
docs: add signage app to root CLAUDE.md
ut42tech May 30, 2026
76db9be
docs: document the signage app across README and docs
ut42tech May 30, 2026
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
15 changes: 12 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,21 @@ BETTER_AUTH_SECRET=
# 本番: https://tecnova-api.<your-subdomain>.workers.dev
BETTER_AUTH_URL=

# 管理画面・受付アプリなど、API に cross-origin で来るフロントのオリジン許可リスト。
# 管理画面・受付アプリ・サイネージなど、API に cross-origin で来るフロントのオリジン許可リスト。
# カンマ区切り文字列。CORS と Better Auth の trustedOrigins の両方で参照される。
# 開発: http://localhost:3000,http://localhost:3001
# 本番: https://<checkin-domain>,https://<admin-domain>
# 開発: http://localhost:3000,http://localhost:3001,http://localhost:3002
# 本番: https://<checkin-domain>,https://<admin-domain>,https://<signage-domain>
TRUSTED_ORIGINS=

# -----------------------------------------------------------------------------
# YouTube Data API (apps/signage 動画プレイリスト)
# -----------------------------------------------------------------------------
# サイネージの GET /api/signage/playlist が playlistItems.list を叩くための APIキーと
# プレイリストID。APIキーは「YouTube Data API v3 限定」の制限を付与する(リファラ制限は
# 付けない=サーバ側呼び出し)。本番は Wrangler Secrets、ローカルは apps/api/.dev.vars。
YOUTUBE_API_KEY=
YOUTUBE_PLAYLIST_ID=

# -----------------------------------------------------------------------------
# Frontend (Next.js apps)
# -----------------------------------------------------------------------------
Expand Down
21 changes: 14 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ tecnova-platform/
├── apps/ # エンドユーザー向けアプリ
│ ├── api/ # Hono on Cloudflare Workers
│ ├── checkin/ # Next.js iPad PWA(受付端末)
│ └── admin/ # Next.js 管理PC画面
│ ├── admin/ # Next.js 管理PC画面
│ └── signage/ # Next.js 会場サイネージ(大型モニター・キオスク)
├── packages/ # アプリ間で共有するライブラリ
│ ├── db/ # Drizzle schema・migrations
│ ├── shared/ # 共通型・Zodスキーマ・Sheets連携
Expand All @@ -50,6 +51,11 @@ tecnova-platform/
Better Auth の設定は `packages/auth` ではなく `apps/api/src/lib/auth.ts` に集約している
(リクエスト毎に instance を作る制約上、Workers の Env を直接受け取れる場所に置くのが自然なため)。

フロント3アプリ(checkin / admin / signage)はいずれも **Next.js 16 / React 19**。各
`apps/*/CLAUDE.md` は冒頭で `@AGENTS.md` を読み込み、App Router の API がトレーニングデータと
乖離しているため**実装前に `node_modules/next/dist/docs/` を確認する**よう指示している。
ポート・env・PWA 設定などアプリ固有の制約は各アプリの `CLAUDE.md` を参照すること。

**新しい機能の追加先を判断する基準:**

- 単一アプリ固有 → 該当 `apps/*`
Expand Down Expand Up @@ -191,7 +197,7 @@ DBは Cloudflare D1(SQLite)。インタラクティブ・トランザクシ
| マージ前の成果物レビュー依頼 | `superpowers:requesting-code-review` |
| レビュー指摘を受け取って対応 | `superpowers:receiving-code-review` |
| 完了・修正完了を宣言する前 | `superpowers:verification-before-completion` |
| フロントの UI 構築(checkin / admin| `frontend-design` / `vercel:shadcn` |
| フロントの UI 構築(checkin / admin / signage)| `frontend-design` / `vercel:shadcn` |
| Next.js の設計・デバッグ | `vercel:nextjs` |
| ライブラリ・SDK の最新ドキュメント確認 | context7 (MCP) |

Expand Down Expand Up @@ -232,11 +238,12 @@ DBは Cloudflare D1(SQLite)。インタラクティブ・トランザクシ
pnpm dev

# 特定アプリのみ起動
# 注: workspace 名は package.json の "name" を使う。フロント2つは
# @tecnova/ プレフィックスを付けていない(checkin / admin)。
pnpm --filter @tecnova/api dev
pnpm --filter checkin dev
pnpm --filter admin dev
# 注: workspace 名は package.json の "name" を使う。フロント3つは
# @tecnova/ プレフィックスを付けていない(checkin / admin / signage)。
pnpm --filter @tecnova/api dev # :8787
pnpm --filter checkin dev # :3000
pnpm --filter admin dev # :3001
pnpm --filter signage dev # :3002

# Lint & format
pnpm biome check --write .
Expand Down
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
最初の導入先は **tec-nova Nagasaki**(長崎市と長崎大学による共同事業)。小学 1 年〜高校 3 年の子どもが 3D プリンタ・プログラミング・ロボット・3D モデリングなどに自由来場で取り組むファブリケーション活動です。

- 🎯 **解決する課題** — 紙とスプレッドシートに依存していた受付・名簿照合・二重登録の混乱を、構造的に解消する
- 🧩 **システム構成** — 1 つの API(Hono on Cloudflare Workers)+ クライアント(iPad 受付 / 管理 PC)を同一バックエンドから提供する API-first 設計
- 🧩 **システム構成** — 1 つの API(Hono on Cloudflare Workers)+ クライアント(iPad 受付 / 管理 PC / 会場サイネージ)を同一バックエンドから提供する API-first 設計
- 🔐 **プライバシー設計** — 住所・連絡先などの機微情報は内製 DB に持たず運営側の管理下に限定。保持するのは氏名・ニックネーム・学年のみ

### 受付のしくみ
Expand Down Expand Up @@ -59,6 +59,7 @@
| 🔍 名前検索 & 手入力 | QR が読めない場合のフォールバック |
| 📋 受付履歴 & 一括チェックアウト | 当日の全操作ログとワンタップ一括退場 |
| 📂 Drive 自動連携 | アクティベート時に GAS 経由で Drive フォルダ自動生成 |
| 📺 会場サイネージ | 大型モニター向け配信風表示。動画再生・チャイム・来場/にぎわい情報を巡回 |

> 今後のロードマップ・フェーズ計画は [`docs/requirements.md`](./docs/requirements.md)(スコープとフェーズ計画)と [`docs/handoff.md`](./docs/handoff.md)(進捗・残タスク)を参照してください。

Expand Down Expand Up @@ -105,11 +106,13 @@ graph TB
subgraph Clients["🖥️ クライアント"]
C1["📱 Checkin<br/><small>iPad PWA</small>"]
C3["💻 Admin<br/><small>PC ブラウザ</small>"]
C4["📺 Signage<br/><small>大型モニター</small>"]
end

subgraph Hosting["☁️ Vercel"]
V1["Next.js<br/>:3000"]
V3["Next.js<br/>:3001"]
V4["Next.js<br/>:3002"]
end

subgraph Edge["⚡ Cloudflare"]
Expand All @@ -125,9 +128,11 @@ graph TB

C1 --> V1
C3 --> V3
C4 --> V4

V1 -- "REST (type-safe)" --> API
V3 -- "REST (type-safe)" --> API
V4 -- "REST (type-safe)" --> API

API --> D1
API --> GS
Expand Down Expand Up @@ -177,11 +182,17 @@ tecnova-platform/
│ │ ├── app/ # App Router pages (/, /login, /first-time, etc.)
│ │ ├── components/ # QRスキャナ, 受付カード, 履歴テーブル
│ │ └── lib/ # api-client, auth-client
│ └── admin/ # Next.js 16 — 管理画面 (PC)
│ ├── admin/ # Next.js 16 — 管理画面 (PC)
│ │ └── src/
│ │ ├── app/ # ダッシュボード, 参加者一覧, メンター管理
│ │ ├── components/ # データテーブル, フォーム
│ │ └── lib/ # api-client, auth-client
│ └── signage/ # Next.js 16 — 会場サイネージ (大型モニター・キオスク)
│ └── src/
│ ├── app/ # ダッシュボード, 参加者一覧, メンター管理
│ ├── components/ # データテーブル, フォーム
│ └── lib/ # api-client, auth-client
│ ├── app/ # 配信風サイネージ画面
│ ├── components/ # 動画ステージ, チャイムレール, インフォティッカー
│ ├── config/ # フォールバック動画プレイリスト
│ └── lib/ # api-client, auth-client, now (時刻ソース)
├── packages/
│ ├── db/ # Drizzle schema + D1 migrations
│ ├── shared/ # 共通型・Zod スキーマ・Sheets 連携
Expand Down Expand Up @@ -247,7 +258,7 @@ pnpm dev
```

> [!TIP]
> `pnpm dev` は Turborepo 経由で `api` (`:8787`)・`checkin` (`:3000`)・`admin` (`:3001`) を同時起動します。
> `pnpm dev` は Turborepo 経由で `api` (`:8787`)・`checkin` (`:3000`)・`admin` (`:3001`)・`signage` (`:3002`) を同時起動します。

詳細なセットアップ手順は [`docs/mvp.md`](./docs/mvp.md#8-セットアップ手順) を参照してください。

Expand Down Expand Up @@ -276,7 +287,7 @@ graph LR
end

subgraph Vercel["▲ Vercel"]
VA["auto deploy<br/>checkin + admin"]
VA["auto deploy<br/>checkin + admin + signage"]
end

PR --> CI
Expand All @@ -295,6 +306,7 @@ graph LR
| `apps/api` | Cloudflare Workers | GitHub Actions — `main` push (paths filter) |
| `apps/checkin` | Vercel | GitHub 連携 — 自動デプロイ |
| `apps/admin` | Vercel | GitHub 連携 — 自動デプロイ |
| `apps/signage` | Vercel | GitHub 連携 — 自動デプロイ |

### Required GitHub Secrets

Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { authRoute } from './routes/auth';
import { checkinRoute } from './routes/checkin';
import { healthRoute } from './routes/health';
import { preRegistrationsRoute } from './routes/pre-registrations';
import { signageRoute } from './routes/signage';
import type { AppEnv } from './types';

const app = new Hono<AppEnv>();
Expand All @@ -26,6 +27,7 @@ app.use('/checkin/*', requireAuthenticatedMentor);

app.route('/api', adminRoute);
app.route('/api/pre-registrations', preRegistrationsRoute);
app.route('/api/signage', signageRoute);
app.route('/checkin', checkinRoute);
app.route('/', healthRoute);

Expand Down
70 changes: 70 additions & 0 deletions apps/api/src/lib/signage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type * as schema from '@tecnova/db';
import { events, sessions } from '@tecnova/db';
import type {
SignagePlaylistResponse,
SignagePreviousSummaryResponse,
} from '@tecnova/shared/schemas';
import { toJstDateString } from '@tecnova/shared/venue-schedule';
import { summarizeStays } from '@tecnova/shared/visit-summary';
import { fetchPlaylistVideos, type YouTubePlaylistVideo } from '@tecnova/shared/youtube';
import { desc, eq, lt } from 'drizzle-orm';
import type { DrizzleD1Database } from 'drizzle-orm/d1';
import type { Bindings } from '../types';

type Db = DrizzleD1Database<typeof schema>;

// プレイリストはサーバ側で数分キャッシュする。Data API のクォータ節約と、プレイリスト
// 更新の反映遅延(数分)は許容(spec §5.1)。Workers がリサイクルされたら自然に再取得。
const CACHE_TTL_MS = 5 * 60_000;

let cache: { items: YouTubePlaylistVideo[]; expiresAt: number } | null = null;

export const fetchSignagePlaylist = async (env: Bindings): Promise<SignagePlaylistResponse> => {
const now = Date.now();
if (!cache || cache.expiresAt <= now) {
const items = await fetchPlaylistVideos(env.YOUTUBE_API_KEY, env.YOUTUBE_PLAYLIST_ID);
cache = { items, expiresAt: now + CACHE_TTL_MS };
}
return { items: cache.items, refreshAt: new Date(cache.expiresAt).toISOString() };
};

// 前回開催日(JST の今日より前で最新の event)の来場・滞在データを集計して返す。
// events.date は TEXT 'YYYY-MM-DD' なので辞書順比較=日付順で「今日より前の最新」を取れる。
// 返すのは集計値のみ(PII なし)。前回開催が無ければ previous=null。
export const fetchPreviousEventSummary = async (
db: Db,
): Promise<SignagePreviousSummaryResponse> => {
const today = toJstDateString(new Date());
const [event] = await db
.select({ id: events.id, date: events.date })
.from(events)
.where(lt(events.date, today))
.orderBy(desc(events.date))
.limit(1);
if (!event) return { previous: null };

const rows = await db
.select({
participantId: sessions.participantId,
checkedInAt: sessions.checkedInAt,
checkedOutAt: sessions.checkedOutAt,
})
.from(sessions)
.where(eq(sessions.eventId, event.id));

// 退館→再入館で同一人物が複数行になるため、participantId を渡してユニーク人数で集計する。
const summary = summarizeStays(
rows.map((r) => ({
participantId: r.participantId,
checkedInAt: r.checkedInAt.getTime(),
checkedOutAt: r.checkedOutAt ? r.checkedOutAt.getTime() : null,
})),
);
return {
previous: {
date: event.date,
participantCount: summary.count,
averageStayMinutes: summary.averageStayMinutes,
},
};
};
30 changes: 30 additions & 0 deletions apps/api/src/routes/signage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { events } from '@tecnova/db';
import { count } from 'drizzle-orm';
import { Hono } from 'hono';
import { fetchPreviousEventSummary, fetchSignagePlaylist } from '../lib/signage';
import { createDb } from '../middleware/auth';
import type { AppEnv } from '../types';

// サイネージ用エンドポイント。requireAuthenticatedMentor は index.ts で /api/* に適用済み
// なので、ここでは付けない(メンター認証済みの信頼端末からのみ叩かれる)。
export const signageRoute = new Hono<AppEnv>();

// 動画プレイリスト(順序付き videoId 列)。取得失敗(APIキー未設定・YouTube エラー等)は
// throw され apiErrorHandler が 500 化し、クライアントは §5.4 のフォールバック videoId に倒れる。
signageRoute.get('/playlist', async (c) => c.json(await fetchSignagePlaylist(c.env)));

// 前回開催日の来場・滞在データ(集計のみ・PII なし)。
signageRoute.get('/previous-summary', async (c) =>
c.json(await fetchPreviousEventSummary(createDb(c.env))),
);

// 基盤システムの稼働確認。軽量な DB 到達クエリを1本投げ、成功なら ok を返す
// (失敗時は apiErrorHandler が 500 化し、サイネージ側で「障害」表示に倒れる)。
// /health(公開・root マウント)は CORS 対象外で別オリジンのサイネージから叩けないため、
// CORS 済みの /api 配下に置く。
signageRoute.get('/health', async (c) => {
const db = createDb(c.env);
// count() は1行に畳まれるので limit は不要。到達できれば ok。
await db.select({ n: count() }).from(events);
return c.json({ status: 'ok', time: new Date().toISOString() });
});
3 changes: 3 additions & 0 deletions apps/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export type Bindings = {
BETTER_AUTH_SECRET: string;
BETTER_AUTH_URL: string;
TRUSTED_ORIGINS: string;
// サイネージの動画プレイリスト取得(YouTube Data API v3・APIキーのみ)。
YOUTUBE_API_KEY: string;
YOUTUBE_PLAYLIST_ID: string;
};

export interface AuthUser {
Expand Down
41 changes: 41 additions & 0 deletions apps/signage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
3 changes: 3 additions & 0 deletions apps/signage/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
Loading
Loading