diff --git a/chros/README.md b/chros/README.md index 766946b..6de3080 100644 --- a/chros/README.md +++ b/chros/README.md @@ -1,24 +1,38 @@ # CHroS WebApp -Next.js製のWebアプリケーション。Viewer・Console・API を1つのアプリに内包しています。 -## 環境構築 -必要なツール -- Node.js 22 LTS -- pnpm +Next.js製のアプリです。運営画面・会場掲示・APIを1つのプロセスで動かします。 -依存の解決は monorepo のルートで行います。手順はルートの `README.md` を参照してください。 +## 画面付きの手順書 -1. リポジトリのルートで `pnpm install` を実行 -2. `.env.example` を `.env` にコピー -3. 環境変数を設定する +- [手順書の入口](public/manual/index.html) +- [第1部:大会運営](public/manual/operations.html) +- [第2部:開発](public/manual/development.html) -## 環境変数 -|キー|必須?|内容| -|---|---|---| -|`DATABASE_URL`|`true`|PostgreSQL 接続文字列| -|`NEXT_PUBLIC_APP_URL`|`false`|Viewer が SSE を張る先。同一オリジンなら空でよい| +起動中のアプリでは `/manual/index.html` を開いてください。GitHubのファイル画面ではHTMLが +ソースとして表示されるため、手元では `public/manual` をフォルダーごと保存し、 +`index.html` をブラウザーで開きます。画像はHTML内にBase64で埋め込んでいます。 -## ディレクトリ -- `app/console/` 運営向け操作画面 -- `app/display/` 配信用 Viewer 画面。SSE で状態に追従し、画面切り替えはルーティングではなく状態で行う -- `app/api/` Route Handlers(REST + SSE) +## 初回起動 + +Node.js 22以上とpnpm 9.12.0を用意し、**リポジトリのルート**で実行します。 + +```sh +pnpm install --frozen-lockfile +pnpm dev +``` + +ターミナルに表示されたURLの `/console` が運営画面、`/display` が会場掲示です。 +通常のポートは3000です。初回は架空の参加者を使ったサンプル大会が作成されます。 + +## 保存と環境変数 + +現在の試作はJSONファイルへ保存します。PostgreSQLやPrismaの初期化、`.env` の作成は不要です。 + +| 項目 | 内容 | +| --- | --- | +| `CHROS_DATA_DIR` | 任意。大会の保存先。未指定なら起動ディレクトリの `.chros`(通常は `chros/.chros`) | +| `DATABASE_URL` | 将来のDB構成用。現在の試作では使いません | +| SSE接続先 | 同一サーバーの `/api/stream`。接続先の環境変数は不要です | + +保存先を共有する複数プロセスでの同時起動は非対応です。バックアップと復旧は +[開発手順書](public/manual/development.html#recovery)を参照してください。 diff --git a/chros/public/manual/assets/data-flow.svg b/chros/public/manual/assets/data-flow.svg new file mode 100644 index 0000000..1dd76c1 --- /dev/null +++ b/chros/public/manual/assets/data-flow.svg @@ -0,0 +1,79 @@ + + 入力から保存、掲示までのデータの流れ + + 運営画面が更新番号と操作をPOSTし、APIが検証、tournamentが進行、scoringが判定を扱います。storeがJSONへ保存した後、SSEが全状態を運営画面と会場掲示へ配信します。 + + + + + + 単一の Next.js / Node プロセス + + + + + + + + + + + + + + + 運営画面 + 会場掲示 + API / state + tournament + store + scoring + JSON ファイル + + + Console / useControl + Display / Board + 入力・更新番号・Originの検証 + 大会操作・保護・操作履歴 + 保存完了後に変更を通知 + 各戦・試合・順位を計算 + 現行状態・直前状態・退避 + + + SSE / stream + 全状態を snapshot として配信 + + + + + + + + + + + + + + + + + + + + + + + POST + 判定 + 保存 + + + diff --git a/chros/public/manual/assets/manual.css b/chros/public/manual/assets/manual.css new file mode 100644 index 0000000..e40fd06 --- /dev/null +++ b/chros/public/manual/assets/manual.css @@ -0,0 +1,877 @@ +:root { + color-scheme: light; + --ink: #1d2c38; + --muted: #546773; + --line: #d7e1e3; + --paper: #ffffff; + --page: #f3f6f5; + --teal: #08685f; + --teal-soft: #e4f2ee; + --amber: #835013; + --amber-soft: #fff4df; + --radius: 16px; +} +* { + box-sizing: border-box; +} +html { + scroll-behavior: smooth; + scroll-padding-top: 100px; +} +body { + margin: 0; + background: var(--page); + color: var(--ink); + font-family: + -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', 'Yu Gothic', Meiryo, sans-serif; + font-size: 16px; + line-height: 1.9; +} +a { + color: var(--teal); + text-underline-offset: 4px; + overflow-wrap: anywhere; +} +a:hover { + text-decoration-thickness: 2px; +} +:focus-visible { + outline: 3px solid #e19128; + outline-offset: 4px; +} +button, +input { + font: inherit; +} +button { + cursor: pointer; +} +.skip-link { + position: fixed; + z-index: 10; + top: -100px; + left: 16px; + padding: 10px 18px; + background: var(--ink); + color: white; +} +.skip-link:focus { + top: 8px; +} +.topbar { + position: sticky; + top: 0; + z-index: 5; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 14px max(24px, calc((100vw - 1440px) / 2)); + border-bottom: 1px solid var(--line); + background: #fffffffa; +} +.brand { + color: var(--ink); + text-decoration: none; + display: flex; + align-items: baseline; + gap: 14px; + white-space: nowrap; +} +.brand strong { + font-size: 26px; + line-height: 1; + letter-spacing: -1.2px; +} +.brand small { + font-size: 12px; + color: var(--muted); + letter-spacing: 0.08em; +} +.topbar nav { + display: flex; + align-items: center; + gap: 20px; + font-size: 13px; +} +.topbar nav a { + text-decoration: none; + color: var(--muted); +} +.topbar nav a[aria-current='page'] { + color: var(--teal); + font-weight: 700; + border-bottom: 2px solid var(--teal); +} +.print-button { + border: 1px solid var(--line); + border-radius: 8px; + padding: 6px 13px; + background: white; + font-size: 12px; + color: var(--ink); +} +.layout { + max-width: 1376px; + margin: 0 auto; + padding: 40px 32px 80px; + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + gap: 48px; +} +.sidebar { + position: sticky; + top: 100px; + align-self: start; + max-height: calc(100vh - 125px); + overflow: auto; +} +.sidebar .nav-label { + margin: 0 0 14px; + font-size: 11px; + font-weight: 700; + color: var(--muted); + letter-spacing: 0.15em; +} +.sidebar ol { + list-style: none; + padding: 0; + margin: 0; +} +.sidebar li { + margin: 4px 0; +} +.sidebar a { + display: flex; + gap: 12px; + border-radius: 8px; + padding: 8px 10px; + color: var(--muted); + text-decoration: none; + font-size: 13px; + line-height: 1.65; +} +.sidebar a:hover, +.sidebar a[aria-current='location'] { + color: var(--teal); + background: var(--teal-soft); +} +.sidebar a span { + font-size: 11px; + padding-top: 2px; + font-variant-numeric: tabular-nums; +} +.sidebar .side-note { + margin: 25px 10px; + font-size: 12px; + color: var(--muted); +} +main { + min-width: 0; +} +.hero { + position: relative; + padding: 8px 0 36px; + border-bottom: 2px solid var(--ink); + margin-bottom: 36px; +} +.eyebrow { + font-size: 11px; + font-weight: 750; + letter-spacing: 0.17em; + color: var(--teal); + margin: 0 0 16px; +} +h1 { + font-size: clamp(30px, 3.3vw, 45px); + line-height: 1.4; + letter-spacing: -0.04em; + margin: 0 0 18px; +} +.hero .lead { + max-width: 740px; + color: var(--muted); + font-size: 16px; + margin-bottom: 22px; +} +.meta { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + font-size: 11px; + color: var(--muted); +} +.pill { + border: 1px solid var(--line); + border-radius: 20px; + background: white; + padding: 2px 12px; +} +section.chapter { + margin: 0 0 52px; + padding-top: 10px; +} +.chapter-heading { + display: flex; + gap: 14px; + align-items: baseline; + margin: 0 0 22px; + border-bottom: 1px solid var(--line); + padding-bottom: 14px; +} +.chapter-number { + font-size: 13px; + font-weight: 700; + color: var(--teal); + font-variant-numeric: tabular-nums; +} +h2 { + font-size: 25px; + letter-spacing: -0.025em; + line-height: 1.5; + margin: 0; +} +h3 { + font-size: 18px; + line-height: 1.6; + margin: 30px 0 12px; +} +h4 { + font-size: 16px; + margin: 18px 0 8px; +} +p { + margin: 0 0 16px; +} +ul, +ol { + padding-left: 1.5em; +} +li + li { + margin-top: 7px; +} +strong { + font-weight: 700; +} +.small, +figcaption { + color: var(--muted); + font-size: 13px; +} +.cards { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; + margin: 22px 0; +} +.card { + padding: 25px; + background: var(--paper); + border: 1px solid var(--line); + border-radius: var(--radius); +} +.card h3 { + margin-top: 0; +} +.card p:last-child { + margin-bottom: 0; +} +.card .tag { + color: var(--teal); + font-size: 11px; + letter-spacing: 0.1em; + font-weight: 700; +} +.card-link { + display: block; + color: var(--ink); + text-decoration: none; + transition: + border-color 0.2s, + transform 0.2s; +} +.card-link:hover { + border-color: var(--teal); + transform: translateY(-2px); +} +.card-link .arrow { + display: inline-block; + margin-top: 14px; + color: var(--teal); + font-weight: 700; +} +.note { + background: var(--teal-soft); + border-left: 3px solid var(--teal); + padding: 18px 22px; + margin: 22px 0; + border-radius: 0 10px 10px 0; +} +.note > strong:first-child { + display: block; + margin-bottom: 4px; +} +.note p:last-child { + margin-bottom: 0; +} +.note.warning { + background: var(--amber-soft); + border-color: var(--amber); +} +.note.warning > strong:first-child { + color: var(--amber); +} +.path { + font-size: 13px; + color: var(--teal); + background: var(--teal-soft); + padding: 10px 15px; + border-radius: 8px; + margin: 18px 0; +} +.path span { + color: var(--muted); + margin: 0 8px; +} +.steps { + padding-left: 0; + list-style: none; + counter-reset: step; + margin: 22px 0; +} +.steps > li { + position: relative; + padding: 0 0 22px 46px; + counter-increment: step; +} +.steps > li::before { + content: counter(step); + position: absolute; + left: 0; + top: 1px; + display: grid; + place-items: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--ink); + color: white; + font-size: 12px; + font-weight: 700; +} +.steps > li + li { + margin-top: 0; +} +.steps > li p { + margin: 4px 0 0; + color: var(--muted); +} +.done { + color: var(--teal); + background: white; + border: 1px solid #afccc4; + padding: 12px 18px; + border-radius: 8px; + font-size: 14px; + margin-top: 16px; +} +.done::before { + content: '完了の目印'; + font-size: 11px; + display: inline-block; + font-weight: 750; + margin-right: 14px; +} +figure { + margin: 26px 0; +} +.screenshot { + background: white; + border: 1px solid var(--line); + border-radius: 12px; + overflow: hidden; +} +.screenshot > a { + display: block; + line-height: 0; +} +.screenshot img { + width: 100%; + height: auto; + display: block; +} +figcaption { + padding: 12px 16px; + line-height: 1.75; + background: white; +} +figcaption strong { + color: var(--ink); +} +figcaption .zoom { + float: right; + margin-left: 12px; + font-size: 11px; +} +.compact-shot { + max-width: 710px; + margin-left: auto; + margin-right: auto; +} +.diagram { + padding: 24px; + border: 1px solid var(--line); + border-radius: var(--radius); + background: white; +} +.diagram img { + display: block; + width: 100%; + height: auto; +} +.diagram figcaption { + padding: 16px 0 0; +} +.table-wrap { + overflow-x: auto; + border: 1px solid var(--line); + border-radius: 10px; + background: white; + margin: 20px 0; +} +table { + width: 100%; + border-collapse: collapse; + text-align: left; + font-size: 14px; + line-height: 1.8; +} +caption { + text-align: left; + padding: 14px 18px; + color: var(--muted); + font-size: 12px; +} +th, +td { + padding: 13px 18px; + vertical-align: top; + border-top: 1px solid var(--line); +} +thead th { + background: #ebf0ef; + font-size: 12px; + color: var(--muted); + border-top: 0; +} +tbody th { + min-width: 130px; + font-weight: 650; +} +td > :last-child { + margin-bottom: 0; +} +code, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} +code { + font-size: 0.88em; + overflow-wrap: anywhere; + background: #e7eded; + border-radius: 4px; + padding: 2px 5px; +} +pre { + background: #172c35; + color: #edf8f5; + padding: 22px; + border-radius: 10px; + overflow-x: auto; + font-size: 13px; + line-height: 1.9; + margin: 18px 0; +} +pre code { + background: none; + padding: 0; + border-radius: 0; + font-size: inherit; + overflow-wrap: normal; +} +.checklist { + display: grid; + gap: 10px; + padding: 0; + list-style: none; +} +.checklist label { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 18px; + background: white; + border: 1px solid var(--line); + border-radius: 8px; + cursor: pointer; +} +.checklist input { + flex-shrink: 0; + accent-color: var(--teal); + width: 17px; + height: 17px; + margin: 6px 0 0; +} +footer { + border-top: 1px solid var(--line); + margin-top: 46px; + padding-top: 20px; + color: var(--muted); + font-size: 12px; +} +.footer-nav { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 14px; + margin-top: 25px; +} +.landing { + max-width: 1100px; + margin: 0 auto; + padding: 66px 32px 72px; +} +.landing .hero { + padding-bottom: 40px; +} +.landing .card { + padding: 34px; +} +.landing .card h2 { + margin: 10px 0 14px; +} +.landing .part { + font-size: 44px; + font-weight: 700; + line-height: 1; + color: #a2beb7; +} +.zoom-trigger { + display: block; + width: 100%; + padding: 0; + border: 0; + background: transparent; + cursor: zoom-in; +} +.narrow-shot { + max-width: 380px; +} +.image-dialog { + width: min(1600px, calc(100vw - 32px)); + max-height: calc(100vh - 32px); + padding: 18px; + border: 1px solid var(--line); + border-radius: 12px; +} +.image-dialog::backdrop { + background: #122521d9; +} +.image-dialog img { + display: block; + max-width: 100%; + height: auto; + margin: 12px auto 0; +} +.image-dialog button { + position: sticky; + top: 0; + display: block; + margin-left: auto; + border: 1px solid var(--line); + border-radius: 8px; + padding: 8px 16px; + color: var(--ink); + background: white; +} +.image-dialog-toolbar { + position: sticky; + top: 0; + left: 0; + z-index: 1; + display: flex; + justify-content: flex-end; + gap: 10px; + padding-bottom: 10px; + background: white; +} +.image-dialog-toolbar button { + position: static; + margin: 0; + font-size: 13px; +} +.image-dialog.is-original img { + max-width: none; +} +@media (max-width: 1000px) { + .layout { + grid-template-columns: 180px minmax(0, 1fr); + gap: 28px; + padding: 30px 22px 60px; + } + .topbar { + padding: 14px 22px; + } + .topbar nav { + gap: 14px; + } + .brand small { + display: none; + } +} +@media (max-width: 760px) { + html { + scroll-padding-top: 120px; + } + .topbar { + flex-wrap: wrap; + gap: 10px; + padding: 12px 18px; + } + .topbar nav { + gap: 16px; + flex-wrap: wrap; + } + .print-button { + padding: 4px 8px; + } + .layout { + display: block; + padding: 20px 18px 50px; + } + .sidebar { + position: static; + max-height: none; + margin-bottom: 28px; + padding-bottom: 18px; + border-bottom: 1px solid var(--line); + } + .sidebar ol { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 3px; + } + .sidebar a { + padding: 6px; + font-size: 12px; + } + .sidebar .side-note { + display: none; + } + .hero { + margin-bottom: 25px; + } + .cards { + grid-template-columns: 1fr; + } + h2 { + font-size: 22px; + } + .chapter-heading { + gap: 10px; + } + .card { + padding: 20px; + } + .diagram { + padding: 12px; + } + .steps > li { + padding-left: 40px; + } + .note { + padding: 15px 17px; + } + th, + td { + padding: 11px 13px; + } + .landing { + padding: 35px 18px; + } + .landing .card { + padding: 26px; + } +} +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .card-link { + transition: none; + } +} +@page { + size: A4; + margin: 16mm 14mm; + @bottom-left { + content: 'CHroS / 手順書'; + font-family: sans-serif; + font-size: 8pt; + color: #546773; + } + @bottom-right { + content: counter(page) ' / ' counter(pages); + font-family: sans-serif; + font-size: 8pt; + color: #546773; + } +} +@media print { + :focus-visible { + outline: none; + } + html { + scroll-behavior: auto; + } + body { + background: white; + font-size: 10pt; + line-height: 1.7; + } + .topbar, + .sidebar, + .print-button, + .skip-link, + .zoom, + .footer-nav { + display: none !important; + } + .layout, + .landing { + display: block; + max-width: none; + padding: 0; + } + .hero { + padding: 0 0 18px; + margin-bottom: 22px; + } + h1 { + font-size: 28pt; + } + h2 { + font-size: 18pt; + } + h3 { + font-size: 13pt; + } + h2, + h3, + h4, + .chapter-heading { + break-after: avoid; + } + section.chapter { + margin-bottom: 26px; + } + figure, + .note, + .done, + .card, + tr, + pre, + .checklist li { + break-inside: avoid; + } + .screenshot, + .diagram { + max-width: 100%; + margin: 18px 0; + } + th, + td { + padding: 8px 10px; + } + table { + font-size: 9pt; + } + a { + color: inherit; + } + pre { + color: var(--ink); + background: #f2f4f4; + border: 1px solid var(--line); + white-space: pre-wrap; + overflow-wrap: anywhere; + } + .cards { + gap: 12px; + } + .card { + padding: 18px; + } + p, + li { + orphans: 3; + widows: 3; + } + .small, + figcaption, + footer { + font-size: 8pt; + } + .image-dialog { + display: none !important; + } + .steps > li { + padding-bottom: 12px; + break-inside: avoid; + } + .steps { + margin: 16px 0; + } + .screenshot { + margin: 14px auto; + } + .screenshot img { + max-height: 112mm; + object-fit: contain; + } + .note { + margin: 16px 0; + padding: 12px 16px; + } + .landing h1 { + font-size: 22pt; + } + .landing .hero { + margin-bottom: 14px; + } + .landing .lead { + font-size: 10pt; + margin-bottom: 12px; + } + .landing .card { + padding: 14px; + } + .landing .card h2 { + font-size: 14pt; + margin: 8px 0; + } + .landing .part { + font-size: 28px; + } + .table-wrap { + overflow: visible; + break-inside: avoid; + } + p { + margin-bottom: 10px; + break-inside: avoid; + } + .narrow-shot { + max-width: 380px; + } +} diff --git a/chros/public/manual/assets/manual.js b/chros/public/manual/assets/manual.js new file mode 100644 index 0000000..e8e45cc --- /dev/null +++ b/chros/public/manual/assets/manual.js @@ -0,0 +1,71 @@ +for (const button of document.querySelectorAll('[data-print]')) { + button.hidden = false; + button.addEventListener('click', () => window.print()); +} + +const chapterLinks = [...document.querySelectorAll('.sidebar a[href^="#"]')]; +if ('IntersectionObserver' in window && chapterLinks.length) { + const observer = new IntersectionObserver( + (entries) => { + const visibleChapter = entries.find((entry) => entry.isIntersecting); + if (!visibleChapter) return; + for (const link of chapterLinks) { + if (link.hash === `#${visibleChapter.target.id}`) { + link.setAttribute('aria-current', 'location'); + } else { + link.removeAttribute('aria-current'); + } + } + }, + { rootMargin: '-15% 0px -65% 0px' }, + ); + document.querySelectorAll('section.chapter').forEach((chapter) => observer.observe(chapter)); +} + +const screenshotImages = document.querySelectorAll('img[data-screenshot]'); +if (screenshotImages.length && typeof HTMLDialogElement !== 'undefined') { + const dialog = document.createElement('dialog'); + dialog.className = 'image-dialog'; + dialog.setAttribute('aria-label', 'スクリーンショットの拡大'); + const closeButton = document.createElement('button'); + closeButton.type = 'button'; + closeButton.textContent = '閉じる ×'; + closeButton.addEventListener('click', () => dialog.close()); + const sizeButton = document.createElement('button'); + sizeButton.type = 'button'; + sizeButton.textContent = '原寸で表示'; + sizeButton.setAttribute('aria-pressed', 'false'); + sizeButton.addEventListener('click', () => { + const originalSize = dialog.classList.toggle('is-original'); + sizeButton.textContent = originalSize ? '全体を表示' : '原寸で表示'; + sizeButton.setAttribute('aria-pressed', String(originalSize)); + }); + const toolbar = document.createElement('div'); + toolbar.className = 'image-dialog-toolbar'; + toolbar.append(sizeButton, closeButton); + const enlargedImage = document.createElement('img'); + dialog.append(toolbar, enlargedImage); + document.body.append(dialog); + dialog.addEventListener('click', (event) => { + if (event.target === dialog) dialog.close(); + }); + + for (const screenshot of screenshotImages) { + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'zoom-trigger'; + trigger.setAttribute('aria-label', `${screenshot.alt}(画像を拡大)`); + screenshot.before(trigger); + trigger.append(screenshot); + trigger.addEventListener('click', () => { + enlargedImage.src = screenshot.src; + enlargedImage.alt = screenshot.alt; + dialog.classList.remove('is-original'); + sizeButton.textContent = '原寸で表示'; + sizeButton.setAttribute('aria-pressed', 'false'); + dialog.showModal(); + }); + const hint = trigger.closest('figure').querySelector('.zoom'); + if (hint) hint.hidden = false; + } +} diff --git a/chros/public/manual/assets/tournament-flow.svg b/chros/public/manual/assets/tournament-flow.svg new file mode 100644 index 0000000..1d4f8e1 --- /dev/null +++ b/chros/public/manual/assets/tournament-flow.svg @@ -0,0 +1,57 @@ + + 大会の進行と担当する操作 + 準備、開会、予選、本戦、閉会の順。予選から本戦へは進出者の確認が必要です。 + + + + + + + + + + + + + + + + + + 01 PREPARE + 02 OPENING + 03 QUALIFYING + 04 FINALS + 05 CLOSING + + + 準備 + 開会 + 予選 + 本戦 + 閉会 + + + 設定・参加者 + 組み合わせ作成 + 進行案内を掲示 + 接続と表示を確認 + 総当たり・2戦記録 + 順位と進出者を確認 + 組み合わせを確定 + 決勝まで勝ち上がり + 閉会式を掲示 + CSV・JSONを保存 + + + 進行区分と会場の画面は、運営が手動で切り替えます。 + + + diff --git a/chros/public/manual/assets/two-games.svg b/chros/public/manual/assets/two-games.svg new file mode 100644 index 0000000..c9820c7 --- /dev/null +++ b/chros/public/manual/assets/two-games.svg @@ -0,0 +1,51 @@ + + 1試合は先攻と後攻を交換する2戦 + + 入力欄の左は若葉、右は高原のまま。第1戦は若葉がCOOL、第2戦は高原がCOOLになります。第2戦の記録後に勝者を判定します。 + + + + + + + + + 第1戦 + 第2戦 + + + じゃんけん後に先攻を登録 + 先攻・後攻を自動で反転 + + + + + + + COOL · 先攻 + HOT · 後攻 + HOT · 後攻 + COOL · 先攻 + + + 若葉 ひなた + 高原 ゆう + 若葉 ひなた + 高原 ゆう + + + + 第1戦を記録 + + + 入力欄の人物は動きません。名前と COOL / HOT を毎戦確認。 + + + diff --git a/chros/public/manual/development.html b/chros/public/manual/development.html new file mode 100644 index 0000000..d818554 --- /dev/null +++ b/chros/public/manual/development.html @@ -0,0 +1,872 @@ + + + + + + + + 開発の手順書 | CHroS 手順書 + + + + + +
+ CHroS.FIELD GUIDE + +
+
+ +
+
+

