Skip to content

refactor: 仕様書に基づいてコンポーネントを再設計#90

Merged
umekikazuya merged 3 commits into
developfrom
refactor/frontend
Apr 27, 2026
Merged

refactor: 仕様書に基づいてコンポーネントを再設計#90
umekikazuya merged 3 commits into
developfrom
refactor/frontend

Conversation

@umekikazuya
Copy link
Copy Markdown
Owner

Summary

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Walkthrough

ページ読み込み前にルートの data-theme をパスベースで初期化するインラインスクリプトを追加し、認証をイベント駆動に切り替える新規ガードとフォーム/リポジトリのイベント化を導入する一連のフロントエンド変更を含みます。

Changes

Cohort / File(s) Summary
Theme initialization
frontend/index.html
ドキュメントルートの data-themewindow.location.pathname/admin または /admin/*admin、それ以外 → public)に基づき、/src/main.ts 読み込み前に即時実行スクリプトで設定。
New auth guard component
frontend/src/components/admin/ui/me-auth-guard.ts
me-auth-guard Lit 要素を追加。authContext から IAuthRepository を購読し、リポジトリ更新ごとに Observer を差し替え、unknown 時に refreshSession() を呼び出す。認証状態によりスロット表示/メッセージ制御。
Form components (reflect + internals sync)
frontend/src/components/admin/ui/me-select.ts, frontend/src/components/admin/ui/me-text-input.ts, frontend/src/components/admin/ui/me-textarea.ts
プロパティを属性反映へ変更(label/name/value/disabled/required 等)、ElementInternals 同期を _syncInternals() に集約。checkValidity()/reportValidity() を削除し、disabled バインディングを内部要素へ移動。テンプレート/スタイル調整。
App shell / root routing and auth flow
frontend/src/components/app-admin-shell.ts, frontend/src/components/app-root.ts
コンテキスト消費/RepositoryObserver ベースの認証追跡を削除し、authenticated/isChecking の明示プロパティへ変更。ルート描画を <me-auth-guard> に切替、auth:status-change イベントに基づくリダイレクト/セッション更新ロジックを新しいハンドラに移設。イベントリスナーのライフサイクル解放を追加。
RepositoryObserver and domain EventTarget changes
frontend/src/controllers/RepositoryObserver.ts, frontend/src/domain/ArticleRepository.ts, frontend/src/domain/AuthRepository.ts, frontend/src/domain/ProfileRepository.ts
RepositoryObserver に optional onChange callback と AbortController ベースのリスナ管理を追加。複数リポジトリ型を EventTarget ベースに変更し、型付き addEventListener オーバーロードとカスタムイベント発行(article:admin-dirty-changeauth:status-changeprofile:public-changeprofile:admin-change)を導入。API 名の一部をエイリアス化し、status-change 発行を中央化。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: フロントエンド開発 #9: frontend entry/bootstrapping と index.html の変更箇所(インラインスクリプト/main.ts 読み込み順)で直接関連。
  • refactor: フロントエンド開発 #81: RepositoryObserver とリポジトリのイベント化(auth/profile/article)に対する変更点が重複しているため実装/型影響が強く関連。
  • feat: 管理画面のUX調整 #43: app-root 側のルーティング・認証連携の改修が重なっており、ルート描画とナビゲーション制御の衝突が発生する可能性が高い。

Suggested labels

type:chore

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning 説明文が空欄(概要の下がハイフンのみ)であり、変更内容についての有意な情報が含まれていない。 PR説明に変更内容の要約を追加してください。例えば、認証フロー、フォーム検証、コンポーネントの再設計内容について簡潔に説明することを推奨します。
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed タイトルはスペック書に基づくコンポーネント再設計というPRの主要な目的を的確に要約しており、変更セット全体と一致している。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/frontend

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
frontend/src/components/app-admin-shell.ts (1)

39-45: ⚠️ Potential issue | 🟡 Minor

認証確認中のステータスが二重表示されます。

Line 41-43 のメッセージを出しつつ Line 45 で slot も常に描画しているため、保護ルートでは <me-auth-guard> 側のローディング表示と重なります。待機中 UI はどちらか一方に寄せた方が自然です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/app-admin-shell.ts` around lines 39 - 45, The status
message and the default slot are both rendered causing duplicate loading UIs;
update the render logic in the component that uses isChecking (the template
around main/outlet and the isChecking property) so that when isChecking is true
you render only the status paragraph and do not render the <slot> (i.e.,
conditionally render the slot only when !this.isChecking) so the <me-auth-guard>
loading UI and this component's loading UI don't overlap.
frontend/src/domain/AuthRepository.ts (2)

175-189: ⚠️ Potential issue | 🟡 Minor

changeEmail()も同様に_accountError_accountSuccessをクリアしていない

revokeAllSessions()と同様に、changeEmail()も操作開始時にエラー/成功メッセージをクリアしていません。

🐛 修正案
   async changeEmail(input: ChangeEmailInput) {
     this._accountBusyAction = 'change-email'
+    this._accountError = ''
+    this._accountSuccess = ''
     this.notifyChange()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/domain/AuthRepository.ts` around lines 175 - 189, The
changeEmail method fails to clear prior status messages at start; update
changeEmail (in AuthRepository) to reset this._accountError = '' and
this._accountSuccess = '' (same approach used in revokeAllSessions) before
setting this._accountBusyAction and calling notifyChange, so previous
error/success messages don't persist across operations.

159-172: ⚠️ Potential issue | 🟡 Minor

revokeAllSessions()_accountError_accountSuccessがクリアされていない

logout()(Lines 120-121)では操作開始時に_accountError_accountSuccessをクリアしていますが、revokeAllSessions()ではクリアしていません。以前の操作からのメッセージが残る可能性があります。

また、logout()は成功時に_loginNoticeを設定していますが、revokeAllSessions()は設定していません。これは意図的な違いでしょうか?

🐛 一貫性のための修正案
   async revokeAllSessions() {
     this._accountBusyAction = 'revoke-sessions'
+    this._accountError = ''
+    this._accountSuccess = ''
     this.notifyChange()
     try {
       await apiRevokeAllSessions()
       this._status = 'guest'
+      this._loginNotice = 'すべてのセッションを無効化しました。'
+      this._accountSuccess = 'すべてのセッションを無効化しました。'
       this.dispatchStatusChange()
     } catch (error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/domain/AuthRepository.ts` around lines 159 - 172,
revokeAllSessions() currently doesn't clear prior status messages or set the
same success notice as logout; update revokeAllSessions to clear
this._accountError and this._accountSuccess at the start (same as logout()), and
on successful completion set this._loginNotice the same way logout() does (or
explicitly decide and document the difference), then
notifyChange/dispatchStatusChange as appropriate so stale messages aren't shown;
refer to the revokeAllSessions and logout methods and the fields _accountError,
_accountSuccess, and _loginNotice when making the change.
frontend/src/domain/ProfileRepository.ts (1)

89-119: 🧹 Nitpick | 🔵 Trivial

notifyChange()の重複呼び出しによる冗長なイベント発行

notifyPublicChange()は内部でnotifyChange()を呼び出していますが、loadPublicProfile()finallyブロック(Line 118)でもnotifyChange()が呼ばれます。成功時に'change'イベントが2回発行されます。

同様のパターンがloadAdminProfile()saveAdminProfile()にも存在します。saveAdminProfile()の成功時はnotifyAdminChange() + notifyPublicChange() + finallyで3回のnotifyChange()呼び出しになります。

RepositoryObserverが複数回の再描画をトリガーする可能性があります。

♻️ 提案: finallyブロックでの重複呼び出しを回避
   async loadPublicProfile() {
     if (this._publicProfile || this._publicLoading) return
     this._publicLoading = true
     this.notifyChange()
     try {
       await this._internalFetch()
+      this._publicLoading = false
       this.notifyPublicChange()
     } catch {
       // Fallback handled by components
+      this._publicLoading = false
+      this.notifyChange()
-    } finally {
-      this._publicLoading = false
-      this.notifyChange()
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/domain/ProfileRepository.ts` around lines 89 - 119,
notifyPublicChange() and notifyAdminChange() each call notifyChange() causing
duplicate 'change' events when callers (loadPublicProfile, loadAdminProfile,
saveAdminProfile) also call notifyChange() in their finally blocks; fix by
removing the internal notifyChange() calls from notifyPublicChange() and
notifyAdminChange() (so those methods only dispatch their specific CustomEvent),
and ensure callers invoke notifyChange() exactly once after successful/failed
flows (keep the finally-based notifyChange() or explicitly call notifyChange()
after calling notifyPublicChange()/notifyAdminChange() where necessary) to
prevent multiple redundant re-renders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/index.html`:
- Around line 15-22: The FOUC prevention script sets data-theme from
localStorage or prefers-color-scheme, but AppRoot
(frontend/src/components/app-root.ts — the AppRoot component) later overwrites
document.documentElement.setAttribute('data-theme') based on the URL path,
causing a flash; update the inline script to use the exact same path-based logic
as AppRoot to choose 'public' vs 'admin' (i.e., inspect location.pathname and
apply the same predicate AppRoot uses) and set
document.documentElement.setAttribute('data-theme', 'public'|'admin')
accordingly so the initial attribute matches AppRoot at load time.

In `@frontend/src/components/admin/ui/me-auth-guard.ts`:
- Around line 13-19: The authRepo setter only swaps the RepositoryObserver and
doesn't re-run session validation, so when a new IAuthRepository with state
'unknown' is injected the guard can remain stuck; update the authRepo setter
(the set authRepo(repo: IAuthRepository) method) so after assigning
this._authRepo and creating/replacing this._observer (RepositoryObserver) you
also invoke void this.checkSession() (or await if appropriate) to trigger
immediate session re-check; ensure you still disconnect the old observer
(this._observer?.disconnect()) before creating the new one.

In `@frontend/src/components/admin/ui/me-select.ts`:
- Around line 26-29: The formResetCallback currently forces an empty selection
by setting this.value = '' which prevents restoring the original selected
option; capture and persist the initial/default value when the component is
initialized (e.g., in the constructor or connectedCallback into a field like
this._initialValue) and then change formResetCallback to restore that stored
value (e.g., this.value = this._initialValue) before calling
this._syncInternals(); keep the existing symbols formResetCallback, this.value,
and this._syncInternals to locate and update the code.

In `@frontend/src/components/admin/ui/me-text-input.ts`:
- Around line 44-46: formResetCallback currently forces this.value to '' so
form.reset() cannot restore the component's initial/default value; change it to
restore a stored initial value (e.g., set this.value = this._initialValue) and
keep the existing this._syncInternals() call. Ensure you capture and store that
initial value when the element is created/connected (in the constructor or
connectedCallback) by reading the initial this.value or the initial attribute
(e.g., this.getAttribute('value') or a defaultValue prop) into
this._initialValue so formResetCallback and other logic can restore the original
default.

In `@frontend/src/components/admin/ui/me-textarea.ts`:
- Around line 31-33: The formResetCallback currently forces this.value = ''
which breaks restoring initial values on form.reset(); change formResetCallback
(in me-textarea) to restore the saved initial/default value (the same mechanism
used by me-text-input and me-select) instead of hardcoding empty string — e.g.
set this.value = this._initialValue (or this._defaultValue / the stored default
you use elsewhere) and then call this._syncInternals(); ensure the initial value
is captured at construction/connectedCallback so reset can revert to it.

In `@frontend/src/components/app-root.ts`:
- Around line 169-175: handleRouteChange currently calls auth.refreshSession
only for routes where isProtectedAdminPath(this.currentPath) is true, so
navigating directly to "/admin/login" (not wrapped by me-auth-guard) skips
session recheck; update handleRouteChange to also call await
this.auth.refreshSession() when this.currentPath === '/admin/login' and
this.auth.status === 'unknown' (in addition to the existing protected-route
check) so that auth.status is populated and authenticated users get redirected;
locate the method handleRouteChange and the use of isProtectedAdminPath,
currentPath, and auth.refreshSession/auth.status to implement this extra
conditional.

In `@frontend/src/domain/AuthRepository.ts`:
- Around line 90-97: dispatchStatusChange currently calls notifyChange and
callers like login, logout, revokeAllSessions also call notifyChange in their
finally blocks, causing duplicate notifications; follow the refreshSession
pattern instead: remove the notifyChange() call from dispatchStatusChange()
(leave it to only dispatch the CustomEvent) and keep the existing notifyChange()
calls in the callers (login, logout, revokeAllSessions) so each status change
results in a single notifyChange invocation; update references to
dispatchStatusChange(), login(), logout(), revokeAllSessions(), and
refreshSession() accordingly.

---

Outside diff comments:
In `@frontend/src/components/app-admin-shell.ts`:
- Around line 39-45: The status message and the default slot are both rendered
causing duplicate loading UIs; update the render logic in the component that
uses isChecking (the template around main/outlet and the isChecking property) so
that when isChecking is true you render only the status paragraph and do not
render the <slot> (i.e., conditionally render the slot only when
!this.isChecking) so the <me-auth-guard> loading UI and this component's loading
UI don't overlap.

In `@frontend/src/domain/AuthRepository.ts`:
- Around line 175-189: The changeEmail method fails to clear prior status
messages at start; update changeEmail (in AuthRepository) to reset
this._accountError = '' and this._accountSuccess = '' (same approach used in
revokeAllSessions) before setting this._accountBusyAction and calling
notifyChange, so previous error/success messages don't persist across
operations.
- Around line 159-172: revokeAllSessions() currently doesn't clear prior status
messages or set the same success notice as logout; update revokeAllSessions to
clear this._accountError and this._accountSuccess at the start (same as
logout()), and on successful completion set this._loginNotice the same way
logout() does (or explicitly decide and document the difference), then
notifyChange/dispatchStatusChange as appropriate so stale messages aren't shown;
refer to the revokeAllSessions and logout methods and the fields _accountError,
_accountSuccess, and _loginNotice when making the change.

In `@frontend/src/domain/ProfileRepository.ts`:
- Around line 89-119: notifyPublicChange() and notifyAdminChange() each call
notifyChange() causing duplicate 'change' events when callers
(loadPublicProfile, loadAdminProfile, saveAdminProfile) also call notifyChange()
in their finally blocks; fix by removing the internal notifyChange() calls from
notifyPublicChange() and notifyAdminChange() (so those methods only dispatch
their specific CustomEvent), and ensure callers invoke notifyChange() exactly
once after successful/failed flows (keep the finally-based notifyChange() or
explicitly call notifyChange() after calling
notifyPublicChange()/notifyAdminChange() where necessary) to prevent multiple
redundant re-renders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 30d9f106-db00-4749-b876-2b899ebc1d1f

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa7769 and 36b89c0.

📒 Files selected for processing (11)
  • frontend/index.html
  • frontend/src/components/admin/ui/me-auth-guard.ts
  • frontend/src/components/admin/ui/me-select.ts
  • frontend/src/components/admin/ui/me-text-input.ts
  • frontend/src/components/admin/ui/me-textarea.ts
  • frontend/src/components/app-admin-shell.ts
  • frontend/src/components/app-root.ts
  • frontend/src/controllers/RepositoryObserver.ts
  • frontend/src/domain/ArticleRepository.ts
  • frontend/src/domain/AuthRepository.ts
  • frontend/src/domain/ProfileRepository.ts

Comment thread frontend/index.html
Comment on lines +13 to +19
@consume({ context: authContext, subscribe: true })
set authRepo(repo: IAuthRepository) {
if (this._authRepo === repo) return
this._authRepo = repo
this._observer?.disconnect()
if (repo) this._observer = new RepositoryObserver(this, repo)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

authRepo の差し替え時に checkSession() が再実行されません。

この setter は observer を張り替えるだけなので、新しい repo が 'unknown' のまま入ってきたケースを取りこぼします。差し替え直後にも void this.checkSession() を呼んでおかないと、ガードが空描画のまま止まる可能性があります。

修正案
   set authRepo(repo: IAuthRepository) {
     if (this._authRepo === repo) return
     this._authRepo = repo
     this._observer?.disconnect()
-    if (repo) this._observer = new RepositoryObserver(this, repo)
+    if (repo) {
+      this._observer = new RepositoryObserver(this, repo)
+      void this.checkSession()
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@consume({ context: authContext, subscribe: true })
set authRepo(repo: IAuthRepository) {
if (this._authRepo === repo) return
this._authRepo = repo
this._observer?.disconnect()
if (repo) this._observer = new RepositoryObserver(this, repo)
}
`@consume`({ context: authContext, subscribe: true })
set authRepo(repo: IAuthRepository) {
if (this._authRepo === repo) return
this._authRepo = repo
this._observer?.disconnect()
if (repo) {
this._observer = new RepositoryObserver(this, repo)
void this.checkSession()
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/admin/ui/me-auth-guard.ts` around lines 13 - 19, The
authRepo setter only swaps the RepositoryObserver and doesn't re-run session
validation, so when a new IAuthRepository with state 'unknown' is injected the
guard can remain stuck; update the authRepo setter (the set authRepo(repo:
IAuthRepository) method) so after assigning this._authRepo and
creating/replacing this._observer (RepositoryObserver) you also invoke void
this.checkSession() (or await if appropriate) to trigger immediate session
re-check; ensure you still disconnect the old observer
(this._observer?.disconnect()) before creating the new one.

Comment on lines 26 to 29
formResetCallback() {
this.value = ''
this._internals.setFormValue('')
this._syncInternals()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

formResetCallback() が空選択へ固定されています。

Line 27 で常に '' を入れると、初期選択があるフォームでも reset 後に元の option へ戻りません。ここも既定値へ戻す実装にしてください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/admin/ui/me-select.ts` around lines 26 - 29, The
formResetCallback currently forces an empty selection by setting this.value = ''
which prevents restoring the original selected option; capture and persist the
initial/default value when the component is initialized (e.g., in the
constructor or connectedCallback into a field like this._initialValue) and then
change formResetCallback to restore that stored value (e.g., this.value =
this._initialValue) before calling this._syncInternals(); keep the existing
symbols formResetCallback, this.value, and this._syncInternals to locate and
update the code.

Comment on lines 44 to +46
formResetCallback() {
this.value = ''
this._internals.setFormValue('')
this._syncInternals()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

formResetCallback() が初期値ではなく空文字へ固定されています。

Line 45 で常に '' を代入しているため、初期値付きのフォームでも form.reset() が元の値へ戻りません。ここは空文字固定ではなく、コンポーネントの既定値へ戻す必要があります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/admin/ui/me-text-input.ts` around lines 44 - 46,
formResetCallback currently forces this.value to '' so form.reset() cannot
restore the component's initial/default value; change it to restore a stored
initial value (e.g., set this.value = this._initialValue) and keep the existing
this._syncInternals() call. Ensure you capture and store that initial value when
the element is created/connected (in the constructor or connectedCallback) by
reading the initial this.value or the initial attribute (e.g.,
this.getAttribute('value') or a defaultValue prop) into this._initialValue so
formResetCallback and other logic can restore the original default.

Comment on lines 31 to +33
formResetCallback() {
this.value = ''
this._internals.setFormValue('')
this._syncInternals()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

textarea も reset 時に初期値へ戻っていません。

Line 32 が空文字固定なので、既定値付きフォームで form.reset() しても初期内容を復元できません。me-text-input/me-select と同様に、ここも保存している既定値へ戻す必要があります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/admin/ui/me-textarea.ts` around lines 31 - 33, The
formResetCallback currently forces this.value = '' which breaks restoring
initial values on form.reset(); change formResetCallback (in me-textarea) to
restore the saved initial/default value (the same mechanism used by
me-text-input and me-select) instead of hardcoding empty string — e.g. set
this.value = this._initialValue (or this._defaultValue / the stored default you
use elsewhere) and then call this._syncInternals(); ensure the initial value is
captured at construction/connectedCallback so reset can revert to it.

Comment thread frontend/src/components/app-root.ts
Comment on lines +90 to +97
private dispatchStatusChange() {
this.dispatchEvent(
new CustomEvent('auth:status-change', {
detail: { status: this._status },
}),
)
this.notifyChange()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

ProfileRepositoryと同様のnotifyChange()重複呼び出し

dispatchStatusChange()が内部でnotifyChange()を呼び出していますが、login()finallyブロック(Line 114)でもnotifyChange()が呼ばれます。logout()revokeAllSessions()も同様です。

一方、refreshSession()finallyブロックでnotifyChange()を呼ばないため、重複がありません。これを他のメソッドの参考パターンにできます。

Also applies to: 99-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/domain/AuthRepository.ts` around lines 90 - 97,
dispatchStatusChange currently calls notifyChange and callers like login,
logout, revokeAllSessions also call notifyChange in their finally blocks,
causing duplicate notifications; follow the refreshSession pattern instead:
remove the notifyChange() call from dispatchStatusChange() (leave it to only
dispatch the CustomEvent) and keep the existing notifyChange() calls in the
callers (login, logout, revokeAllSessions) so each status change results in a
single notifyChange invocation; update references to dispatchStatusChange(),
login(), logout(), revokeAllSessions(), and refreshSession() accordingly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
frontend/src/components/app-root.ts (1)

166-176: ⚠️ Potential issue | 🟠 Major

/admin/login の再確認に refreshSession() をそのまま使うと 401 通知の副作用が出ます。

frontend/src/domain/AuthRepository.ts:138-157refreshSession() は 401 で status = 'guest' だけでなく loginNotice = 'セッションが切れました。' も設定します。ここの無条件呼び出しだと、未ログインで /admin/login を開いただけのケースやログアウト直後の遷移でも、不要な再確認と期限切れ通知が発生します。ログインページのブートストラップは silent な再確認に分けるか、最低でも status === 'unknown' のときだけ呼ぶようにしてください。

最小修正案
  private async handleRouteChange() {
-    if (this.currentPath === '/admin/login') {
+    if (
+      this.currentPath === '/admin/login' &&
+      this.auth.status === 'unknown'
+    ) {
       await this.auth.refreshSession()
       return
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/app-root.ts` around lines 166 - 176, The current
handleRouteChange calls auth.refreshSession() unconditionally for
'/admin/login', but AuthRepository.refreshSession() sets loginNotice on 401,
causing unwanted "session expired" notices when simply opening the login page;
change handleRouteChange so it only triggers a full refresh when auth.status ===
'unknown' or else call a silent recheck method instead (e.g. implement and call
auth.silentRefreshSession() that mirrors AuthRepository.refreshSession() without
setting loginNotice on 401). Update the logic in handleRouteChange (and add the
silent method to the auth API if missing) so '/admin/login' bootstraps via a
silent check or is gated by auth.status === 'unknown' to prevent spurious
notices.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@frontend/src/components/app-root.ts`:
- Around line 166-176: The current handleRouteChange calls auth.refreshSession()
unconditionally for '/admin/login', but AuthRepository.refreshSession() sets
loginNotice on 401, causing unwanted "session expired" notices when simply
opening the login page; change handleRouteChange so it only triggers a full
refresh when auth.status === 'unknown' or else call a silent recheck method
instead (e.g. implement and call auth.silentRefreshSession() that mirrors
AuthRepository.refreshSession() without setting loginNotice on 401). Update the
logic in handleRouteChange (and add the silent method to the auth API if
missing) so '/admin/login' bootstraps via a silent check or is gated by
auth.status === 'unknown' to prevent spurious notices.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: abe0c725-a12f-4e30-9b7c-c7fc77c8bdc9

📥 Commits

Reviewing files that changed from the base of the PR and between 36b89c0 and 528f22c.

📒 Files selected for processing (2)
  • frontend/index.html
  • frontend/src/components/app-root.ts

@umekikazuya umekikazuya merged commit 2456786 into develop Apr 27, 2026
1 check passed
@umekikazuya umekikazuya deleted the refactor/frontend branch April 27, 2026 14:16
@coderabbitai coderabbitai Bot mentioned this pull request May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant