作業ガイド
SPIKES の運営メンバーが、やりたいことから画面と手順を引くための頁である。作業ごとに、開く画面、操作の順番、誰がやれるか、同じことを API や AI(MCP)でするにはどうするかを書いている。
画面のパスの中の「<イベント>」「<募集>」のような印は、開いているイベントや募集ごとに変わる部分である。<イベント> はイベントの URL の名前に置き換える。印の無いパスは押すとその画面が開く(ログインと権限が要る)。
やりたいこと
- タイムテーブル(企画)を作って公開するコンテンツを日とステージの格子に置き、登壇者を確定し、検査を通してから公開する。
- 募集(BOSHU)を作って公開し、応募を審査する募集を作って質問を組み、文面を確かめて公開し、応募を審査して結果を通知する。
- 参加登録・チケットの設問を直す(モジュールアンケート)イベントの「フォーム」で、登録フォームとチケットの設問をモジュールの並びで組む。
- 団体の申請を審査する学生団体・学生発ベンチャーの登録申請を読み、承認するか見送る。
- 会場図とブースを用意する会場図にブースの枠を置き、出展者を割り当て、チェックインの QR を用意する。
- AI(MCP)に下書きを作らせるClaude や ChatGPT を BOUNTY につなぎ、文で頼んで下書きを作らせ、画面で確かめて公開する。
- 外部のサイトに応募フォームを埋め込む公開した募集の「共有」から埋め込みコードを写し、外部のページに貼る。
- 外部サイトに会場マップとタイムテーブルを出す公開したタイムテーブルと会場図を、ログインの要らない公開の API から外部のページで読み、自分のデザインで描く。
権限の呼び方
各作業の「誰がやれるか」は、次の呼び方で書いている。
- 運営スタッフ
- SPIKES の運営の役割を持つ人。主のイベント(SPIKES が主催する本体のイベント)の管理の画面を開ける。
- 運営の管理者
- 運営の役割のうち管理者の人。どのイベントでも、そのイベントの管理者として扱われる。API トークンの発行は運営の管理者だけができる。
- イベントの管理者
- 4S でそのイベントの管理者・編集者になっている人。主のイベント以外のイベントは、この人たちが管理の画面を使う。確かめてから時間が経っていると、管理の画面を開いたときに 4S で確かめ直す。
- 団体の管理者
- ギルド・学生発ベンチャーに在籍中のオーナーと管理者。団体の募集と名簿を扱う。運営であっても団体の中のことは代わりに扱わない。
タイムテーブル(企画)を作って公開する
コンテンツを日とステージの格子に置き、登壇者を確定し、検査を通してから公開する。
誰がやれるか
主のイベントでは運営スタッフが開ける。ステージそのものの管理と担当者の選択は運営の管理者だけが行う。ほかのイベントでは、そのイベントの管理者が同じ段の画面を使う。公開と公開の取り下げは、運営かイベントの管理者がブラウザの画面で行う。
手順
ラインを用意する。ラインはステージを受け持つ担当の組で、名前・点の色・受け持つステージ・既定のスタッフを持つ。日程の札の色はラインの色になる。
/events/<イベント>/manage/lines
コンテンツを作り、日・ステージ・時刻の格子に置く。下書きの間は同じステージで時間が重なってもよく、画面に重なりとして出る。
/events/<イベント>/manage/content
登壇者を足す。運営がプロフィールの提案を書き、本人が限定リンクから直し、人が画面で確定して初めて掲載の内容になる。確定する前の紹介文や写真は公開に出ない。
/events/<イベント>/manage/speakers
検査を回す。公開を止める指摘(重なり・時刻やステージの欠け・長さの誤り)は直してから進む。止めない指摘(会期の外・格子からのずれ・ラインや登壇者の欠け)は内容を見て判断する。
/events/<イベント>/manage/content
公開する。公開したコンテンツだけが、参加者の日程・会場図・チェックインの QR と、公開の API に出る。下書きに戻すと同じ URL は外から読めなくなる。
気を付けること
- 同じブロックを複数の人が同時に直したときは、後から保存した側が今の状態を受け取り、上書きはしない。保存できなかったら画面を読み直してから直す。
- 企画の一覧の「公募を作る」から、その企画に合う募集の下書きを作れる(次の作業)。
API と MCP で同じことをするには
企画の API で同じ下書きを組める。ブラウザのログイン、アプリのトークン、コンテンツの範囲を持つ API トークンのどれでも呼べる。MCP からは時間割・ライン・登壇者・サムネイルの下書きまでで、公開・登壇者の確定・限定リンクの発行は人が画面で行う。公開中のコンテンツへのトークンからの書き込みは断られる。
続けて読む
募集(BOSHU)を作って公開し、応募を審査する
募集を作って質問を組み、文面を確かめて公開し、応募を審査して結果を通知する。
誰がやれるか
募集の持ち主で決まる。運営と主のイベントの募集は運営スタッフ、ほかのイベントの募集はそのイベントの管理者、ギルド・ベンチャーの募集は団体の管理者が扱う。ギルド・ベンチャーの募集の公開の承認は、システム管理(/admin)に入れる運営スタッフが行う。
手順
募集を作る。イベントの募集は、そのイベントの管理の募集の一覧から作る。主のイベントの一覧の「新規作成」は運営の募集を作る。団体の募集は団体の管理の画面から作る。
/events/<イベント>/manage/boshu/guilds/<団体>/manage/boshu/new/ventures/<団体>/manage/boshu/new
題・説明・応募の条件と、追加の質問を組む。質問はモジュールアンケートで、棚のモジュールを置いて作る(次の次の作業)。
/events/<イベント>/manage/boshu/<募集>/edit
ギルド・ベンチャーの募集は承認待ちで作られ、運営が承認するまで参加者に見えない。承認の後に、運営の管理者でない持ち主が中身を直すと承認待ちに戻る。
応募と結果のメールの文面を確認してから公開する。公開・受付中・承認済みがそろうと、持ち主に応じた宛先へ一度だけ告知が出る。
/events/<イベント>/manage/boshu/<募集>
応募を審査する。応募ごとに保留・採択・補欠・見送りを付け、まとめて決めることも、採択した人の出演順を並べることもできる。応募者の詳細で回答を読む。
/events/<イベント>/manage/boshu/<募集>/applicants/<応募>
「結果を通知」で、まだ知らせていない応募にまとめて結果を届ける。本人に見えるのは知らせた後の決定だけで、それまでは審査中に見える。
気を付けること
- 先着の募集は応募の時点で受付が確定し、審査の段は無い。
- 登壇・ピッチの採択を通知すると、対象のコンテンツの登壇者としてタイムテーブルへ写る。
- 個人情報の印を付けた質問の回答は、運営の管理者とその募集の管理者だけが読める。審査の画面・CSV・外部の API で同じ判定になる。
API と MCP で同じことをするには
募集の管理の API で、作成・公開・審査・通知を同じように行える。外部の API は運営の管理者が発行するトークンで募集を作り、応募を読める。MCP からは募集・設問・選考・自動返信の下書きまでで、公開と承認は人が画面で行う。
続けて読む
参加登録・チケットの設問を直す(モジュールアンケート)
イベントの「フォーム」で、登録フォームとチケットの設問をモジュールの並びで組む。
誰がやれるか
そのイベントの管理者(4S でそのイベントの管理者・編集者の人と、運営の管理者)。募集の追加の質問は、その募集を扱える人が募集の編集の画面で直す。
手順
イベントの管理の「フォーム」を開き、「登録フォーム」と「チケットの設問」を切り替える。チケットの画面の系統の「設問」からも同じ画面へ移る。切り替えても直しかけの内容は消えない。
/events/<イベント>/manage/forms/events/<イベント>/manage/tickets
棚のモジュールを「追加」でページの最後へ置き、つまみを掴んで並べ替える。キーボードでも並べ替えられる。その設問だけの質問は固有のモジュールとして足す。
プレビューで、答える人にどう見えるかを確かめる。登録済みの値は引用され、欠けている欄だけを聞く形になる。
チケットの設問は、下書きを直してから公開する。募集の設問は保存がそのまま新しい版になり、古い応募は答えたときの文言のまま読める。
気を付けること
- プロフィールのページには共通モジュールしか置けない。本人に残す回答の鍵をイベントをまたいでそろえるためである。
- ログインが要るモジュールを置いた募集は、応募のログインを「不要」にできない。組み立ての画面がその理由を出す。
- 団体と登録者の段は直せないシステムのページで、鍵付きの「固定」のカードとして出る。
API と MCP で同じことをするには
MCP の道具で、棚を読む・設問をモジュールの形で読む・操作を試す・操作を当てる、ができる。試す道具は保存と同じ処理を走らせて必ず巻き戻し、版が増えるかや承認のやり直しが起きるかを返す。共通モジュールそのものの編集と公開は人が画面で行う。
続けて読む
団体の申請を審査する
学生団体・学生発ベンチャーの登録申請を読み、承認するか見送る。
誰がやれるか
団体の登録の審査は、システム管理(/admin)に入れる運営スタッフが行う。イベントのパートナー募集に申し込んだ団体の参加は、そのイベントの運営が団体の審査の画面で判定する。
手順
団体の登録審査を開く。審査待ちの申請が並び、審査を終えた申請は審査履歴に並ぶ。
団体名を押して詳細を開き、フォームの回答・招待・法人番号の照合を確かめる。
/admin/orgs/<申請>
承認するか見送る。承認すると団体が公開され、申請者が在籍中のオーナーになる。見送りでは公開しない。結果は本人へ通知される。学生発ベンチャーは、学生発の定義を満たさなければ承認できない。
パートナー募集の申込は、イベントの管理の団体の審査で判定する。ほかのイベントでは、SPIKES の運営が団体の登録を判定するまで、ここでは判定できない。
/events/<イベント>/manage/partners/review
気を付けること
- 承認前の団体は、団体のページ・一覧・ランキングのどれにも出ない。
- 同じ名前の団体があれば新しい登録は断られ、その団体の管理の申し出へ案内される。
API と MCP で同じことをするには
審査は運営の画面で行う。公開の API で読めるのは、承認済みの団体と在籍の情報で、名前・メール・連絡先・申請の回答は載らない。
続けて読む
会場図とブースを用意する
会場図にブースの枠を置き、出展者を割り当て、チェックインの QR を用意する。
誰がやれるか
主のイベントの運営スタッフが開ける。ブース配置の操作は運営の管理者だけで、運営スタッフは見るだけである。会場図・ブース・チェックインの記録を持つのは、今は主のイベントだけである。
手順
会場図をつくる画面で、図面にステージ・ブースの枠と番号を置く。
/events/<イベント>/manage/venue/design
出展の画面で、出展者(ブース)を登録する。ブースはロゴ・カバー・担当者の顔と名前を持てる。
/events/<イベント>/manage/exhibitions
ブース配置の画面で、出展者をブースの枠へドラッグで割り当て、確かめてから公開する。割当は全日か日付ごとで、同じ日に同じ枠へ複数のブースは置けず、同じブースを複数の枠にも置けない。
/events/<イベント>/manage/venue/placement
会場の画面で、セッションとブースのチェックインの状態を確かめる。掲示する QR は QR 印刷の画面から印刷する。
/events/<イベント>/manage/venue/events/<イベント>/manage/venue/print
気を付けること
- 出展者が決まっていない枠は、参加者の会場図にピンを立てない。
- 資料からセッション・ブース・会場図をひとまとまりで取り込む入口もある。取り込みは審査待ちに送るところまでで、公開は人が決める。
API と MCP で同じことをするには
出展ブースの情報と、会場図のブースの枠と番号は MCP で下書きできる。その図を現行にするのは人である。公開されたプログラムと現行の会場図は、ログインの要らない公開の API から読める。
続けて読む
AI(MCP)に下書きを作らせる
Claude や ChatGPT を BOUNTY につなぎ、文で頼んで下書きを作らせ、画面で確かめて公開する。
誰がやれるか
BOUNTY のアカウントでつなぐ。AI ができることは、ログインした本人の権限と所属の範囲までである。外部のシステムからトークンで呼ぶための API トークンは、運営の管理者だけが発行できる。
手順
MCP の接続案内を開き、使っている AI(Claude・ChatGPT・Claude Code・Codex など)の手順を見る。
AI のコネクタに接続先の URL を貼り、BOUNTY でログインして許可する。トークンを貼る手順は無い。
やりたいことを文で頼む。頼める範囲は下の「AI に頼めること」の一覧のとおりで、AI は適切な道具を選んで非公開の下書きを作る。
BOUNTY の画面で下書きを確かめ、人が公開・承認する。MCP で入れた企画の下書きには、公開の前に内容を確かめる印が付く。
つなぐのをやめるときは、マイページの「連携中のアプリ」から解除する。
外部のシステムからトークンで呼ぶときは、運営の管理者が API トークンを発行し、使える入口を発行時に選ぶ。鍵は発行時だけ表示される。
API と MCP で同じことをするには
MCP の道具は、人が画面で使うのと同じ API を通る。ただし届くのは下書きと提案までで、公開・承認・確定は人が画面で行う。どの道具が何をするかは、MCP の接続案内と、各機能の頁の API と MCP の説明に書いてある。
AI に頼めること
- 募集募集・設問・選考・自動返信の下書き
- 会社ページ紹介文・ロゴ・メンバー
- プログラムセッションと登壇の下書き
- 企画時間割・ライン・登壇者
- 出展ブースブースの情報とロゴ
- 会場マップ図面にブースを配置
続けて読む
外部のサイトに応募フォームを埋め込む
公開した募集の「共有」から埋め込みコードを写し、外部のページに貼る。
誰がやれるか
その募集を扱える人が埋め込みコードを写せる。埋め込める先のサイトは運営が設定し、設定の無いサイトの中ではフォームは開かない。
手順
募集を公開する。コードを写すだけでは募集は公開されない。
/events/<イベント>/manage/boshu/<募集>
募集の管理の画面の「共有」を開く。応募リンクと、埋め込み先が設定されていれば埋め込みコードが出る。未設定なら運営へ設定を頼む。
「埋め込みコードをコピー」で iframe のコードを写し、外部のページに貼る。
埋め込みでも、応募は同じ仕組みと同意を通る。応募する人は別の窓で 4S のログインか登録をして、同じ枠の中で入力・応募・編集・取り下げをする。
埋め込めないページでは、応募リンク(独立した応募の画面)をそのまま案内する。
/forms/boshu/<募集>
API と MCP で同じことをするには
外部のページの JavaScript からは、ログインなしで募集のフォームの定義(公開中の質問の版と入力の形)を読める。応募・回答・本人の情報は返さない。応募を外部のシステムで扱うときは、運営の管理者が発行したトークンで募集の外部 API を呼ぶ。
続けて読む
外部サイトに会場マップとタイムテーブルを出す
公開したタイムテーブルと会場図を、ログインの要らない公開の API から外部のページで読み、自分のデザインで描く。
誰がやれるか
読むだけなら誰でもよく、ログインもトークンも要らない。どのサイトからでもブラウザの JavaScript で読める。出る中身を決めるのは、タイムテーブルと会場図を公開する運営かイベントの管理者である。
手順
タイムテーブルを公開する。外部のページに出るのは公開したコンテンツだけで、下書き・非公開・審査で隠したものは出ない(「タイムテーブル(企画)を作って公開する」)。
/events/<イベント>/manage/content
会場図とブース配置を公開する。出るのは現行のベース図とブースの枠と、確定した割当だけで、仮の割当や下書きの出展者は出ない(「会場図とブースを用意する」)。
/events/<イベント>/manage/venue/placement
イベントの URL の名前を確かめる。公開の API はイベントの URL の名前で引く。イベントが無いか非公開なら、決まった誤りの印だけが返る。
/events/<イベント>
外部のページから企画の全体を一度で読む。日・ステージ・ライン・登壇者・コンテンツ・会場とフロア・会場図が一つの応答に入っている。下の見本をそのまま貼ると、日ごと・ステージごとのタイムテーブルと、ブースの枠を重ねた会場図が出る。
タイムテーブルを組む。時刻のあるコンテンツを日で分け、ステージの並びごとに開始の時刻の順で並べる。時刻は世界時で届くので、イベントのタイムゾーンで表示する。
会場図を描く。ベース図を画像として置き、同じ座標(viewBox の単位)でブースの枠を重ね、選んだ日の割当から出展者を引く。色や枠の見た目はサイトの側で決める。
空のときの表示を決める。公開したものがまだ無ければ日・コンテンツ・会場図は空で返るので、「まだありません」を出す。イベントが無い・非公開になったときは枠ごと隠す。
読み直しの間隔を決める。応答は同じ中身なら同じ印(ETag)を持ち、ブラウザは変わっていなければ本文を読み直さない。当日の差し替えや取り下げを拾うため、ページを開いたときと数分おきに読み直す。
気を付けること
- 実際の外部サイトでの表示の確認は、出す先のサイトが決まってから、そのサイトの上で行う。
- 応答の版(schemaVersion)を確かめ、知らない版なら描かずに知らせる。同じ版のまま欄が足されることがあるので、知らない欄は読み飛ばす。
- 取り下げたコンテンツ・ロゴ・写真・会場図の画像は、同じ URL で読めなくなる。画像を外部のサイトに写して置かず、応答の URL をそのまま使う。サムネイルは絵の版が URL に入るので、絵が変われば URL も変わる。
- ブースの番号は会場・フロアごとに一意で、会場が違えば同じ番号がありうる。会場を分けたイベントでは、番号に会場・フロアの名前を添える。
- API の文字(題・説明・名前)は HTML として差し込まず、文字として入れる。
応答の欄
event- イベントの名前・URL の名前・タイムゾーン・会期・会場の名前。時刻の表示はこのタイムゾーンを使う。
days- 日の一覧(イベントのタイムゾーンの暦日)。会期と、公開中のコンテンツのある日。タブや見出しの並びにそのまま使う。
stages- ステージ(名前・短い記号・並び・会場とフロア)。タイムテーブルの列はこの並びにする。
lines- ステージを受け持つライン。色は名前で届くので、サイトの色に対応させる。
speakers- 公開中のコンテンツに出る登壇者。名前・肩書・会社は掲載の内容、紹介文・写真・英語の欄はプロフィールを確定した人だけで、ほかは空。
contents- 公開中のコンテンツ。schedule が timed なら day と開始・終了の時刻(世界時)とステージを持ち、allday は会期を通して、unscheduled は時刻が未定。登壇者・サムネイル・結んだ募集・出展者(ブースの行だけ)を持つ。
areas- 会場とフロアの木(会場 → そのフロアの順)。会場を分けていないイベントでは空。
maps- 会場図を会場・フロアごとに一枚。base はベース図の画像の URL と大きさと viewBox、暗い地で読める描き直し(描き直せない図は空)。slots はブースの枠(番号・形・viewBox の単位の頂点・区画・種別)。
maps[].assignments- 確定した割当。日ごと(day が空なら全日)に、枠の鍵とコンテンツの id を結ぶ。出展者の名前やロゴは contents から引く。
map- 古い読み手のための一枚の会場図(maps の最初と同じ図)。新しく作るページは maps を読む。
見本
依存の無い一枚の HTML。BOUNTY の公開の URL とイベントの URL の名前を書き換えて外部のページに貼ると、日の切り替え・ステージごとのタイムテーブル・ブースの枠を重ねた会場図が出る。見た目はサイトのデザインに合わせて直す。
GET /api/v1/public/events/{slug}/planGET /api/v1/public/images/{key}
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>タイムテーブルと会場マップ</title>
<style>
#bounty-plan { color-scheme: light; color: #111; background: #fff; font-family: system-ui, sans-serif; max-width: 960px; margin: 0 auto; padding: 16px; }
.days button { margin: 0 4px 8px 0; }
.days button[aria-pressed="true"] { font-weight: bold; }
.stage ul { list-style: none; margin: 0 0 16px; padding: 0; }
.stage li { padding: 6px 0; border-top: 1px solid #ddd; }
.stage time { font-variant-numeric: tabular-nums; margin-right: 8px; }
.map svg { display: block; width: 100%; height: auto; max-height: 80vh; background: #fff; }
.slot-list { list-style: none; margin: 8px 0 16px; padding: 0; }
.slot-list li { padding: 6px 0; border-top: 1px solid #ddd; }
.slot { fill: transparent; stroke: #999; }
.slot.assigned { fill: rgba(250, 200, 0, 0.45); stroke: #333; }
.slot-number { font-size: 12px; text-anchor: middle; dominant-baseline: central; pointer-events: none; }
</style>
</head>
<body>
<div id="bounty-plan">読み込み中…</div>
<script>
(function () {
// 公開の読み取り API(ログイン不要・どのオリジンからでも読める)
var ORIGIN = "https://bounty.example"; // BOUNTY の公開の URL(オリジン)に置き換える
var SLUG = "spikes-2026-tokyo"; // イベントの URL の名前に置き換える
var SVG = "http://www.w3.org/2000/svg";
var root = document.getElementById("bounty-plan");
var selectedDay = null;
// 文字は必ず textContent で入れる(API の文字を HTML として解釈させない)
function h(tag, attrs, children) {
var node = document.createElement(tag);
Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, attrs[k]); });
(children || []).forEach(function (c) { node.append(c); });
return node;
}
function s(tag, attrs) {
var node = document.createElementNS(SVG, tag);
Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, attrs[k]); });
return node;
}
async function load() {
// ブラウザの HTTP キャッシュが ETag で確かめ直す(変わっていなければ 304 で本文を読まない)
var res = await fetch(ORIGIN + "/api/v1/public/events/" + encodeURIComponent(SLUG) + "/plan");
if (res.status === 404) { root.hidden = true; return; } // 無い・非公開のイベント: 枠ごと隠す
if (!res.ok) { if (!root.dataset.ready) root.textContent = "読み込めませんでした。時間をおいて開き直してください。"; return; }
var plan = await res.json();
if (plan.schemaVersion !== 2) { root.textContent = "対応していない版です。"; return; }
root.hidden = false;
root.dataset.ready = "1";
render(plan);
}
function render(plan) {
var tz = plan.event.timezone;
var clock = new Intl.DateTimeFormat("ja-JP", { timeZone: tz, hour: "2-digit", minute: "2-digit" });
// day は「イベントのタイムゾーンの暦日」の YYYY-MM-DD。暦日として出すので UTC で読む
var dayName = new Intl.DateTimeFormat("ja-JP", { timeZone: "UTC", month: "numeric", day: "numeric", weekday: "short" });
var speakerName = new Map(plan.speakers.map(function (p) { return [p.id, p.name]; }));
var contentById = new Map(plan.contents.map(function (c) { return [c.id, c]; }));
var areaById = new Map(plan.areas.map(function (a) { return [a.id, a]; }));
// フロアは親の会場の名前を添える(「本館 1F」)
function areaLabel(id) {
var area = areaById.get(id);
if (!area) return "会場マップ";
var parent = area.parentId ? areaById.get(area.parentId) : null;
return parent ? parent.name + " " + area.name : area.name;
}
if (!plan.days.includes(selectedDay)) selectedDay = plan.days[0] || null;
root.replaceChildren(h("h2", {}, [plan.event.title]));
if (!selectedDay) { root.append(h("p", {}, ["公開中のタイムテーブルはまだありません。"])); return; }
// 日の切り替え
var tabs = h("div", { class: "days", role: "group", "aria-label": "日" });
plan.days.forEach(function (day) {
var b = h("button", { type: "button", "aria-pressed": String(day === selectedDay) }, [dayName.format(new Date(day + "T00:00:00Z"))]);
b.addEventListener("click", function () { selectedDay = day; render(plan); });
tabs.append(b);
});
root.append(tabs);
// タイムテーブル: その日の時刻のあるコンテンツを、ステージの並び(stages の順)ごとに時刻順で
var timed = plan.contents.filter(function (c) { return c.schedule === "timed" && c.day === selectedDay; })
.sort(function (a, b) { return a.startsAt.localeCompare(b.startsAt); });
var columns = plan.stages.map(function (st) { return { id: st.id, name: st.name }; }).concat([{ id: null, name: "ステージ未定" }]);
var shown = 0;
columns.forEach(function (col) {
var rows = timed.filter(function (c) { return c.stageId === col.id; });
if (!rows.length) return;
shown += rows.length;
var list = h("ul");
rows.forEach(function (c) {
var names = c.speakers.map(function (sp) { return speakerName.get(sp.speakerId); }).filter(Boolean).join("・");
list.append(h("li", {}, [
h("time", { datetime: c.startsAt }, [clock.format(new Date(c.startsAt)) + "–" + clock.format(new Date(c.endsAt))]),
h("strong", {}, [c.title]),
names ? h("div", {}, [names]) : ""
]));
});
root.append(h("section", { class: "stage" }, [h("h3", {}, [col.name]), list]));
});
if (!shown) root.append(h("p", {}, ["この日の時刻の決まったプログラムはまだありません。"]));
// 会場図: 葉(会場・フロア)ごとに 1 枚。ベース図の上に、viewBox の単位で枠を重ねる
if (!plan.maps.length) { root.append(h("p", {}, ["公開中の会場マップはまだありません。"])); return; }
plan.maps.forEach(function (m) {
var vb = m.base.viewBox;
var svg = s("svg", { viewBox: vb.join(" "), role: "img", "aria-label": "会場マップ" });
svg.append(s("image", { href: m.base.image.url, x: vb[0], y: vb[1], width: vb[2], height: vb[3] }));
// その日の割当(day = null は全日)。contentId は contents の id
var assigned = new Map(m.assignments.filter(function (a) { return a.day === null || a.day === selectedDay; })
.map(function (a) { return [a.slotKey, contentById.get(a.contentId)]; }));
m.slots.forEach(function (slot) {
var content = assigned.get(slot.slotKey);
var shape = s("polygon", { class: content ? "slot assigned" : "slot", points: slot.points.map(function (p) { return p.join(","); }).join(" ") });
var title = s("title");
title.textContent = slot.number + (content && content.exhibitor ? " " + content.exhibitor.name : "");
shape.append(title);
svg.append(shape);
var cx = slot.points.reduce(function (sum, p) { return sum + p[0]; }, 0) / slot.points.length;
var cy = slot.points.reduce(function (sum, p) { return sum + p[1]; }, 0) / slot.points.length;
var label = s("text", { class: "slot-number", x: cx, y: cy });
label.textContent = slot.number;
svg.append(label);
});
// 図の下に番号と出展者を並べる(title はスマホで読めない)
var list = h("ul", { class: "slot-list" });
m.slots.forEach(function (slot) {
var content = assigned.get(slot.slotKey);
if (content) list.append(h("li", {}, [h("strong", {}, [slot.number]), " ", content.exhibitor ? content.exhibitor.name : content.title]));
});
// 番号は会場・フロアごとに一意なので、分けているイベントでは名前を添える
var heading = m.areaId ? areaLabel(m.areaId) : "会場マップ";
root.append(h("section", { class: "map" }, [h("h3", {}, [heading]), svg, list]));
});
}
load();
// 当日の差し替え・取り下げを拾うため、数分おきに読み直す(変わっていなければ 304)
setInterval(load, 5 * 60 * 1000);
})();
</script>
</body>
</html>
API と MCP で同じことをするには
企画の全体と画像は、公開の読み取りの API から読む。ログインもトークンも要らず、どのサイトからでも呼べる。応答の形と見本は API の頁にある。書き込みの入口は無いので、中身を変えるのは管理の画面で公開・取り下げをする人である。