From 538fd636e25595d88a958344d285c0e7cf44e530 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Wed, 6 May 2026 22:24:38 +0900 Subject: Async scraping, scrape_logs, and CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Background scraping: - POST /api/scrape returns 202 immediately with run_id; scraping runs async - GET /api/scrape-status?run_id=xxx polls for results per venue - scrape_logs table: per-venue status (running/ok/error), events_saved, error, timestamps CLI (npm run scrape): - npm run scrape — 全会場をスクレイプ、結果を色付きで出力 - npm run scrape liquid-room — 特定会場のみ - npm run scrape -- --list — 登録済み会場一覧を表示 - エラー時は exit code 1 + エラーメッセージを dim 表示 Venues page: - 最終スクレイプ日時・成否をインラインで表示 - 会場ごとの「更新」ボタンを追加 Bug fix: upsertEvent に description/optional fields のデフォルト値を設定し better-sqlite3 の "Missing named parameter" エラーを解消 Co-Authored-By: Claude Sonnet 4.6 --- app/routes/venues.tsx | 115 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 84 insertions(+), 31 deletions(-) (limited to 'app/routes/venues.tsx') diff --git a/app/routes/venues.tsx b/app/routes/venues.tsx index 23b052f..affa72a 100644 --- a/app/routes/venues.tsx +++ b/app/routes/venues.tsx @@ -1,17 +1,19 @@ -import { useLoaderData, Link } from "react-router"; +import { useLoaderData, Link, Form } from "react-router"; import type { Route } from "./+types/venues"; -import { getVenues } from "~/lib/db.server"; +import { getVenues, getLastScrapePerVenue, type ScrapeLog } from "~/lib/db.server"; import { getScraperIds } from "~/lib/venue-meta.server"; export async function loader(_: Route.LoaderArgs) { const venues = getVenues(); const scraperIds = getScraperIds(); - return { venues, scraperIds }; + const scrapeStatus = getLastScrapePerVenue(); + return { venues, scraperIds, scrapeStatus }; } export default function Venues() { - const { venues, scraperIds: scraperIdList } = useLoaderData(); + const { venues, scraperIds: scraperIdList, scrapeStatus } = useLoaderData(); const scraperIds = new Set(scraperIdList); + const statusMap = new Map(scrapeStatus.map((s) => [s.venue_id, s])); return (
@@ -26,43 +28,94 @@ export default function Venues() {
-
-

会場一覧

-

- 現在 {scraperIdList.length} 会場のスクレイパーが登録されています。 - 新しい会場を追加するには app/scrapers/ に - モジュールを追加して index.ts に登録してください。 -

+
+
+

会場一覧

+

+ 現在 {scraperIdList.length} 会場のスクレイパーが登録されています。 +

+
+
+ +
{venues.length === 0 ? ( -

まだ会場データがありません。「情報を更新」してください。

+

まだ会場データがありません。「全会場を更新」してください。

) : ( -
- {venues.map((v) => ( - -
-

{v.name}

- {v.area &&

{v.area}

} +
+ {venues.map((v) => { + const log = statusMap.get(v.id); + return ( +
+ {/* 会場名 + エリア */} +
+ + {v.name} + + {v.area &&

{v.area}

} +
+ + {/* イベント件数 */} + + {v.event_count ?? 0} 件 + + + {/* 最終スクレイプ状態 */} + {log ? ( + + ) : ( + 未実行 + )} + + {/* 個別更新ボタン */} {scraperIds.has(v.id) && ( - - スクレイパー登録済 - +
+ + +
)}
- - {v.event_count ?? 0} - - - - ))} + ); + })}
)}
); } + +function ScrapeStatus({ log }: { log: ScrapeLog }) { + if (log.status === "running") { + return ⟳ 実行中...; + } + if (log.status === "error") { + return ( + + ✖ エラー + + ); + } + const time = log.finished_at?.slice(0, 16).replace("T", " ") ?? ""; + return ( + + ✔ {time} + + ); +} -- cgit v1.2.3