PART 02 / DEVELOPMENT

+

開発の手順書

+

+ 初回起動から、変更・検証・会場運用・復旧まで。現在の実装をたどりながら、安全に開発を進めるための手順です。 +

+
+ 実装・保守・技術担当者向け対人戦の試作版更新 2026.09.23 +
+
+
+
+ 01 +

まず手元で起動する

+
+

+ この手順は、リポジトリを初めて触る開発者向けです。現在のアプリはNext.jsの1プロセスで動き、保存先はJSONファイルです。PostgreSQL・Prismaの初期化や、CHaser試合サーバーとの接続は起動に必要ありません。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 用意するもの +
必要なものこのリポジトリの指定確認方法
Node.js22以上。PRの自動検査は22系。node --version
pnpm9.12.0(packageManagerで固定)pnpm --version
Gitリポジトリを取得できることgit --version
ブラウザー運営用と掲示用の2タブを開く開発用URLに接続
+
+

+ コマンドはmacOS/Linuxのシェルを基準に記載しています。WindowsではWSL等の同等の環境を使ってください。Node.jsを先に用意し、pnpmがなければ次のコマンドで指定版を入れます。 +

+
npm install --global pnpm@9.12.0
+

+ 長いコードや表は、枠内を横にスクロールして読めます。コマンド末尾のバックスラッシュ(\)は次の行へ続く印です。実行するときは、続く行もまとめてコピーしてください。 +

