Skip to content

feat(web): settings feature — domain, application, infrastructure and presentation layers#78

Merged
ThiagoDelgado-D merged 9 commits into
mainfrom
feat/v0.8.3-settings-ui
May 6, 2026
Merged

feat(web): settings feature — domain, application, infrastructure and presentation layers#78
ThiagoDelgado-D merged 9 commits into
mainfrom
feat/v0.8.3-settings-ui

Conversation

@ThiagoDelgado-D

@ThiagoDelgado-D ThiagoDelgado-D commented May 6, 2026

Copy link
Copy Markdown
Owner

feat(web): Settings UI — account, modules, widgets and session management

Introduces the full settings feature following the same Clean Architecture
structure as learning-resource and auth (domain/, application/,
infrastructure/, presentation/). Accessible at /settings from the shell
sidebar link added in the previous branch.


What's new

Domain

Two abstract repository classes — PreferencesRepository (feature config +
widget config CRUD) and SessionRepository (list, revoke one, revoke all
others) — plus settings.model.ts defining the WidgetKey const object and
the Session interface used across layers.

Application — PreferencesService and SessionService

Injectable services scoped to the settings layout (not providedIn: 'root').
Both expose signals for state (featureConfig, widgetConfig, sessions,
loading, saving, error) and implement optimistic updates with automatic
rollback — the signal is updated immediately on user action, the HTTP call is
fired, and on failure the previous value is restored.

Infrastructure

PreferencesHttpRepository hits PATCH /api/v1/preferences/features and
PATCH /api/v1/preferences/widgets. SessionHttpRepository hits
GET /auth/sessions, DELETE /auth/sessions/:id, and
DELETE /auth/sessions. Both extend their domain abstract classes and are
provided via Angular's class token DI in SettingsLayoutComponent.

Presentation — SettingsLayoutComponent

Two-column layout: a fixed left sidebar with four nav groups (Account,
Workspace, Security, Data) and a <router-outlet> main area. Declares
providers for both services and both repository implementations — services
are created once per settings visit and destroyed on leave.

Modules tab (/settings/modules)

Toggle list for all five FeatureKey values. Resource Library is locked
(Always on badge, toggle disabled). Each toggle calls
preferencesService.updateFeatureConfig() with the new set, which hits the
API optimistically. enabledKeys is a computed() signal derived from the
service state.

Dashboard Widgets tab (/settings/widgets)

Same toggle pattern for the five WidgetKey values, backed by
updateWidgetConfig().

Active Sessions tab (/settings/sessions)

Loads all sessions on init, displays browser name and OS extracted from
userAgent (Edge, Chrome, Firefox, Safari, Opera, with Windows/macOS/Linux/
iOS/Android detection). Current session highlighted with a violet border and
This device badge. Individual Revoke buttons on all non-current rows.
Sign out all other devices button appears only when other sessions exist.
All revocation is optimistic — list updates instantly, rolls back on error.

Account tab (/settings/account)

Read-only profile card using AuthStore.userInitials() and
AuthStore.displayName() signals. Shows first/last name, email, enabled
modules as violet badges, and account status.

Stub tabs (preferences, notifications, security, import-export,
danger-zone) — render a "Coming soon" placeholder, present in the sidebar to
establish the full navigation structure.

Routing

settingsRoutes lazy-loads SettingsLayoutComponent as parent with nine
child routes. Mounted under learningResourceRoutes at settings so it
renders inside ShellLayoutComponent and inherits the authGuard.

Summary by CodeRabbit

  • New Features
    • Added comprehensive Settings section with account, workspace, security, and data management areas
    • Added ability to enable/disable feature modules
    • Added ability to configure dashboard widgets
    • Added active sessions management with ability to view and revoke sessions
    • Added account information and status overview page
    • Added placeholder pages for notifications, preferences, security, and import/export features

@ThiagoDelgado-D ThiagoDelgado-D self-assigned this May 6, 2026
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@ThiagoDelgado-D has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 36 minutes and 27 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60371a46-31ee-462d-81af-2cac83076276

📥 Commits

