The UI is a single main.js file executed in the browser. There are no build steps or external dependencies; everything runs in vanilla ES2018+.
api object wraps fetch and exposes semantic methods (getUsers, createSource, searchWordPressPosts, etc.). All requests are relative to /api/*.php, return parsed JSON, and throw on non-2xx statuses. The helper injects Basic Auth headers and redirects to #/login on 401/403. Every new backend endpoint should be mirrored here for consistent error handling.appState) – Holds cached users, the currently selected author id, an optional pageRefresher callback for auto-refresh, a isRefreshing flag, and an authorChangeHandler hook for pages that need to intercept author changes.editorial:lastAuthorId, and Basic Auth credentials are stored under editorial:authUsername / editorial:authPassword.createElement() abstracts DOM creation, renderKeywordChips()/renderChipList() build list UIs, and resolveWordPressTitle() decodes pre-escaped titles coming from wp_posts.title (see backend docs). Title decoding uses a shared hidden <textarea> to avoid DOM injection.startAutoRefresh() sets a 10-second interval that calls refreshAuthors() plus any page-specific pageRefresher. Focus changes pause/resume the interval. Auto refresh is disabled when the user is not authenticated.location.hash), so a single HTML page can host multiple “screens”.renderApp() re-renders the top bar, breadcrumbs, and the content area whenever the hash changes.renderRoute():
#/login – login screen for Basic Auth credentials.#/new-source – default landing page for source intake.#/sources – list of working sources for the selected author.#/sources/{id} – source detail/editor.#/users and #/users/{id} – manage users plus view per-user stats.#/fact-check/manual – manual fact-check submission (title/description/body).#/fact-check/url – WordPress URL-based fact-check submission.#/fact-check/{filter}/{page?} – fact-check task group list (all/pending/completed).#/fact-check/result/{id} – task group detail with fact/editorial results.#/keywords and #/keywords/{keyword} – keyword directory plus working/done breakdown.#/search/{query} – keyword suggestion + source matches for arbitrary text.#/wp-search/{query} – search mirrored WordPress posts by keywords derived from the query.#/wp-keywords/{list} – show posts whose keywords include the provided comma-delimited list.#/wp-authors – list mirrored WordPress authors stored locally.#/wp-authors/{id} – view a mirrored WordPress author profile plus their latest posts.#/wp-posts/{id} – WordPress post detail (cards use resolveWordPressTitle() and show categories/tags/keywords/description).#/logs/{page?} – structured logs list.#/search/*) and WordPress search (#/wp-search/*).appState.users. Updating the dropdown persists the choice and triggers a re-render; some pages register appState.authorChangeHandler to react instantly.| Page | Highlights |
| — | — |
| Login (renderLoginPage) | Captures Basic Auth credentials, validates by calling /api/users.php, stores credentials in localStorage, and redirects to #/users. |
| New Source (renderNewSourcePage) | URL input + “URL取得” fetch button. Uses Firecrawl via /api/crawl.php, detects keywords via /api/detect-keywords.php, shows Markdown + editable fields, and opens a duplicate-check dialog before submission. The dialog enforces: duplicate URL override allowed only when the URL contains a query; keyword duplicates require explicit confirmation; and WORKING_SOURCES_LIMIT (30) is enforced by querying /api/user-counts.php. Successful commits hit /api/sources.php with author id, sanitized URL, Markdown, and keywords. |
| Sources list (renderSourcesPage) | Fetches /api/sources.php?author_id=X&state=working and renders cards with title, URL, author, timestamps, comment, and keyword chips. Buttons direct to detail pages. |
| Source detail (renderSourceDetailPage) | Loads a single source, displays metadata, and offers inline editing for title/comment/content. Contains actions to update state to done/aborted via api.updateSourceState. |
| Users (renderUsersPage) | Shows all users, allows creating new ones, and encourages selecting an author before creating sources. |
| User detail (renderUserDetailPage) | Pulls /api/user-counts.php, shows counts per state, and lets admins rename users. |
| Keyword list/detail (renderKeywordListPage, renderKeywordDetailPage) | Lists every keyword/count. Detail view calls /api/search-sources.php twice (working/done) to show matching sources. |
| Search (renderSearchPage) | For an arbitrary string, fetches keyword suggestions via /api/search-keywords.php, displays them as chips, and queries /api/search-sources.php per keyword cluster. |
| WordPress search (renderWordPressSearchPage, renderWordPressKeywordPage) | Either use a free-text query to derive keywords or accept a comma-separated list. Both call /api/wp-post.php with keywords and render WordPress cards. |
| WordPress author list (renderWordPressAuthorListPage) | Calls /api/wp-authors.php (list mode) to show local WordPress authors with links into their detail pages. |
| WordPress post detail (renderWordPressPostPage) | Fetches /api/wp-post.php?id=…, renders a single card, and shows tags/categories/keywords plus external link. |
| WordPress author (renderWordPressAuthorPage) | Calls /api/wp-authors.php + /api/wp-posts.php to display author metadata (name, slug, job title, bio) alongside their latest mirrored posts. |
| Logs (renderLogsPage) | Fetches /api/logs.php with limit/page and renders JSON log entries. |
/api/sources.php is queried with state=working and the candidate keywords to detect duplicates by URL and by overlapping keywords./api/user-counts.php to ensure the selected author stays under WORKING_SOURCES_LIMIT.setStatus() writes message text + tone CSS class (info, success, error). Pages consistently report loading/progress states through this helper.#/keywords/<term> or #/wp-keywords/).&, ', etc.). The frontend always calls resolveWordPressTitle() when displaying these strings to guarantee readable text and to avoid double-escaping.(無題) or muted “なし”.#/wp-authors/{id} for the mirrored author profile.runAutoRefresh() is triggered after startAutoRefresh() and on visibilitychange. Each page can set appState.pageRefresher to a custom async function; the auto refresher awaits it so new data trickles in without manual reloads.