+
    +
  1. + リポジトリを取得し、developから作業ブランチを作る +

    + すでに手元にある場合はcloneを省略します。ローカルの未コミット変更は、内容を確認してコミットまたは退避してから切り替えます。 +

    +
    git clone https://github.com/UDHI-LAB/CHaserProgressionControlSystem.git
    +cd CHaserProgressionControlSystem
    +git fetch origin
    +git switch develop
    +git pull --ff-only origin develop
    +git switch -c codex/your-change
    +
  2. +
  3. + 依存をリポジトリのルートにまとめて入れる +
    pnpm install --frozen-lockfile
    +

    + chros/や各packageへ移動して個別にインストールする必要はありません。通常の起動では環境変数ファイルの作成も不要です。 +

    +
  4. +
  5. + 開発サーバーを起動する +
    pnpm dev
    +

    + ターミナルに表示されたURLを開きます。通常は + http://localhost:3000 + です。ポートが使用中の場合は、実際に表示された番号を使います。 +

    +
  6. +
  7. + 2つの画面を開いて、記録と掲示を確かめる +

    + /console と + /display + を開きます。新しい保存先ならサンプル大会が作られます。運営画面で「サーバー接続中」を確認し、2戦を記録する手順を試します。 +

    +
  8. +
+
運営画面と掲示画面が開き、保存した結果が両方へ反映された。
+
+ 練習用データの保存先をはっきり分ける +

+ 通常は + chros/.chros + に保存されます。分離したい場合は、ルートで次を実行します。絶対パスを使うと、起動する場所が変わっても同じ保存先を指せます。 +

+
CHROS_DATA_DIR="$PWD/chros/.chros/development" pnpm dev
+
+
+
+
+ 02 +

入力から掲示までの流れを読む

+
+

+ 運営画面が送るのは、現在の更新番号と「何をするか」という操作です。サーバーが検証・計算・保存した後、同じ全状態を運営画面と会場掲示に配信します。画面側で別の採点処理を持たせない構成です。 +

+
+ 運営画面からAPIに操作を送り、大会処理と採点を経てJSON保存。保存後にSSEが運営画面と会場掲示へ全状態を配信 +
+ データの流れ。scoringは入出力を持たない計算、storeは保存と通知を担当します。 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 変更したい内容からファイルを探す(パスはリポジトリのルートから) +
変更内容読む入口
状態・操作・入力の型と検証packages/shared/src/control.ts
1戦・2戦の採点packages/scoring/src/match.ts
予選順位・同順位packages/scoring/src/standings.ts
大会操作の振り分け・履歴chros/lib/tournament.ts
参加者・予選・本戦・結果の記録chros/lib/tournament/ 配下の各操作
保存・CSV・SSE + chros/lib/store.ts、chros/lib/export-results.ts、chros/app/api/stream/route.ts +
ブラウザーの通信状態・送信chros/lib/use-control.ts
運営画面 + chros/components/ConsoleApp.tsx → + console/、management/、matches/ +
掲示画面とプレビュー + chros/components/Board.tsx → board/、ページ数は + chros/lib/board-pages.ts +
表示順の編集と実行 + chros/components/Rundown.tsx、rundown/CueEditor.tsx +
+
+

採点を変更する前に押さえる4点

+
    +
  • + Match.a/b + は固定の参加者枠です。先攻側という意味ではありません。初戦の先攻は + firstCool に保存します。 +
  • +
  • + scoreA/scoreB + は元の得点です。特殊P・ゲーム勝数・換算得点は計算で導出します。 +
  • +
  • + 試合の勝者は2戦がそろってから確定します。同値で再試合待ちなら、予選順位へ加算しません。 +
  • +
  • + 再試合前の記録は previousAttempts、訂正前の値と理由は操作履歴に残します。 +
  • +
+
+
+
+ 03 +

変更を小さく作り、読みやすく保つ

+
+

+ 最初に「誰が、何をしたとき、何が変わるか」を一文で決めます。その変更を担う場所から読み、画面の都合と大会の規則を混ぜないようにします。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 変更の種類と確認すべき範囲 +
例変更する場所一緒に確認すること
文言・レイアウトを変える対象のコンポーネント、必要ならCSS運営・掲示の両方、長い名前、狭い画面
採点・順位の基準を変えるscoring、規則版・説明資料得点比較、特殊勝利、同点、A/B反転、順位への算入
大会の操作を増やすsharedのコマンド → tournament → 画面入力検証、進行上の保護、履歴、掲示への反映
保存項目を増やすsharedの状態 → 初期値 → 保存・出力既存JSONの読込互換性、再起動、バックアップからの復旧
+
+

レビューで確認する書き方

+
    +
  • + match、player、recordedGame + のように役割が伝わる名前を使います。 +
  • +
  • + 関連する処理をまとめ、採点・保存・HTTP応答・描画の責任を分けます。単純な一行まで分割する必要はありません。 +
  • +
  • 入力不備などの例外は先に扱い、深い条件分岐を減らします。
  • +
  • + 複数段階の操作には + saveResult + のように名前を付けます。単純な入力更新は入力欄の近くに置きます。 +
  • +
  • + コメントは理由や制約の説明に絞ります。関数名から分かる処理の言い換えは増やしません。 +
  • +
+

+ 命名と処理のまとまりはZennの可読性の記事、条件分岐はZennの早期リターンの記事を参考にした方針です。 +

+

コミットとPRは、変更目的で分ける

+

+ 整形だけの変更、判定の変更、画面の変更、資料の変更を区別するとレビューしやすくなります。細かいファイル単位ではなく、ひとつの理由で説明できるまとまりにします。PRには変更後の動き・確認したこと・残る制約を書き、取り込み先は + develop にします。 +

+
+ 保存データの形を変える場合は、移行も設計する +

+ 現在の保存状態には + schemaVersion + がありますが、版を上げるだけで古いJSONが移行される仕組みではありません。保存済みデータを残した読込方法や移行手順を用意し、サンプルを自動生成して上書きする回避策は取らないでください。 +

+
+
+
+
+ 04 +

変更を検証する

+
+

まず、ルートで通常の検査を実行

+
pnpm format
+pnpm format:check
+pnpm typecheck
+pnpm test
+pnpm build
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 各コマンドで分かること +
確認目的と期待する結果
format / format:check + Prettierで書式を統一・検査します。HTML手順書のCSS・JSも対象です。命名の良し悪しは別途レビューします。 +
typecheck + shared・scoring・アプリの型、未使用の変数・引数を検査します。エラーなしで終了することを確認します。 +
test + 採点と大会進行のユニットテストを実行します。作成時点では29件です。変更に応じた境界条件を追加します。 +
build + Next.jsの本番ビルドを作成します。実行中の開発サーバーを止めてから行います。同じ + .next を使うためです。 +
+
+

+ GitHub + ActionsはPRで整形・型・テストを実行します。本番ビルドと、次のAPI・画面の確認は別途行ってください。 +

+

APIの一連の流れを確認する

+

+ 専用の空の保存先と別ポートを使います。このテストは大会を切り替え、参加者を登録し、予選から決勝まで結果を書き込みます。 +

+
+ 本番や自分の作業データがあるサーバーには実行しない +

+ --reset-test-data + はテスト開始の明示です。データを保護するスイッチではありません。必ず下記のように専用の保存先を作ります。 +

+
+

+ ターミナルA:ルートからビルドし、毎回新しいテスト用フォルダーで起動。 +

+
pnpm build
+CHROS_TEST_DATA="$(mktemp -d)"
+CHROS_DATA_DIR="$CHROS_TEST_DATA" \
+  pnpm --filter chros exec next start \
+  --hostname 127.0.0.1 --port 3114
+

ターミナルB:ルートで実行。

+
node scripts/smoke-prototype.mjs http://127.0.0.1:3114 --reset-test-data
+

+ "result": "PASS" + を確認します。参加者登録・総当たり・2戦記録・本戦・決勝のほか、SSE、更新競合409、別Originの書き込み拒否403、CSV・JSONも確認します。終了後はターミナルAで + Ctrl+C を押します。 +

+

最後はブラウザーで、運営の操作として確認

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ 変更の影響範囲について、検査の結果と実操作で確かめた範囲をPRに記載した。 +
+
+
+
+ 05 +

会場向けに起動する

+
+

+ サーバー役のPCを1台決め、運営・掲示の端末を同じ会場LANにつなぎます。現在は単一プロセスでの運用が対象です。同じ保存先に対し、複数のアプリを同時に起動しないでください。 +

+
    +
  1. + 依存と本番ビルドを用意する +
    pnpm install --frozen-lockfile
    +pnpm build
    +
  2. +
  3. + 大会専用の保存先を指定し、LANへ公開して起動する +

    + 次の + /absolute/path/to/event-data + を、書き込める実際の絶対パスに置き換えます。 +

    +
    CHROS_DATA_DIR=/absolute/path/to/event-data \
    +  pnpm --filter chros exec next start \
    +  --hostname 0.0.0.0 --port 3000
    +
  4. +
  5. + 端末ごとにURLを開く +

    + サーバー自身では http://localhost:3000/console。他の端末では + http://サーバーPCのLANアドレス:3000/display などを開きます。他の端末の + localhost は、その端末自身を指すため使いません。 +

    +
  6. +
  7. + 接続・保存・投影をリハーサルする +

    + 端末間で更新されること、ファイアウォールの許可、投影の文字サイズ、PCのスリープ設定を確認します。停止は起動したターミナルで + Ctrl+C。保存後の状態は同じ保存先で再起動すると読み込まれます。 +

    +
  8. +
+
+ 現在の運用範囲 +

+ 会場LAN内の試作で、ログイン・権限管理はありません。インターネットへ一般公開する前提の設定ではありません。試合サーバーとの自動連携や複数コートの割当は含みません。 +

+
+

Docker構成を使う場合

+

+ リポジトリには docker-compose.yaml があります。アプリの保存先は + /data/chros、volume名は chros-data です。DBは + database + プロファイルに分けられ、現在の起動に不要です。Dockerでのビルド・起動はこの資料では未検証のため、会場採用前に永続化まで確認してください。down -v + は保存用volumeも削除するので、記録の保管前に実行しないでください。 +

+
+
+
+ 06 +

保存先を理解し、必要なときに復旧する

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ CHROS_DATA_DIRの中にあるファイル +
ファイル内容・使いどころ
state.json現在の大会全体。起動後の読み込み元。
state.previous.json + 通常の操作で更新する、直前の変更前状態。長期履歴のバックアップではありません。 +
archive-時刻-revision.json「空の大会」「サンプル」へ切り替えたときに退避した大会。
ダウンロードした全記録JSON運営が書き出した時点の大会。外部保管・復旧に使えます。
+
+

復旧の手順

+
    +
  1. + 運営と相談し、全員の操作を止めてサーバーを停止する +

    + どの時点へ戻すかを決めます。戻した時点より後の記録は、復旧後の状態に含まれません。再入力が必要な記録を確保してください。 +

    +
  2. +
  3. + 現在の保存先を丸ごと別の場所へコピーする +

    + 壊れている可能性があっても、現行ファイルは消さずに保管します。サーバーを動かしたままファイルを差し替えないでください。 +

    +
  4. +
  5. + 復旧候補を、隔離した保存先で先に確かめる +

    + 候補の全記録JSONまたは退避JSONを、別のフォルダーへ + state.json という名前でコピーします。そのフォルダーを + CHROS_DATA_DIR + に指定し、別ポートで起動します。大会名・参加者・最新の結果・履歴が意図した時点か確認します。 +

    +
  6. +
  7. + 確認できた候補で本番の現行状態を置き換え、再起動する +

    + 隔離したサーバーを停止します。本番も停止したままであることを確かめ、候補JSONを本番保存先の + state.json + にコピーします。同じ保存先を指定して再起動し、運営と掲示を再読み込みして内容を照合します。 +

    +
  8. +
+
+ CSVは復旧用の全状態ではありません +

+ 復旧には全記録JSONを使います。壊れたJSONを削除して起動すると、新しいサンプル大会が作られる可能性があります。ファイルの削除を復旧手段にせず、元データを保全して原因を確認してください。 +

+
+
運営と合意した時点の記録が戻り、別画面への配信も確認できた。
+
+
+
+ 07 +

不具合を切り分ける

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 症状から調べる場所を決める +
症状最初に確認する場所次に見るもの
画面が開かない起動ターミナルのURL・エラーポート、LAN接続、待受アドレス、ファイアウォール
接続が続かない開発者ツールのNetworkで /api/stream + snapshot + の受信、プロキシのタイムアウト/バッファリング。SSEが接続中のままになるのは正常。 +
保存が拒否されるPOST /api/state の応答本文下表の状態コード、入力値、大会の進行状態
サンプルに戻ったように見える指定した CHROS_DATA_DIR と起動場所別の保存先・別ポートを見ていないか。未指定時は作業ディレクトリが基準。
起動後に保存状態を読めないサーバーログと state.jsonJSONの破損・状態スキーマとの不一致。先に保存先を保全する。
得点・勝者がおかしい元の得点・勝因・特殊勝者・計算方式 + evaluateGame → evaluateMatch → + standings の順に確認 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ POST /api/stateの主な応答 +
状態コード意味対処
400形式・入力・操作条件の不備応答の error と入力内容を確認します。
403別のOriginからの書き込み + 画面とAPIのホスト・ポートを合わせます。検証を無効化して回避しないでください。 +
409他の操作により更新番号が進んでいる + 返された最新状態を確認し、必要な操作だけ再送します。無条件に自動再送しないでください。 +
413要求のサイズが上限を超えたロゴ等のサイズと送信内容を確認します。
+
+

不具合報告に残すもの

+

+ 再現手順、期待した結果、実際の結果、対象コミット、計算方式、試合番号、端末側のエラー、サーバーログをまとめます。再現用データは架空の参加者で用意し、実大会の全記録JSONを公開PRへ添付しないようにします。 +

+
+
+
+ 08 +

この手順書を更新する

+
+