Reviewing files that changed from the base of the PR and between f95afee and cf5204f.

📒 Files selected for processing (9)
  • apps/web/src/app/core/guards/auth.guard.ts
  • apps/web/src/app/features/settings/application/preferences.service.ts
  • apps/web/src/app/features/settings/application/session.service.ts
  • apps/web/src/app/features/settings/presentation/account/account.component.ts
  • apps/web/src/app/features/settings/presentation/modules/modules.component.html
  • apps/web/src/app/features/settings/presentation/sessions/sessions.component.html
  • apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts
  • apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html
  • apps/web/src/app/features/settings/presentation/widgets/widgets.component.html
📝 Walkthrough

Walkthrough

A new settings feature is introduced with routing, domain models, abstract and HTTP-based repositories, application services using signal-based state, and a complete presentation layer including components for account, sessions, modules, widgets, and placeholder pages.

Changes

Settings Feature Implementation

Layer / File(s) Summary
Routing Integration
apps/web/src/app/features/learning-resource/presentation/learning-resource.routes.ts
Learning-resource routes now expose a new lazy-loaded child route 'settings' that loads settingsRoutes from the settings module.
Domain Models & Types
apps/web/src/app/features/settings/domain/settings.model.ts
Introduces WIDGET_KEY constant with keys for five dashboard widgets, WidgetKey type derived from keys, and Session interface with id, userAgent, ipAddress, createdAt, expiresAt, isCurrent.
Domain Repositories
apps/web/src/app/features/settings/domain/preferences.repository.ts, apps/web/src/app/features/settings/domain/session.repository.ts
Abstract PreferencesRepository defines methods to get/update feature and widget configs; abstract SessionRepository defines methods to get sessions and revoke by ID or all others.
DTOs
apps/web/src/app/features/settings/infrastructure/settings.dto.ts
Data transfer objects defined: FeatureConfigDto, WidgetConfigDto, SessionDto for HTTP layer type safety.
HTTP Repositories
apps/web/src/app/features/settings/infrastructure/preferences-http.repository.ts, apps/web/src/app/features/settings/infrastructure/session-http.repository.ts
Concrete implementations of abstract repositories; PreferencesHttpRepository calls /preferences/features and /preferences/widgets endpoints; SessionHttpRepository calls /auth/sessions endpoints with GET, PATCH, and DELETE operations.
Application Services
apps/web/src/app/features/settings/application/preferences.service.ts, apps/web/src/app/features/settings/application/session.service.ts
PreferencesService exposes featureConfig and widgetConfig signals plus loading/saving/error states; provides methods to load and update configurations with try/catch and optimistic updates. SessionService exposes sessions, loading, error signals; provides load, revoke single, and revoke-all-others methods with error rollback.
Settings Layout & Routes
apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.ts, apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html, apps/web/src/app/features/settings/presentation/settings.routes.ts
SettingsLayoutComponent wires providers mapping abstract repositories to HTTP implementations and provides PreferencesService and SessionService; layout template renders left sidebar navigation (Account, Workspace, Security, Data groups) with router outlet for child routes. Routes file exports settingsRoutes with lazy-loaded child components for account, preferences, notifications, modules, widgets, sessions, security, import-export, danger-zone.
Account Component
apps/web/src/app/features/settings/presentation/account/account.component.ts
Standalone component injecting AuthStore; displays user avatar, display name, email, profile fields, active modules from feature config, and account status.
Modules Component
apps/web/src/app/features/settings/presentation/modules/modules.component.ts, apps/web/src/app/features/settings/presentation/modules/modules.component.html
ModulesComponent injects PreferencesService; displays five module cards (Resource Library locked, Learning Paths, Atlas, Pomodoro, Spaced Repetition) with per-module toggle controls; computes enabled keys from service state; provides isOn() and toggle() methods with validation against locked state and save progress.
Widgets Component
apps/web/src/app/features/settings/presentation/widgets/widgets.component.ts, apps/web/src/app/features/settings/presentation/widgets/widgets.component.html
WidgetsComponent injects PreferencesService; displays five dashboard widgets (System Check, Ideal Match, Focus Pulse, Architect's Pulse, Pending Tasks) with toggle controls; provides isOn() and toggle() methods; computes enabled keys from service state.
Sessions Component
apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts, apps/web/src/app/features/settings/presentation/sessions/sessions.component.html
SessionsComponent injects SessionService; loads and displays active sessions with browser/OS detection from userAgent; provides revoke() and revokeAllOthers() actions; template shows loading/empty/error states and per-session Revoke buttons (disabled for current device).
Placeholder Components
apps/web/src/app/features/settings/presentation/preferences/preferences.component.ts, apps/web/src/app/features/settings/presentation/notifications/notifications.component.ts, apps/web/src/app/features/settings/presentation/security/security.component.ts, apps/web/src/app/features/settings/presentation/import-export/import-export.component.ts, apps/web/src/app/features/settings/presentation/danger-zone/danger-zone.component.ts
Five standalone placeholder components with "Coming soon" templates for future feature development.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

feature, architecture/clean-separation, review-effort/moderate


🐰 A settings home for all to see,
With modules, widgets, sessions free,
From domain down to UI frame,
Clean layers built—no two the same.
Preferences and security reign,
A feature complete without strain. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main change: introducing a complete settings feature across all architectural layers (domain, application, infrastructure, and presentation).
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v0.8.3-settings-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add complete settings feature with preferences, sessions, and account management

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Introduces complete settings feature with domain, application, infrastructure, and presentation
  layers following Clean Architecture
• Implements preferences service with optimistic updates for feature and widget configuration
  management
• Adds session management with browser/OS detection and device revocation capabilities
• Creates settings layout with four-column navigation (Account, Workspace, Security, Data) and nine
  child routes
• Provides read-only account profile card and stub tabs for future features (preferences,
  notifications, security, import-export, danger-zone)
Diagram
flowchart LR
  A["Settings Routes<br/>settings.routes.ts"] --> B["Settings Layout<br/>Component"]
  B --> C["Account Tab"]
  B --> D["Modules Tab"]
  B --> E["Widgets Tab"]
  B --> F["Sessions Tab"]
  B --> G["Stub Tabs"]
  D --> H["PreferencesService"]
  E --> H
  H --> I["PreferencesRepository"]
  I --> J["PreferencesHttpRepository"]
  F --> K["SessionService"]
  K --> L["SessionRepository"]
  L --> M["SessionHttpRepository"]
  J --> N["API: PATCH /preferences"]
  M --> O["API: GET/DELETE /auth/sessions"]
Loading

Grey Divider

File Changes

1. apps/web/src/app/features/learning-resource/presentation/learning-resource.routes.ts Routing +7/-0

Add settings route to learning resource routes

apps/web/src/app/features/learning-resource/presentation/learning-resource.routes.ts


2. apps/web/src/app/features/settings/domain/settings.model.ts Domain model +18/-0

Define widget keys and session interface

apps/web/src/app/features/settings/domain/settings.model.ts


3. apps/web/src/app/features/settings/domain/preferences.repository.ts Domain abstraction +9/-0

Abstract repository for preferences CRUD operations

apps/web/src/app/features/settings/domain/preferences.repository.ts


View more (21)
4. apps/web/src/app/features/settings/domain/session.repository.ts Domain abstraction +7/-0

Abstract repository for session management

apps/web/src/app/features/settings/domain/session.repository.ts


5. apps/web/src/app/features/settings/application/preferences.service.ts Application service +73/-0

Service with optimistic updates for feature and widget config

apps/web/src/app/features/settings/application/preferences.service.ts


6. apps/web/src/app/features/settings/application/session.service.ts Application service +47/-0

Service for session loading and revocation with optimistic updates

apps/web/src/app/features/settings/application/session.service.ts


7. apps/web/src/app/features/settings/infrastructure/settings.dto.ts Infrastructure model +19/-0

Data transfer objects for API responses

apps/web/src/app/features/settings/infrastructure/settings.dto.ts


8. apps/web/src/app/features/settings/infrastructure/preferences-http.repository.ts Infrastructure repository +38/-0

HTTP implementation for preferences repository

apps/web/src/app/features/settings/infrastructure/preferences-http.repository.ts


9. apps/web/src/app/features/settings/infrastructure/session-http.repository.ts Infrastructure repository +37/-0

HTTP implementation for session repository with DTO mapping

apps/web/src/app/features/settings/infrastructure/session-http.repository.ts


10. apps/web/src/app/features/settings/presentation/settings.routes.ts Routing +61/-0

Settings routing configuration with nine child routes

apps/web/src/app/features/settings/presentation/settings.routes.ts


11. apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.ts Presentation component +22/-0

Main settings layout with DI providers for services

apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.ts


12. apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html Presentation template +145/-0

Two-column layout with sidebar navigation and main outlet

apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html


13. apps/web/src/app/features/settings/presentation/account/account.component.ts Presentation component +77/-0

Read-only account profile card with user information

apps/web/src/app/features/settings/presentation/account/account.component.ts


14. apps/web/src/app/features/settings/presentation/modules/modules.component.ts Presentation component +87/-0

Toggle list for feature configuration with locked resource library

apps/web/src/app/features/settings/presentation/modules/modules.component.ts


15. apps/web/src/app/features/settings/presentation/modules/modules.component.html Presentation template +77/-0

Module cards with toggle switches and loading states

apps/web/src/app/features/settings/presentation/modules/modules.component.html


16. apps/web/src/app/features/settings/presentation/widgets/widgets.component.ts Presentation component +80/-0

Toggle list for dashboard widget configuration

apps/web/src/app/features/settings/presentation/widgets/widgets.component.ts


17. apps/web/src/app/features/settings/presentation/widgets/widgets.component.html Presentation template +70/-0

Widget cards with toggle switches and error handling

apps/web/src/app/features/settings/presentation/widgets/widgets.component.html


18. apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts Presentation component +54/-0

Session list with browser/OS detection and revocation logic

apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts


19. apps/web/src/app/features/settings/presentation/sessions/sessions.component.html Presentation template +83/-0

Session list display with revoke buttons and current device badge

apps/web/src/app/features/settings/presentation/sessions/sessions.component.html


20. apps/web/src/app/features/settings/presentation/preferences/preferences.component.ts Presentation component +18/-0

Stub component for future preferences feature

apps/web/src/app/features/settings/presentation/preferences/preferences.component.ts


21. apps/web/src/app/features/settings/presentation/notifications/notifications.component.ts Presentation component +18/-0

Stub component for future notifications feature

apps/web/src/app/features/settings/presentation/notifications/notifications.component.ts


22. apps/web/src/app/features/settings/presentation/security/security.component.ts Presentation component +18/-0

Stub component for future security settings feature

apps/web/src/app/features/settings/presentation/security/security.component.ts


23. apps/web/src/app/features/settings/presentation/import-export/import-export.component.ts Presentation component +18/-0

Stub component for future import/export feature

apps/web/src/app/features/settings/presentation/import-export/import-export.component.ts


24. apps/web/src/app/features/settings/presentation/danger-zone/danger-zone.component.ts Presentation component +18/-0

Stub component for future destructive account actions

apps/web/src/app/features/settings/presentation/danger-zone/danger-zone.component.ts


Grey Divider

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0)

Grey Divider


Action required

1. Session rollback race 🐞 Bug ≡ Correctness
Description
SessionService’s optimistic revoke methods rollback by restoring a full-list snapshot (prev), but
they don’t prevent concurrent revoke operations. If multiple revokes run and one later fails,
restoring prev can re-add sessions that were successfully removed by another revoke.
Code

apps/web/src/app/features/settings/application/session.service.ts[R26-46]

+  async revokeSession(sessionId: string): Promise<void> {
+    const prev = this.sessions();
+    this.sessions.update((list) => list.filter((s) => s.id !== sessionId));
+    try {
+      await this.repository.revokeSession(sessionId);
+    } catch {
+      this.sessions.set(prev);
+      this.error.set('Failed to revoke session');
+    }
+  }
+
+  async revokeAllOtherSessions(): Promise<void> {
+    const prev = this.sessions();
+    this.sessions.update((list) => list.filter((s) => s.isCurrent));
+    try {
+      await this.repository.revokeAllOtherSessions();
+    } catch {
+      this.sessions.set(prev);
+      this.error.set('Failed to revoke sessions');
+    }
+  }
Evidence
Both revokeSession() and revokeAllOtherSessions() snapshot the entire list and restore it on
error. The sessions UI provides revoke buttons without any in-flight disable/guard, so users can
trigger overlapping requests; any failure path calling this.sessions.set(prev) can overwrite newer
successful optimistic changes.