+ HTML・CSS・JavaScript・SVG図は + chros/public/manual/ + にまとまっています。スクリーンショットはBase64のデータURLとしてHTMLに埋め込み、PNG・JPEG等のバイナリはGitに追加しません。外部のフォントやライブラリーの読み込みはありません。フォルダーごとコピーすれば、サーバーなしでも + index.html を開いて読めます。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 資料のファイル構成 +
ファイル役割
index.html2部の入口・読み方
operations.html大会運営の手順
development.html開発・検証・起動・復旧の手順
assets/manual.css共通の見た目・スマートフォン表示・印刷
assets/manual.js印刷ボタン・目次の現在位置・画像の拡大と原寸表示
data-screenshotHTML内にBase64で埋め込んだ実画面。画像の識別名で差し替えます。
assets/*.svg大会の流れ・先後交代・データの流れの図
+
+
    +
  1. + 画面のボタン名と資料の表記を合わせる +

    + 見出し・入力欄・完了の目印も見直します。規則の説明を変えたときは、設計資料と運営手順の両方を更新します。 +

    +
  2. +
  3. + 架空データの環境で画像を撮り直し、HTMLへ埋め込む +

    + 本番の保存先を使わず、運営・掲示を実際に操作します。基本の撮影幅は1440px。説明するパネルをPNG・JPEG・WebPで撮影し、リポジトリの外へ一時保存します。次は、識別名 + score-entry の画像だけを差し替える例です。 +

    +
    node scripts/embed-manual-image.mjs \
    +  chros/public/manual/operations.html \
    +  score-entry /tmp/new-score-entry.png
    +

    + 画像はBase64に変換され、HTMLの + src に入ります。元の画像はGitへ追加しません。識別名はHTMLの + data-screenshot、説明は + alt と図のキャプションで管理します。 +

    +
  4. +
  5. + 通常表示・狭い画面・印刷を確認する +

    + /manual/index.html + を開き、2部のリンク、目次、画像の拡大、表の横スクロールを確認します。印刷プレビューで画像・表・見出しが切れていないか見ます。外部通信を止めた状態や、HTMLを直接開く方法でも確認します。 +

    +
  6. +
+

関連資料の使い分け

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ リポジトリのdocsフォルダーにある背景資料 +
パス読む目的
docs/prototype-proposal.md試作の判断D01〜D13、確認してほしい項目、検証範囲
docs/tournament-rules.md公開ルールと依頼者の運用説明の出典・違い
docs/technical-requirements.md現在の状態・API・保存・計算の仕様
docs/development.md命名・責務・コメント等の開発基準
docs/technical-roadmap.md将来構想。現在実装済みの機能一覧としては使わない。
+
+
画面と手順が一致し、画像・目次・印刷の確認が済んだ。
+
+ +
+
+ + diff --git a/chros/public/manual/index.html b/chros/public/manual/index.html new file mode 100644 index 0000000..eb32cf8 --- /dev/null +++ b/chros/public/manual/index.html @@ -0,0 +1,116 @@ + + + + + + + + 手順書の入口 | CHroS 手順書 + + + + + +
+ CHroS.FIELD GUIDE + +
+
+
+

CHASER PROGRESSION CONTROL SYSTEM / FIELD GUIDE

+

記録から、会場への掲示まで。
CHroSの使い方。

+

+ 大会を動かす人にも、システムを育てる人にも。
実画面と図でたどる、2部構成の手順書です。 +

+
+ 画面付きオフラインで閲覧印刷対応2026.09.23 / 1.0 +
+
+
+ 01 +

大会運営の手順書

+

当日の準備、参加者登録、2戦の記録、本戦作成、表示切り替え、終了後の保存。

+

対象:大会運営・記録・掲示の担当者

+ 運営の手順を開く →
02 +

開発の手順書

+

環境構築、コードの読み方、変更と検証、会場向けの起動、バックアップと復旧。

+

対象:開発者・保守担当・会場の技術担当

+ 開発の手順を開く →
+
+
+

いま必要な手順から

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
やりたいこと参照先
初めて大会を準備する大会設定と名簿の準備
試合が終わったので得点を入れる2戦の記録と勝因
入力を間違えた/再試合になった訂正と仕切り直し
会場の画面を切り替えたい掲示と表示の進行表
手元でアプリを動かす初回起動
保存した大会を復旧したい技術担当向けの復旧手順
+
+
+
+ この資料の読み方 +

+ 運営向けは操作場所・手順・完了の目印を一組にしてあります。スクリーンショットは押すと拡大できます。印刷する場合は右上の「印刷 + / PDF保存」を使い、ブラウザーの印刷画面で保存先や用紙を選んでください。 +

+
+

+ 画面写真はHTML内にBase64で埋め込んでいます。画像ファイルや外部サービスへの接続は不要です。フォルダーをコピーして使う場合は、HTMLとassetsをまとめて保存してください。JavaScriptを無効にしても本文・画像・目次を読めます。 +

+ +
+ + diff --git a/chros/public/manual/operations.html b/chros/public/manual/operations.html new file mode 100644 index 0000000..4e41158 --- /dev/null +++ b/chros/public/manual/operations.html @@ -0,0 +1,900 @@ + + + + + + + + 大会運営の手順書 | CHroS 手順書 + + + + + +
+ CHroS.FIELD GUIDE + +
+
+ +
+
+

PART 01 / OPERATIONS

+

大会運営の手順書

+

+ 準備から記録、掲示、終了後の保存まで。画面のどこを押し、何を確かめればよいかを、当日の流れに沿って説明します。 +

+
+ 運営・記録・掲示の担当者向け対人戦の試作版更新 2026.09.23 +
+
+
+
+ 01 +

まず、今日の流れをつかむ

+
+

+ CHroSは、対人戦の得点と勝因を記録し、試合結果・予選順位・会場掲示をつなぐ道具です。CHaserの試合そのものは、別の試合サーバーで行います。この手順書では、釧路向けの「先後を交代する2戦」を基本に説明します。 +

+
+ 準備、開会、総当たり予選、本戦トーナメント、閉会と記録保存の5段階 +
進行の全体像。休憩や幕間の表示は、必要なタイミングで挟めます。
+
+
+
+ 運営担当の画面 +

記録する・画面を選ぶ

+

+ 担当者から案内された運営画面を開きます。アドレスの末尾は + /console。参加者・得点の入力と、会場の表示切り替えをここで行います。 +

+
+
+ 参加者に見せる画面 +

会場に投影する

+

+ 運営画面の右上の「会場スクリーン ↗」を開きます。末尾は + /display。投影用モニターに移動し、ブラウザーを全画面にします。 +

+
+
+

+ 初回起動や別の端末からの接続は、技術担当向けの起動手順を参照してください。掲示端末も同じサーバーにつなぎます。 +

+
+ 「1戦」と「1試合」を区別します +

+ CHaserを1回行うのが1戦。第1戦と第2戦の結果をまとめるのが1試合です。得点は各戦が終わってから入力し、2戦終了後に試合の勝者が決まります。 +

+
+
+ 運営画面。左にメニュー、中央に進捗と会場プレビュー、右に表示の進行表がある +
+ 図1|運営のホーム画面。中央のプレビューが会場の表示。得点入力欄はその下に続きます。右の進行表で用意した順に表示できます。 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 迷ったときの操作場所 +
やりたいこと開く場所
いまの試合を入力する・掲示を切り替えるコントロール
過去の記録を直す・順位を見る・本戦を作る対戦・記録
参加者と予選グループを登録する参加者
大会設定・ロゴ・書き出し・操作履歴設定・履歴
+
+
+
+
+ 02 +

大会を準備する

+
+
設定・履歴 → 記録を手元に/大会の設定
+
    +
  1. + 練習用と本番用の大会を区別する +

    + 初回は架空の参加者が入ったサンプルです。実際の大会を登録するときは「空の大会を作成」を押し、内容を確認して「確認して実行」を選びます。すでに記録がある場合は、先に「全記録JSON」を保存します。 +

    +
  2. +
  3. + 大会名・計算方式・進出人数を入力する +

    + 「大会の設定」で、大会名とサブタイトル、主催者が確認した計算方式、各グループから本戦へ進む人数を指定します。「設定を保存」を押します。 +

    +
  4. +
  5. + 必要なら幕間のロゴを登録する +

    + 「ロゴ画像を選ぶ」でPNG・JPEG・WebPの画像を選びます。容量は1 + MB以下。選んだ時点で保存されるので、会場に見せる前にプレビューで確認します。 +

    +
  6. +
+
+ 大会を切り替える前に確認 +

+ 「空の大会を作成」「サンプルに戻す」は、現在の大会全体を切り替えます。元の大会はサーバーに退避されますが、画面から元に戻す機能はありません。本番中の練習には使わず、別の練習環境を技術担当に用意してもらってください。 +

+
+
+ 大会設定の入力欄と、結果CSV・全記録JSON・空の大会作成のボタン +
+ 図2|設定と保存。左側で大会名と方式を設定し、右側から記録を書き出します。撮影用の空の大会を使っています。 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 試合を始める前に主催者と合わせる項目 +
項目現在の動き確認すること
釧路向けの試合判定2戦の特殊P合計 → 同じなら合計得点採用する判定順と一致しているか
釧路向けの予選順位試合勝数 → 特殊P → 合計得点同順位の裁定方法と進出枠
本戦の配置2組から各2名ならA1−B2、B1−A2当日のシード・抽選の運用
+
+

+ この表は現在の試作が採用した計算・配置です。全大会の公式ルールを保証するものではありません。旭川向けは対人戦の計算のみで、Bot予選や全国固有の採点方式は未対応です。 +

+
大会名が画面上部に反映され、計算方式と進出人数を確認できた。
+
+
+
+ 03 +

参加者を登録し、予選を作る

+
+
+ 参加者 → 参加者を追加 → 予選の組み合わせを作成 +
+
    +
  1. + 表示名・所属・予選グループを登録する +

    + 右側の入力欄に記入し、「参加者を追加 + +」を押します。表示名が重複すると登録できません。同姓同名は参加者を区別できる名前にします。 +

    +
  2. +
  3. + 全員の一覧と組分けを読み合わせる +

    + 全員で総当たりなら全員を「A」にします。グループ予選なら「A」「B」などを使い分けます。各グループは2名以上、全体で128名までです。誤りは一覧の「編集」で直します。 +

    +
  4. +
  5. + 組み合わせを作成する +

    + 「予選の組み合わせを作成 + →」を押し、確認画面で「確認して実行」を選びます。同じグループの全ペアが1試合ずつ作成されます。 +

    +
  6. +
+
+ A組とB組に2名ずつ登録した参加者一覧と追加フォーム。下に予選の組み合わせを作成ボタン +
+ 図3|予選作成前の登録例。架空の4名を2組に分けています。全員の確認を済ませてから、左下のボタンを押します。 +
+
+
+ 組み合わせ作成後は、参加者の追加・削除と組変更ができません +

+ 表示名と所属は編集できます。名簿や組分けを見直したい場合は、試合を始める前に運営責任者へ相談してください。 +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 作成される予選の試合数の例(1試合=2戦) +
組分け試合数実施する戦数
4名を1組6試合12戦
8名を1組28試合56戦
8名を4名ずつの2組6+6=12試合24戦
+
+
「対戦・記録」に予定した数の対戦が並び、参加者の組が正しい。
+
+
+
+ 04 +

1試合を、2戦とも記録する

+
+
コントロール → 掲示する試合・得点入力
+
+ 先後は第2戦で反転するが、左右の入力欄の人物は変わらない。若葉と高原の例 +
+ 先攻を入れ替えても、入力欄は同じ人物のままです。色だけで判断せず、名前を確認します。 +
+
+
    +
  1. + 掲示する試合を選ぶ +

    + 「掲示対象の試合」で試合番号と対戦者を確認します。会場が「対戦・得点」や「試合結果」を表示中なら、試合の選択変更もすぐに会場へ反映されます。 +

    +
  2. +
  3. + じゃんけんで決まった第1戦の先攻を登録する +

    + 「第1戦の COOL・先攻」で確定した参加者を選び、「試合を開始する + →」を押します。じゃんけんの勝者から先攻を自動判定することはありません。 +

    +
  4. +
  5. + 第1戦が終わったら、両者の得点と勝因を入力する +

    + 記録用紙・試合サーバーの結果と名前を照合します。「得点比較」以外なら勝利した参加者も選びます。入力中の「この戦の判定」を確認し、「第1戦を記録 + ↗」を押します。 +

    +
  6. +
  7. + 実際の試合も先後を交代し、第2戦を行う +

    + 入力画面のCOOL/HOTは自動で反転します。CHaser側の交代は担当者が行います。終了後、第2戦の得点・勝因を入力して「第2戦を記録 + ↗」を押します。 +

    +
  8. +
  9. + 試合の勝者と合計を確認して掲示する +

    + 2戦がそろうと試合判定と予選順位が更新されます。「試合結果」ボタンで勝者を掲示します。同じ比較値になった試合は再試合の手順へ進みます。 +

    +
  10. +
+
+ B-06の未開始画面。第1戦のCOOL・先攻を選び、試合を開始する +
+ 図4|開始前。試合番号・2名の名前・第1戦の先攻を確認します。 +
+
+
+ B-05の第2戦。大地15点、高原18点。大地はHOT、高原はCOOLで、保存前の判定は高原の勝利 +
+ 図5|第2戦の入力例。この戦の勝者は高原ゆうですが、2戦を合計した試合の勝者は大地れんです。「この戦」と「試合」の判定を区別します。 +
+
+
+ 保存と画面切り替えは、別の操作です +

+ 保存すると得点と集計は更新されますが、掲示画面の種類は自動で変わりません。「試合結果」を見せたいときは、そのボタンも押してください。 +

+
+
第1戦・第2戦の両方が「記録済み ✓」となり、試合が「確定」になった。
+
+
+
+ 05 +

勝因を選ぶ・判定を読み取る

+
+

+ 得点欄には、勝因にかかわらず試合終了時の元の得点を入力します。特殊ポイントや換算後の値を、手計算して得点欄へ入れる必要はありません。 +

+
+ + + + + + + + + + + + + + + + + + + + + + +
+ 勝因の選び方 +
画面の選択肢記録する内容
得点比較通常の得点で勝敗が決まった場合。両者の得点から勝者を計算します。
PUT/相手を包囲審判が確定した勝因と、勝った参加者を選びます。
相手の通信切断/相手の移動違反/相手の自己包囲 + 「相手」は勝者から見た相手です。原因を起こした参加者ではなく、勝った参加者を指定します。 +
+
+
+ 若葉12点、高原20点でPUTを選択し、勝利した参加者として若葉を指定した入力例 +
+ 図6|特殊勝利の入力。点が少なくても、PUTで勝った若葉ひなたを指定します。勝因と勝者の両方を確認してください。 +
+
+

釧路向け:特殊P → 合計得点の順に比較

+

+ 得点比較以外の勝利を1回につき1特殊Pとして集計します。2戦の特殊Pが同じときだけ合計得点を比べます。予選順位に入るのは、勝者が確定した試合です。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 特殊Pの例:合計得点だけでは勝敗を決めません +
記録若葉ひなた高原ゆう勝因
第1戦12点・1特殊P20点・0特殊P若葉がPUTで勝利
第2戦5点・0特殊P35点・0特殊P高原が得点比較で勝利
2戦合計17点・1特殊P55点・0特殊P若葉が試合の勝者
+
+

旭川向けを選んだ場合

+

+ 2戦のゲーム勝数を先に比べ、同じなら換算得点を比べます。PUT・包囲による敗者は0点、通信切断・移動違反・自己包囲による敗者は「残りターン数のマイナス」に換算されます。後者では「終了時の残りターン」も入力してください。秒数ではありません。 +

+
+ サーバー障害を、そのまま通信切断負けとして登録しない +

+ 「相手の通信切断」は、審判が参加者に起因する敗北と裁定した場合に選びます。タイムアウトや試合サーバーの故障は、審判の判断を受けて仕切り直しを行います。 +

+
+
+
+
+ 06 +

入力を訂正する・試合をやり直す

+
+
対戦・記録 → 対戦と記録 → 対象の試合
+
+
+ 入力値を間違えた +

結果の訂正

+
    +
  1. 一覧から対象試合を選び、第1戦または第2戦を開きます。
  2. +
  3. 正しい値に直し、「訂正理由」を入力します。
  4. +
  5. 「訂正を保存 ↗」を押します。
  6. +
  7. 集計と「設定・履歴」の操作履歴を確認します。
  8. +
+

例:「記録用紙と照合し、高原ゆうの得点を18に訂正」

+
+
+ 2戦とも再実施する +

両戦の仕切り直し

+
    +
  1. 「両戦を仕切り直す」を押します。
  2. +
  3. 同点・障害などの理由を記入します。
  4. +
  5. 「理由を記録して仕切り直す」を押します。
  6. +
  7. 新マップ等を審判と確認し、先攻登録からやり直します。
  8. +
+

旧結果は無効化して履歴に残ります。第1戦の得点も持ち越しません。

+
+
+
+ 仕切り直しは、理由を記録するボタンを押すと実行されます +

+ 入力訂正だけなら「訂正を保存」を使ってください。本戦を作成した後の予選修正と、次の本戦試合が開始済みで進出者が変わる修正は拒否されます。制限に当たったときは、運営責任者と技術担当に相談してください。 +

+
+
+ 訂正後の結果が更新された、または先攻を登録し直す画面に戻り、理由が履歴に残った。 +
+
+
+
+ 07 +

予選を締め、本戦に進む

+
+
対戦・記録 → 順位・トーナメント
+
    +
  1. + すべての予選試合を確定させる +

    + 未入力・対戦中・再試合待ちの試合が残っていないか確認します。予選がすべて確定するまで、本戦の作成はできません。 +

    +
  2. +
  3. + 順位・進出者・シード順を主催者と確認する +

    + 「本戦へ進む」に候補が上からシード順で並びます。同順位の裁定や候補の順番変更には、理由を記録します。順位が明確な上位者を外して下位者を進出させることはできません。 +

    +
  4. +
  5. + 本戦を確定する +

    + 「進出者を確定して本戦を作成 + →」を押し、最後に確認画面を読みます。誤りがあれば「キャンセル」、確定するなら「確認して実行」を選びます。 +

    +
  6. +
  7. + 本戦表を掲示し、各試合を進める +

    + 「コントロール」で進行区分を「本戦」にし、「本戦トーナメント」を掲示します。各試合は予選と同じ2戦記録です。勝者は次の枠へ自動で入り、不戦進出の枠には得点を入れません。 +

    +
  8. +
+
+ A組とB組の予選順位表、各組上位2名の本戦候補、裁定理由の欄 +
+ 図7|本戦を作る前の確認。順位表と進出者を見比べます。上からA1、B1、A2、B2の順が初期候補です。 +
+
+
+ 本戦確定後は予選結果を変更できない旨を示す確認画面。キャンセルと確認して実行の2ボタン +
+ 図8|本戦作成の最終確認。実行すると予選結果が保護されます。作成後に取り消すボタンはありません。 +
+
+
+ 青葉対大地、星野対北野の準決勝と、勝者待ちの決勝が並ぶ本戦表 +
+ 図9|作成された本戦表。準決勝の勝者が確定すると、決勝の対戦者が入ります。 +
+
+
本戦表の対戦者が合っており、会場にも同じ組み合わせが見えている。
+
+
+
+ 08 +

会場に見せる画面を選ぶ

+
+

+ 進行区分=いま何の時間か、画面=何を見せるか、掲示対象=どの試合かを別々に選びます。「本戦」に変えても本戦表へ自動で切り替わるわけではありません。 +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 6つの画面ボタンの使いどころ +
ボタンこんなときに一緒に確認するもの
01 進行案内開会式・閉会式などを案内する進行区分
02 対戦・得点対戦者と、終了済みの戦の得点を見せる掲示対象の試合
03 試合結果2戦を終えた試合の勝者を発表する掲示対象と「確定」の状態
04 予選順位グループごとの順位を見せる長い表のページ番号
05 本戦トーナメント本戦全体の組み合わせ・進行を見せる本戦作成済みか、ページ番号
06 幕間・ロゴ休憩中に大会ロゴを見せる進行区分と登録した画像
+
+
+ B-05の試合結果。大地れん54点、高原ゆう42点で、大地れんが勝者と表示される会場画面 +
+ 図10|会場側の試合結果。第2戦の入力後は、合計54対42で大地れんが勝者になります。試合中の逐次更新には未対応です。 +
+
+

あらかじめ表示の順番を用意する

+
    +
  1. + 「表示の進行表」の「編集」を押す +

    + 項目の名前・進行区分・画面を設定します。矢印で順番を変え、「+ + 画面を追加」で増やせます。 +

    +
  2. +
  3. + 「表示順を保存」を押す +

    入力しただけでは保存されません。編集を終えたら一覧で順序を確認します。

    +
  4. +
  5. + 当日は項目を押すか「次へ」で進める +

    + 進行区分と画面が一緒に切り替わります。対戦画面は、その時点の「掲示対象の試合」を使います。時刻による自動再生ではありません。 +

    +
  6. +
+
+ 進行表の編集。各項目に名前、進行区分、画面の選択欄と上下移動・削除ボタンがある +
+ 図11|表示の進行表。編集したら下部の「表示順を保存」を押します。画面ごとに試合を固定する機能はありません。 +
+
+
+ 手動で画面を変えた後は、進行表の現在位置を選び直す +

+ 画面ボタンや進行区分の手動変更で、進行表の選択は解除されます。そのまま「次へ」を押すと先頭から始まるので、続けたい項目を一度選んでください。長い表のページも「前のページ」「次のページ」で手動で送ります。 +

+
+
+ 運営のプレビューと会場スクリーンが一致し、試合・進行区分・ページが意図どおりになった。 +
+
+
+
+ 09 +

閉会後に記録を残す

+
+
    +
  1. + 決勝の結果を確認して、閉会式を掲示する +

    進行表の「閉会式」を選ぶか、進行区分を「閉会式」にして「進行案内」を押します。

    +
  2. +
  3. + 「設定・履歴」からCSVとJSONを保存する +

    + 「結果CSV」は各戦の結果を表計算ソフトで確認するためのファイルです。「全記録JSON」は大会の設定・結果・操作履歴を含み、復旧にも使えます。ダウンロード先を確認し、大会名と日付が分かる場所に保管します。 +

    +
  4. +
  5. + 引き継いでから、技術担当がサーバーを終了する +

    + 両ファイルが手元にあることを確認します。画面を閉じるだけではサーバーは停止しません。保管と停止は担当者同士で確認してください。 +

    +
  6. +
+
結果CSVと全記録JSONの2種類を保存し、保管先を次の担当者に共有した。
+

当日用の確認リスト

+

印刷して使えます。画面上のチェックは大会データに保存されません。

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+ 10 +

困ったときに見る早見表

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 表示やメッセージから次の操作を探す +
症状確認と対応
「再接続中…」で入力できない + 会場LANとサーバーの稼働を技術担当に確認します。画面は最後に受信した値の可能性があります。保存失敗時は記録用紙を残し、復帰後に保存済みの内容と照合します。 +
「別の操作で更新されました」 + 他の担当者の操作が先に保存されました。最新の記録を確認し、必要な入力だけやり直します。同じ結果を確認せずに連続保存しないようにします。 +
結果を保存したのに、勝者の画面にならない + 2戦とも記録済みか確認し、掲示対象を合わせて「試合結果」を押します。保存だけでは画面の種類は変わりません。 +
別の試合が会場に映っている + 対戦一覧で開いた試合と、掲示対象は別です。「この試合を掲示対象にする + ↗」を押すか、コントロールで掲示対象を選びます。 +
本戦を作成できない + 未記録・再試合待ちの予選、重複した進出者、各組の人数、同順位やシード変更の理由を確認します。 +
予選結果が保護されている + すでに本戦を確定しています。画面から解除する機能はありません。運営責任者と技術担当に相談してください。 +
画像を登録できない + PNG・JPEG・WebP、1 + MB以下か確認します。画像を選ぶ前の状態もプレビューで見直します。 +
大会を間違って切り替えた + そのまま入力を進めず、技術担当に退避データからの復旧を依頼します。「サンプルに戻す」は元の大会への復元ではありません。 +
+
+
+ 最初の練習はサンプルのB-05がおすすめ +

+ 操作に慣れるため、練習環境でサンプルのB-05を使います。第2戦に大地れん15点・高原ゆう18点を入力し、合計54対42で大地れんが勝者になることを確認してください。本番の大会データでは練習しないでください。 +

+
+
+ +
+
+ + diff --git a/docs/development.md b/docs/development.md index 8acfa25..15ec53f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,8 @@ # コードの読みやすさと検証 +初回起動から検証・会場向け起動・復旧までの流れは、[HTML版の開発手順書](../chros/public/manual/development.html)で説明しています。 +起動中のアプリでは `/manual/development.html` を開けます。[2部構成の資料案内](./manuals.md)も参照してください。 + ## 日常の確認 ```sh @@ -11,6 +14,7 @@ pnpm test PRではGitHub Actionsが整形・型・テストを確認します。Prettierの版と設定を固定し、 インデントや改行の差を持ち込まないようにしています。生成物と大会の保存データは整形対象外です。 +HTML手順書のHTML・CSS・JavaScriptも整形対象です。画像の更新方法は `docs/manuals.md` にまとめています。 整形チェックだけでは命名や責務分割の良し悪しは判断できないため、以下もレビューで確認します。 ## 書き方 diff --git a/docs/manuals.md b/docs/manuals.md new file mode 100644 index 0000000..25cfe34 --- /dev/null +++ b/docs/manuals.md @@ -0,0 +1,57 @@ +# HTML手順書 + +更新日:2026-09-23。実装の確認基準は、PR #2を取り込んだdevelopの `f00a3f2` です。 + +## 読み方 + +アプリを起動して `/manual/index.html` を開きます。サーバーなしで読む場合は +`chros/public/manual` をフォルダーごと保存し、`index.html` をブラウザーで開いてください。 +GitHubのファイル画面ではHTMLソースが表示されます。GitHub Pages等の公開設定は含めていません。 + +- [第1部:大会運営](../chros/public/manual/operations.html):準備、参加者、2戦記録、勝因、訂正・再試合、本戦、掲示、記録保存。 +- [第2部:開発](../chros/public/manual/development.html):初回起動、構成、変更、検証、会場起動、保存・復旧、調査、資料更新。 + +画像の拡大、目次、印刷ボタンを用意しています。スマートフォンでは目次を本文の前へ移し、 +横長の表は表の内側でスクロールできます。JavaScriptなしでも本文と画像は読めます。 + +## 作成時の判断 + +| 判断 | 理由 | +| --- | --- | +| 2部と共通の入口を作る | 当日の運営と、開発・復旧の読者を分ける | +| 操作場所・手順・完了の目印を一組にする | ボタンを押した後に何を確認するか分かるようにする | +| 11枚の実画面をHTML内へBase64で埋め込む | PNG/JPEG等のバイナリをGitへ追加しないという依頼者の指定 | +| 図はテキスト形式のSVGにする | Gitで差分を確認でき、拡大しても文字が読める | +| 外部配信のフォント・ライブラリーを使わない | 会場LANやオフラインでも資料を読めるようにする | +| 本番データと別の保存先で撮影する | 実大会の参加者情報や進行状態を変更・掲載しない | +| 計算方式を「現在の試作」として説明する | 採用した判断と、全大会の公式規則を混同しない | +| 設計の細部は既存Markdownへ残す | 手順書を当日の操作と開発の入口に集中させる | + +## スクリーンショットの更新 + +元のPNG・JPEG・WebPはリポジトリの外に撮影し、画像の識別名を指定して埋め込みます。 + +```sh +node scripts/embed-manual-image.mjs chros/public/manual/operations.html score-entry /tmp/new-score-entry.png +pnpm format +pnpm format:check +``` + +識別名はHTMLの `data-screenshot` にあります。更新されるのは該当する画像の `src` だけです。 +構図や内容が変わった場合は、代替テキスト・寸法・キャプションも更新します。 +元画像の追加は不要です。SVG図は `assets/*.svg` を直接編集します。 + +HTML・CSS・JavaScriptは通常の整形検査に含めています。画面の文言、リンク、画像の拡大、 +狭い画面、印刷プレビュー、オフライン表示を確認してから変更を提出してください。 + +文章・体裁をGeminiで確認した際の指摘と対応判断は、 +[レビュー記録](reviews/2026-09-23-gemini-manuals/README.md)に残しています。 + +## 作成時の確認 + +- 本番ビルドと、本番サーバーからの3ページの配信を確認。 +- PC幅1440px・スマートフォン幅390pxで、ページ全体の横はみ出しがないことを確認。 +- 11枚の埋め込み画像、目次・資料間リンク、画像の拡大・原寸表示・Escでの終了を確認。 +- 外部通信を止め、JavaScriptを無効にしてHTMLを直接開いても本文・図・画像を閲覧可能。 +- A4印刷の全ページを画像化し、図表・ページ番号・改ページを確認。確認用PDFやPNGはGit対象外。 +- 埋め込みツールの正常更新、他のHTMLを維持すること、不明な識別名・不正な画像で更新しないことを確認。 diff --git a/docs/reviews/2026-09-23-gemini-manuals/README.md b/docs/reviews/2026-09-23-gemini-manuals/README.md new file mode 100644 index 0000000..81ee43e --- /dev/null +++ b/docs/reviews/2026-09-23-gemini-manuals/README.md @@ -0,0 +1,98 @@ +# GeminiによるHTML手順書のレビュー + +実施日:2026-09-23。依頼者が用意した `gemini-docs-check` を実行し、文章・体裁の指摘を手順書へ反映しました。スクリプト本体は変更していません。 + +## 実行条件 + +- 修正前の対象:`a749c616f5748ddeeb7feaf09f6f6088302dd05a` の `operations.html` と `development.html`。 +- 指摘を反映した手順書:`ab6e658`。 +- モデル:スクリプト既定の `gemini-3.6-flash`。 +- 表示条件:1440 × 900、デバイス倍率2、全ページ、light、待機500ms、高さ上限20,000px。 +- 本文を抽出したテキストと、Chromiumで描画した画像を別々に送信しました。 +- 結果はJSONで保存しました。以下の保存ファイルでは、入力HTMLの絶対パスをリポジトリ相対パスに置き換え、モデル名を追記しています。指摘内容はそのままです。 +- PNG・PDFは検証用の一時ファイルだけに保存し、Gitには追加していません。HTML内のBase64画像も変更していません。 + +スクリプトのフォルダーで、次のように実行しました。`CHROS_REPO` はこのリポジトリの絶対パスへ置き換えます。 + +```sh +CHROS_REPO=/absolute/path/to/CHaserProgressionControlSystem +.venv/bin/docs-check "$CHROS_REPO/chros/public/manual/operations.html" \ + --format json --save-screenshot /tmp/chros-operations.png \ + > /tmp/chros-operations.json +.venv/bin/docs-check "$CHROS_REPO/chros/public/manual/development.html" \ + --format json --save-screenshot /tmp/chros-development.png \ + > /tmp/chros-development.json +``` + +## 初回の指摘と判断 + +運営は8件(error 4・warning 4)、開発は6件(warning 6)でした。両方とも終了コードは1です。これは既定の `--fail-on warning` による正常な指摘検出です。 + +原文は [運営の初回結果](operations-before.json) と [開発の初回結果](development-before.json) に保存しています。表の番号は各JSONの `findings` の順番です。Geminiの引用には誤読が含まれるため、実際の仕様や正しいコマンドとして使わないでください。 + +| 番号 | 指摘の要旨 | 判断・対応 | +| --- | --- | --- | +| 運営1 | 練習用の得点例は章と無関係なので削除する | 削除は見送り。直前の「最初の練習はサンプルのB-05がおすすめ」という見出しが本文抽出から抜けていました。サンプル実装の第1戦は39対24で、第2戦の15対18と合わせて54対42になります。例を残し、本文にも練習環境とB-05を明記しました。 | +| 運営2 | 04章の図版が空白 | 撮影時の未読込を確認。画像の遅延読み込みを外しました。 | +| 運営3 | 07章の画面画像が空白 | 同上。本戦表はスクロール後には読み込まれ、画像自体は存在していました。 | +| 運営4 | 08章の画面画像が空白 | 同上。初期表示だけで撮影しても全画像が読み込まれるようにしました。 | +| 運営5 | 結果訂正の手順が常体 | 採用。4つの操作説明を「です・ます」に統一しました。 | +| 運営6 | 仕切り直しの手順が常体 | 採用。4つの操作説明を「です・ます」に統一しました。 | +| 運営7 | 「相談します」の指示表現が不統一 | 案内表現の統一として採用。「相談してください」にそろえました。「主語と述語の不一致」という分類までは採用していません。 | +| 運営8 | 冒頭の「行います」の途中で改行される | 見送り。指定のPC幅で強制改行やはみ出しを再現できず、狭い画面でも通常の日本語の折り返しでした。特定の単語だけを改行禁止にはしていません。 | +| 開発1 | 開発基準の箇条書きが常体 | 採用。「です・ます」に統一し、同じ形式の応答コード表の説明もそろえました。 | +| 開発2 | 型検査の説明「終了すること」が不統一 | 採用。「エラーなしで終了することを確認します」に変更し、同じ表の説明を敬体にそろえました。 | +| 開発3 | 確認リストの句点がない | 採用。確認済みの状態を表す文にそろえ、句点を付けました。「画面切替」も「画面切り替え」に統一しました。 | +| 開発4 | pnpm導入コマンドが枠外にはみ出す | はみ出しは再現しませんでした。読みやすさの改善として、段落内のコマンドを独立したコード欄へ移しました。Geminiが誤読した引数・バージョンは採用せず、`npm install --global pnpm@9.12.0` を維持しています。 | +| 開発5 | 会場用の起動コマンドが枠外にはみ出す | はみ出しは再現しませんでしたが、長いコマンドをシェルの行継続で分割しました。検証用の起動コマンドも同じ形式にしました。 | +| 開発6 | 画像差し替えコマンドが枠外にはみ出す | 同上。正しいスクリプト名と引数を維持して行を分割しました。横スクロールと複数行のコピー方法も説明しました。 | + +## スクリプトのテストで分かったこと + +### 本文抽出で文脈が抜ける場合がある + +`extract_blocks()` の対象には、単独の `strong` や `pre` が含まれません。今回の注意書き見出しは運営8個・開発5個、手順の見出しは運営21個・開発15個あり、これらは文章チェックの入力に含まれていませんでした。特に練習例の削除指摘には、この文脈欠落が影響したと考えられます。 + +また、抽出されたテキストにはHTML整形用の改行・空白が残ります。文章チェックの「改行」は、そのままブラウザー上の強制改行を意味しません。 + +### 全ページ撮影と遅延読み込みは別の処理 + +初回の運営手順書では13画像中10画像が撮影直前に未読込でした。`networkidle` と500ms待機だけでは、画面外の `loading="lazy"` 画像は読み込まれません。本戦表について、初回撮影画像は空白、スクロール後は正常表示になることを比較しました。 + +今回はオフラインでも使う静的手順書で、スクリーンショットはすでにHTML内にあります。手順書側の遅延読み込みを外し、全ページ撮影・印刷でも図を利用しやすくしました。汎用チェッカーとして使う場合は、全ページをスクロールして画像の読み込みを待つ処理も検討できます。 + +### 縦長の画像では誤読も起きる + +初回の送信画像は運営2,880 × 38,180px、開発2,880 × 24,496pxでした。Geminiは「2戦」を「2敗」、`pnpm@9.12.0` を別のバージョン、画像更新スクリプト名を存在しない名前として引用しています。長い画像1枚の判定だけで本文やコマンドを変更しないことにしました。章単位の分割撮影は、チェッカーの改善候補です。 + +PCでは20,000pxの高さ上限内に収まりましたが、修正後の運営手順書は390px幅で21,544pxあります。スマートフォン条件で既定の全ページ撮影をすると末尾が対象外になるため、高さ上限や分割を明示する必要があります。 + +### API制限と指摘検出を区別する + +再確認の同時実行では、無料枠のリクエスト制限(HTTP 429、limit 5)に当たりました。スクリプト内の再試行でも解消せず、サーバーが示した待機時間を過ぎてから運営手順書だけを再実行しましたが、今度はlimit 20のHTTP 429で終了しました。エラー文だけでは制限の集計期間を確定できないため、それ以上の再試行やモデル・課金設定の変更は行っていません。 + +APIエラー時も終了コードは1ですが、指摘検出時と違って結果JSONは出力されません。文章・体裁の両方が終わってから出力するため、途中まで成功したチェックもJSONには残らない構成です。自動実行では標準エラーとJSONの有無も確認する必要があります。 + +## 修正後の再レビュー状況 + +Geminiによる再レビューは上記のAPI制限で未完了です。修正後の指摘数は不明であり、「指摘ゼロ」や「Geminiの検査に合格」とは扱っていません。初回の指摘への対応と、次のローカル検証は完了しています。利用枠が回復した後に、同じコマンドで再確認できます。 + +## ローカル検証 + +- 1440px・390pxの両幅で2つの手順書を描画し、ページ全体の横はみ出しがないことを確認しました。 +- スクロール前に運営13画像・開発1画像の読み込みが完了しました。運営の11枚のBase64画像を含め、画像の内容は修正前と同一です。 +- ページ内リンクと、注意書き内の強調語が通常の文章として表示されることを確認しました。 +- 既存10個のコード欄は行継続・空白を除いて同一で、新たなコード欄は既存のpnpm導入コマンドを移したものです。全コード欄が `zsh -n` の構文検査に通りました。 +- A4印刷は運営16ページ・開発12ページです。全体の改ページと、変更した箇所の描画を確認しました。 +- リポジトリ全体の整形検査と `git diff --check` が成功しました。 + +## 使用したスクリプトの識別情報 + +再実行時の比較用に、使用時点のSHA-256を記録します。APIキーや設定ファイルは含めません。 + +| ファイル | SHA-256 | +| --- | --- | +| `cli.py` | `4a56eff7745ecf46c485bd314be62531cf9c5bba6e9db21d5c1c30ffba7b3cc2` | +| `extract.py` | `c144ece22897852ee3b7f9a9648a17eb9a0a45ac6620590009a25525996d6677` | +| `render.py` | `72f095351d7341b119440939fba4b8ec69743295eeac04f13f202deb91f5535b` | +| `review.py` | `f503d5dabc795fdbfb47ecb35833b1409641be20fb8ffb2e8030c80e3bd999c4` | diff --git a/docs/reviews/2026-09-23-gemini-manuals/development-before.json b/docs/reviews/2026-09-23-gemini-manuals/development-before.json new file mode 100644 index 0000000..cf7159a --- /dev/null +++ b/docs/reviews/2026-09-23-gemini-manuals/development-before.json @@ -0,0 +1,69 @@ +{ + "model": "gemini-3.6-flash", + "file": "chros/public/manual/development.html", + "viewport": { + "width": 1440, + "height": 900, + "device_scale": 2.0, + "full_page": true, + "color_scheme": "light", + "wait_ms": 500, + "max_height_px": 20000 + }, + "findings": [ + { + "check": "text", + "severity": "warning", + "category": "敬体・常体の混在", + "location": "[88]", + "quote": "役割が伝わる名前を使う。", + "suggestion": "役割が伝わる名前を使います。", + "reason": "他の箇条書き([65]〜[68])や本文では敬体(です・ます調)が使用されていますが、[88]〜[92]の箇条書きでは常体(辞書形)になっており文体が統一されていません。" + }, + { + "check": "text", + "severity": "warning", + "category": "敬体・常体の混在", + "location": "[105]", + "quote": "エラーなしで終了すること。", + "suggestion": "エラーなしで終了することを確認します。", + "reason": "同一表内の他の説明文([103]や[107]など)が敬体(です・ます調)で書かれているのに対し、本箇所のみ体言止め・指示文の形式になっており不統一です。" + }, + { + "check": "text", + "severity": "warning", + "category": "句読点・記号の不統一", + "location": "[118]", + "quote": "保存と画面切替が反映される", + "suggestion": "保存と画面切替が反映される。", + "reason": "他の箇条書き([65]〜[68]など)では文末に句点が付与されていますが、[118]〜[122]では文末の句点が省略されており不統一です。 " + }, + { + "check": "layout", + "severity": "warning", + "category": "レイアウト崩れ", + "location": "「01 まず手元で起動する」直下の注記テキスト内", + "quote": "npm install -g global pnpm@8.12.0", + "suggestion": "長い文字列やインラインコードには適切な折り返し処理(`word-break: break-all`など)を適用し、コンテンツ領域内に収まるように調整してください。", + "reason": "インラインコードの背景グレー領域が本文テキストエリアの右枠を大きく食い破って右側にはみ出しています。" + }, + { + "check": "layout", + "severity": "warning", + "category": "レイアウト崩れ", + "location": "「05 会場向けに起動する」の「2 大会毎の保存先を指定し、LANへ公開して起動する」直下のコードブロック", + "quote": "CHROS_DATA_DIR=/absolute/path/to/event-data pnpm --filter chros exec next start --hostname 0.0.0.0 --port 3000", + "suggestion": "コードブロックに対して横スクロール(`overflow-x: auto`)を有効にするか、改行を入れて枠内に収まるようにしてください。", + "reason": "長いコマンド文字列がコードブロック(黒枠)の右端を突き破ってはみ出しています。" + }, + { + "check": "layout", + "severity": "warning", + "category": "レイアウト崩れ", + "location": "「08 この手順書を更新する」の「2 閲覧データの環境で画像を撮り直し、HTMLへ埋め込む」直下のコードブロック", + "quote": "node scripts/embed-manual-imgs.cjs public/chroms/public/manual/operations.html 1440", + "suggestion": "コードブロックに対して横スクロール(`overflow-x: auto`)を適用し、右端からはみ出さないように修正してください。", + "reason": "コードブロック全体およびテキストが親要素の右枠を越えて大きくはみ出しています。" + } + ] +} diff --git a/docs/reviews/2026-09-23-gemini-manuals/operations-before.json b/docs/reviews/2026-09-23-gemini-manuals/operations-before.json new file mode 100644 index 0000000..9a28869 --- /dev/null +++ b/docs/reviews/2026-09-23-gemini-manuals/operations-before.json @@ -0,0 +1,87 @@ +{ + "model": "gemini-3.6-flash", + "file": "chros/public/manual/operations.html", + "viewport": { + "width": 1440, + "height": 900, + "device_scale": 2.0, + "full_page": true, + "color_scheme": "light", + "wait_ms": 500, + "max_height_px": 20000 + }, + "findings": [ + { + "check": "text", + "severity": "error", + "category": "見出しと本文の内容が噛み合っていない箇所", + "location": "[205]", + "quote": "第2戦に大地れん15点・高原ゆう18点を入力すると、合計54対42で大地れんが勝者になります。", + "suggestion": "不要な文を削除し、「練習は別環境で行い、本番の大会データとは分けてください。」のみに修正してください。", + "reason": "「困ったときに見る早見表」の文脈と無関係な試合結果の例示文(図10の説明の重複)が誤って混入しています。" + }, + { + "check": "layout", + "severity": "error", + "category": "要素の欠落", + "location": "「04 1試合を、2敗とも記録する」の見出し直下の画像表示領域", + "quote": "04 1試合を、2敗とも記録する", + "suggestion": "グレーのプレースホルダー領域に本来入るべき図版またはスクリーンショットを正しく配置してください。", + "reason": "コンテンツが描画されず大きなグレーの空ブロックが表示されており、情報が完全に欠けているため。" + }, + { + "check": "layout", + "severity": "error", + "category": "要素の欠落", + "location": "「07 予選を締め、本戦に進む」の「4 本戦表を生成し、各試合を進める」直下の領域", + "quote": "4 本戦表を生成し、各試合を進める", + "suggestion": "該当する画面の画像またはコンテンツを正しく読み込んで表示させてください。", + "reason": "グレーのプレースホルダー領域のまま放置されており、手順に必要な画像が閲覧できないため。" + }, + { + "check": "layout", + "severity": "error", + "category": "要素の欠落", + "location": "「08 会場に見せる画面を選ぶ」の「3 項目を動かす(並べ替える)」手順直下の領域", + "quote": "3 項目を項目を動かす(並べ替える)", + "suggestion": "操作手順を示す画面のスクリーンショットを正しく挿入してください。", + "reason": "画像が読み込まれず灰色の領域になっており、視覚的な説明が欠落しているため。" + }, + { + "check": "text", + "severity": "warning", + "category": "敬体(ですます)と常体(である)の混在", + "location": "[122]", + "quote": "一覧から対象試合を選び、第1戦または第2戦を開く。", + "suggestion": "「一覧から対象試合を選び、第1戦または第2戦を開きます。」のように敬体に統一してください。", + "reason": "本文全体が「です・ます」調で書かれている中で、箇条書きの手順の文末が辞書形(常体)になっています。" + }, + { + "check": "text", + "severity": "warning", + "category": "敬体(ですます)と常体(である)の混在", + "location": "[128]", + "quote": "「両戦を仕切り直す」を押す。", + "suggestion": "「「両戦を仕切り直す」を押します。」のように敬体に統一してください。", + "reason": "本文全体が「です・ます」調で書かれている中で、箇条書きの手順の文末が辞書形(常体)になっています。" + }, + { + "check": "text", + "severity": "warning", + "category": "主語と述語の不一致", + "location": "[133]", + "quote": "制限に当たったときは、運営責任者と技術担当に相談します。", + "suggestion": "「制限に当たったときは、運営責任者と技術担当に相談してください。」に変更してください。", + "reason": "読み手(操作担当者)へ向けた指示・案内において「相談します」と書かれており、他の箇所の表現(第62行「相談してください」等)と文末形式が不統一で不自然です。" + }, + { + "check": "layout", + "severity": "warning", + "category": "折り返し", + "location": "「01 まず、今日の流れをつかむ」の見出し直下の段落", + "quote": "で行います。", + "suggestion": "単語「行います」が途中で不自然に改行されないよう、文節や単語の切り替わりに合わせて折り返し位置を調整してください。", + "reason": "「で行」で改行され「います。」が次行頭に溢れており、読み手に違和感を与えるため。" + } + ] +} diff --git a/docs/technical-requirements.md b/docs/technical-requirements.md index 34a1a4d..29ec477 100644 --- a/docs/technical-requirements.md +++ b/docs/technical-requirements.md @@ -10,13 +10,13 @@ | 配置 | 責務 | | --- | --- | | `packages/shared/src/control.ts` | 状態・コマンド・表示ラベル・Zod検証 | -| `packages/scoring/src/match.ts` | 各戦・2戦合計の判定、予選順位。I/Oを持たない | +| `packages/scoring/src/match.ts`、`standings.ts` | 各戦・2戦合計の判定と予選順位。I/Oを持たない | | `chros/lib/tournament.ts` | 総当たり・本戦の生成、入力保護、再試合、監査記録を含む状態遷移 | | `chros/lib/store.ts` | JSON永続化、更新番号検証、保存後のSSE通知 | | `chros/lib/use-control.ts` | ブラウザーでのSSE購読、コマンド送信、通信状態とエラー表示 | | `chros/components/ConsoleApp.tsx` | 進行管理と画面選択 | | `chros/components/MatchEditor.tsx` | 先攻登録、各戦の結果入力・訂正・再試合 | -| `chros/components/Management.tsx` | 参加者、順位、本戦作成、大会設定、エクスポート、履歴 | +| `chros/components/management/` | 参加者、順位、本戦作成、大会設定、エクスポート、履歴 | | `chros/components/Rundown.tsx` | 表示順の編集と手動進行 | | `chros/components/Board.tsx` | 会場掲示とプレビューの共通表示 | diff --git a/package.json b/package.json index 9fbf154..52ec31e 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "start": "pnpm --filter chros start", "test": "pnpm -r test", "typecheck": "tsc --noEmit --noUnusedLocals --noUnusedParameters -p packages/shared && tsc --noEmit --noUnusedLocals --noUnusedParameters -p packages/scoring && tsc --noEmit --noUnusedLocals --noUnusedParameters -p chros", - "format": "prettier --write \"chros/{app,components,lib}/**/*.{ts,tsx,css}\" \"packages/*/src/**/*.ts\" \"scripts/**/*.mjs\"", - "format:check": "prettier --check \"chros/{app,components,lib}/**/*.{ts,tsx,css}\" \"packages/*/src/**/*.ts\" \"scripts/**/*.mjs\"" + "format": "prettier --write \"chros/{app,components,lib}/**/*.{ts,tsx,css}\" \"packages/*/src/**/*.ts\" \"scripts/**/*.mjs\" \"chros/public/manual/**/*.{html,css,js}\"", + "format:check": "prettier --check \"chros/{app,components,lib}/**/*.{ts,tsx,css}\" \"packages/*/src/**/*.ts\" \"scripts/**/*.mjs\" \"chros/public/manual/**/*.{html,css,js}\"" }, "devDependencies": { "prettier": "3.9.8", diff --git a/scripts/embed-manual-image.mjs b/scripts/embed-manual-image.mjs new file mode 100644 index 0000000..a978d74 --- /dev/null +++ b/scripts/embed-manual-image.mjs @@ -0,0 +1,38 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +const [htmlPath, screenshotId, imagePath] = process.argv.slice(2); +if (!htmlPath || !screenshotId || !imagePath || !/^[a-z0-9-]+$/.test(screenshotId)) { + throw new Error( + '使い方: node scripts/embed-manual-image.mjs <画像の識別名> <画像ファイル>', + ); +} + +function imageMimeType(bytes) { + if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) { + return 'image/png'; + } + if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) { + return 'image/jpeg'; + } + if (bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP') { + return 'image/webp'; + } + throw new Error('PNG・JPEG・WebPの画像を指定してください。HTMLは変更していません。'); +} + +const [html, bytes] = await Promise.all([readFile(htmlPath, 'utf8'), readFile(imagePath)]); +const mimeType = imageMimeType(bytes); +const imageTags = html.match(/]*>/g) ?? []; +const identifier = new RegExp(`\\bdata-screenshot=["']${screenshotId}["']`); +const matches = imageTags.filter((tag) => identifier.test(tag)); +if (matches.length !== 1 || !/\bsrc="[^"]*"/.test(matches[0])) { + throw new Error('対応する画像が一つに特定できません。data-screenshotとsrcを確認してください。'); +} + +const dataUrl = `data:${mimeType};base64,${bytes.toString('base64')}`; +const updatedTag = matches[0].replace(/\bsrc="[^"]*"/, () => `src="${dataUrl}"`); +await writeFile( + htmlPath, + html.replace(matches[0], () => updatedTag), +); +console.log(`${screenshotId} を ${mimeType} (${bytes.length} bytes) で埋め込みました。`);