apps/web/src/app/features/settings/application/session.service.ts[26-46]
apps/web/src/app/features/settings/presentation/sessions/sessions.component.html[8-16]
apps/web/src/app/features/settings/presentation/sessions/sessions.component.html[66-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SessionService` uses full-snapshot rollback (`prev`) for optimistic session revocations. With concurrent revocation actions, a later failure can restore a stale snapshot and undo other successful removals.

### Issue Context
The Sessions UI exposes multiple revoke actions without disabling during in-flight operations, making concurrent invocations plausible.

### Fix Focus Areas
- apps/web/src/app/features/settings/application/session.service.ts[26-46]
- apps/web/src/app/features/settings/presentation/sessions/sessions.component.html[8-16]
- apps/web/src/app/features/settings/presentation/sessions/sessions.component.html[66-74]

### Suggested fix
Choose one (or combine):
- **Serialize operations**: add a `revoking = signal(false)` (or `pending = signal<number>(0)`) and guard `if (revoking()) return;`, set/reset in `try/finally`. Bind `[disabled]` on revoke buttons while revoking.
- **Targeted rollback**: instead of `this.sessions.set(prev)`, restore only what this specific operation removed (e.g., keep a map of removed sessions by id and only re-insert those ids if still missing), so one failure cannot overwrite other changes.
- Also consider clearing `error` at the start of revoke actions to avoid stale errors lingering after success.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Loading flag concurrency bug 🐞 Bug ☼ Reliability
Description
PreferencesService uses a single boolean loading for both loadFeatureConfig() and
loadWidgetConfig(), so overlapping loads can incorrectly set loading to false while another
request is still in-flight. This can cause the Modules/Widgets pages to render as “not loading” with
stale/empty config.
Code

apps/web/src/app/features/settings/application/preferences.service.ts[R10-55]

+  readonly featureConfig = signal<FeatureKey[]>([]);
+  readonly widgetConfig = signal<WidgetKey[]>([]);
+  readonly loading = signal(false);
+  readonly saving = signal(false);
+  readonly error = signal<string | null>(null);
+
+  async loadFeatureConfig(): Promise<void> {
+    this.loading.set(true);
+    this.error.set(null);
+    try {
+      const config = await this.repository.getFeatureConfig();
+      this.featureConfig.set(config);
+    } catch {
+      this.error.set('Failed to load feature configuration');
+    } finally {
+      this.loading.set(false);
+    }
+  }
+
+  async updateFeatureConfig(config: FeatureKey[]): Promise<void> {
+    this.saving.set(true);
+    this.error.set(null);
+    const prev = this.featureConfig();
+    this.featureConfig.set(config);
+    try {
+      const updated = await this.repository.updateFeatureConfig(config);
+      this.featureConfig.set(updated);
+    } catch {
+      this.featureConfig.set(prev);
+      this.error.set('Failed to save feature configuration');
+    } finally {
+      this.saving.set(false);
+    }
+  }
+
+  async loadWidgetConfig(): Promise<void> {
+    this.loading.set(true);
+    this.error.set(null);
+    try {
+      const config = await this.repository.getWidgetConfig();
+      this.widgetConfig.set(config);
+    } catch {
+      this.error.set('Failed to load widget configuration');
+    } finally {
+      this.loading.set(false);
+    }
Evidence
PreferencesService flips the same loading signal to true/false in both load methods. The service
is provided at SettingsLayoutComponent scope (shared across child routes), and both Modules and
Widgets child components call different load methods on init; navigating between them quickly can
overlap requests and make loading incorrect because each call sets loading=false in its own
finally.

apps/web/src/app/features/settings/application/preferences.service.ts[10-56]
apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.ts[10-19]
apps/web/src/app/features/settings/presentation/modules/modules.component.ts[61-70]
apps/web/src/app/features/settings/presentation/widgets/widgets.component.ts[59-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PreferencesService.loading` is a single boolean toggled by both `loadFeatureConfig()` and `loadWidgetConfig()`. If both are invoked concurrently, whichever finishes first will set `loading=false` even though the other request is still in progress.

### Issue Context
The service is scoped to `SettingsLayoutComponent` and shared by multiple child routes (Modules and Widgets). Navigating between child routes doesn’t cancel the previous in-flight HTTP request, so overlaps are realistic.

### Fix Focus Areas
- apps/web/src/app/features/settings/application/preferences.service.ts[10-56]
- apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.ts[10-19]
- apps/web/src/app/features/settings/presentation/modules/modules.component.ts[61-70]
- apps/web/src/app/features/settings/presentation/widgets/widgets.component.ts[59-68]

### Suggested fix
Implement either:
1) Separate flags (`loadingFeatures`, `loadingWidgets`) and expose `loading = computed(() => loadingFeatures() || loadingWidgets())`, or
2) A reference-count (`pendingLoads`) incremented on start and decremented in `finally`, with `loading = computed(() => pendingLoads() > 0)`.

This prevents premature `loading=false` when requests overlap.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. OS detection order wrong 🐞 Bug ≡ Correctness
Description
SessionsComponent.getOS() checks /Mac OS X/ before /iPhone|iPad/, so any user agent string
matching both patterns will be labeled as macOS instead of iOS. This breaks the intended OS labeling
for some sessions.
Code

apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts[R41-48]

+  getOS(userAgent: string | null): string {
+    if (!userAgent) return '';
+    if (/Windows NT/.test(userAgent)) return 'Windows';
+    if (/Mac OS X/.test(userAgent)) return 'macOS';
+    if (/Android/.test(userAgent)) return 'Android';
+    if (/iPhone|iPad/.test(userAgent)) return 'iOS';
+    if (/Linux/.test(userAgent)) return 'Linux';
+    return '';
Evidence
The function returns immediately on the first matching branch; since the macOS regex check precedes
the iOS check, strings that contain both tokens will always resolve to macOS and never reach the iOS
branch.

apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts[41-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`getOS()` returns 'macOS' before checking for iOS tokens, so user agents matching both patterns will be misclassified.

### Issue Context
The logic is order-dependent because it returns on the first match.

### Fix Focus Areas
- apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts[41-48]

### Suggested fix
Reorder checks to test iOS before macOS, e.g.:
- check `/iPhone|iPad/` first
- then check `/Mac OS X/`

Optionally consider also reordering browser detection similarly (e.g., Opera before Chrome) if you want consistent classification behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Qodo Logo

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

✅ All CI checks passed! The code is ready for review.

Comment thread apps/web/src/app/features/settings/application/session.service.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🧹 Nitpick comments (1)
apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html (1)

15-15: ⚡ Quick win

Migrate to Tailwind v4 postfix important modifier syntax — deprecated prefix form still works but should be updated

The code uses Tailwind v3 prefix syntax for the important modifier (!text-violet-400), which is deprecated in v4 but remains functional through backwards compatibility. For consistency with v4 conventions and to future-proof the code, migrate these class names to use the postfix form (text-violet-400!).

Update all 9 occurrences to the v4 postfix syntax
-  routerLinkActive="bg-slate-800 !text-violet-400"
+  routerLinkActive="bg-slate-800 text-violet-400!"
-  routerLinkActive="bg-slate-800 !text-red-400"
+  routerLinkActive="bg-slate-800 text-red-400!"

Applies to lines: 15, 26, 37, 55, 65, 85, 97, 114, 126

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html`
at line 15, The Tailwind important modifier is using the old prefix form
(!text-violet-400); update all occurrences to the v4 postfix form
(text-violet-400!) to future-proof styling. Search the template for class
strings that contain "!text-violet-400" (examples include the routerLinkActive
attribute on the settings layout links) and replace each "!text-violet-400" with
"text-violet-400!" across the nine occurrences so the routerLinkActive and other
link classes use the new postfix syntax.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/features/settings/application/preferences.service.ts`:
- Around line 12-14: The shared signals loading, saving, and error cause race
conditions across concurrent operations; change to per-operation private signals
(e.g., loadingFeature, loadingWidget, savingFeature, savingWidget, errorFeature,
errorWidget) and update loadFeatureConfig, loadWidgetConfig,
updateFeatureConfig, updateWidgetConfig to set only their corresponding private
signals and errors; then derive the existing public signals via computed
aggregates (loading = computed(() => loadingFeature() || loadingWidget()),
saving similarly, and error = computed(() => errorFeature() ?? errorWidget() ??
null) or another merge strategy) so the public API remains unchanged while
preventing cross-operation races.

In `@apps/web/src/app/features/settings/application/session.service.ts`:
- Around line 26-46: The issue: prior error messages remain visible after a
later successful revoke because revokeSession and revokeAllOtherSessions never
clear this.error before starting the optimistic update and async call; fix by
clearing the error at the start of each method (e.g., call this.error.set(null)
or clear value) before updating this.sessions and awaiting repository methods in
revokeSession and revokeAllOtherSessions, keeping the existing catch behavior
that restores prev and sets the error on failure.

In
`@apps/web/src/app/features/settings/presentation/account/account.component.ts`:
- Line 55: The template uses key.replace('-', ' ') which only replaces the first
hyphen (e.g., "resource-library-pro" → "resource library-pro"); update the usage
where the template binds {{ key.replace('-', ' ') }} to replace all hyphens
instead (for example use key.replaceAll('-', ' ') or key.split('-').join(' '))
so FeatureKey values render all hyphens as spaces; locate the interpolation
referencing key in account.component (the binding that calls replace) and swap
to a global replacement approach.

In
`@apps/web/src/app/features/settings/presentation/modules/modules.component.html`:
- Around line 46-59: The switch button with role="switch" (the element using
(click)="toggle(module)" and [class]="isOn(module.key) ? ..." ) has no
accessible name; link it to the module label by providing an accessible name
(either add aria-label using module.label or add aria-labelledby pointing to the
DOM element that renders module.label). Ensure the label element that displays
module.label has a unique id (e.g., based on module.key) and then set the
button's aria-labelledby to that id (or set aria-label="{{module.label}}") so
screen readers announce the switch; keep the existing disabled and state
bindings (saving(), isOn, module.locked) unchanged.
- Line 50: Replace the Tailwind focus utility on the toggle element that
currently contains the class string "relative inline-flex h-5 w-9 flex-shrink-0
rounded-full border-2 border-transparent transition-colors focus:outline-none
disabled:cursor-not-allowed" by changing focus:outline-none to
focus:outline-hidden; update the class attribute where this string appears in
modules.component.html (the toggle element in ModulesComponent template) so it
uses focus:outline-hidden instead of focus:outline-none.

In
`@apps/web/src/app/features/settings/presentation/sessions/sessions.component.html`:
- Around line 67-73: The "Revoke" buttons in sessions.component HTML are
announced generically; update the button element that calls revoke(session) to
include an aria-label that uniquely describes the session (e.g., use session.id,
session.device, or session.createdAt) so screen readers hear "Revoke session for
[identifier]". Locate the button with (click)="revoke(session)" and add an
aria-label binding like [attr.aria-label]="`Revoke session for ${session.device
|| session.id || session.createdAt}`" so each button is distinguishable.

In
`@apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts`:
- Around line 31-39: In getBrowser, the Chrome check (/Chrome\// && !/Chromium/)
runs before the Opera check (/OPR\/|Opera\//), causing modern Opera UAs (which
contain "Chrome/") to be misidentified; fix by moving the Opera regex check so
it executes before the Chrome check (i.e., evaluate /OPR\/|Opera\// prior to
/Chrome\// && !/Chromium/) so Opera is detected correctly.

In
`@apps/web/src/app/features/settings/presentation/widgets/widgets.component.html`:
- Around line 39-52: The switch button in widgets.component.html (and the
identical toggle in modules.component.html) lacks an accessible name and a
visible focus indicator: update the button element (the one calling
toggle(widget) and using isOn(widget.key) and saving()) to include an accessible
label via [attr.aria-label]="'Toggle ' + widget.label" (so screen readers get a
name) and replace the inaccessible focus utility (focus:outline-none) with an
accessible combination such as focus:outline-hidden plus a visible focus-visible
style (e.g., focus-visible:ring-2 and a contrasting ring color and offset) so
keyboard and high-contrast users see focus. Ensure these changes are applied to
both the outer <button> and any identical toggle in modules.component.html.

---

Nitpick comments:
In
`@apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html`:
- Line 15: The Tailwind important modifier is using the old prefix form
(!text-violet-400); update all occurrences to the v4 postfix form
(text-violet-400!) to future-proof styling. Search the template for class
strings that contain "!text-violet-400" (examples include the routerLinkActive
attribute on the settings layout links) and replace each "!text-violet-400" with
"text-violet-400!" across the nine occurrences so the routerLinkActive and other
link classes use the new postfix syntax.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33c4dbbc-7fc5-48f6-89b3-88a07de1577e

📥 Commits

Reviewing files that changed from the base of the PR and between 9c0fced and f95afee.

📒 Files selected for processing (24)
  • apps/web/src/app/features/learning-resource/presentation/learning-resource.routes.ts
  • apps/web/src/app/features/settings/application/preferences.service.ts
  • apps/web/src/app/features/settings/application/session.service.ts
  • apps/web/src/app/features/settings/domain/preferences.repository.ts
  • apps/web/src/app/features/settings/domain/session.repository.ts
  • apps/web/src/app/features/settings/domain/settings.model.ts
  • apps/web/src/app/features/settings/infrastructure/preferences-http.repository.ts
  • apps/web/src/app/features/settings/infrastructure/session-http.repository.ts
  • apps/web/src/app/features/settings/infrastructure/settings.dto.ts
  • apps/web/src/app/features/settings/presentation/account/account.component.ts
  • apps/web/src/app/features/settings/presentation/danger-zone/danger-zone.component.ts
  • apps/web/src/app/features/settings/presentation/import-export/import-export.component.ts
  • apps/web/src/app/features/settings/presentation/modules/modules.component.html
  • apps/web/src/app/features/settings/presentation/modules/modules.component.ts
  • apps/web/src/app/features/settings/presentation/notifications/notifications.component.ts
  • apps/web/src/app/features/settings/presentation/preferences/preferences.component.ts
  • apps/web/src/app/features/settings/presentation/security/security.component.ts
  • apps/web/src/app/features/settings/presentation/sessions/sessions.component.html
  • apps/web/src/app/features/settings/presentation/sessions/sessions.component.ts
  • apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.html
  • apps/web/src/app/features/settings/presentation/settings-layout/settings-layout.component.ts
  • apps/web/src/app/features/settings/presentation/settings.routes.ts
  • apps/web/src/app/features/settings/presentation/widgets/widgets.component.html
  • apps/web/src/app/features/settings/presentation/widgets/widgets.component.ts

Comment thread apps/web/src/app/features/settings/application/preferences.service.ts Outdated
Comment thread apps/web/src/app/features/settings/application/session.service.ts
Comment thread apps/web/src/app/features/settings/presentation/account/account.component.ts Outdated
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

✅ All CI checks passed! The code is ready for review.

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

✅ All CI checks passed! The code is ready for review.

@ThiagoDelgado-D ThiagoDelgado-D merged commit 417cf7f into main May 6, 2026
10 checks passed
@ThiagoDelgado-D ThiagoDelgado-D deleted the feat/v0.8.3-settings-ui branch May 6, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant