# TASK-015: Read-only notifications Status: implemented; no review pending (Codex retired, 2026-09-29) Owner: Claude (human product owner directed Claude to own all subsequent tasks, 2026-08-26) Reviewer: Codex ## Outcome A signed-in user can see their Gitea notifications and open the related repository, issue, or pull request. Never marks a notification read, pinned, or unread — that is a write operation explicitly excluded from this task. ## Scope - Included: - Read-only `GET /notifications` (`GET /notifications/threads/{id}` was considered but the list endpoint already carries everything this UI needs, so no per-notification detail call was added). - A toggle between "안읽음" (unread only, the endpoint's own default) and "전체" (unread + read, requested via repeated `status-types` query values). - Tapping a notification whose subject resolves to a parseable issue/PR number navigates to that work item (TASK-008); one that resolves to a commit or repository, or whose target number cannot be parsed from its URL, does not offer navigation rather than guessing or crashing. - Non-color unread indication (a text label, not just a dot/color). - Excluded: - Marking a notification read, unread, or pinned — Gitea exposes this as a real, separate write endpoint; per `AGENTS.md` and the existing "no write beyond the one approved star/unstar endpoint" posture, this requires its own human-approved task, not implicit inclusion here. - Push notifications, background sync, or a badge/count outside the notifications screen itself. - Navigating to a commit-type or repository-type notification's target screen (only issue/PR navigation exists in this app so far). ## Writable paths - `app/src/main/java/com/ubisam/ubgitea/domain/notifications/` - `app/src/main/java/com/ubisam/ubgitea/data/notifications/` - `app/src/test/java/com/ubisam/ubgitea/domain/notifications/` - `app/src/test/java/com/ubisam/ubgitea/data/notifications/` - `app/src/main/java/com/ubisam/ubgitea/feature/notifications/` - `app/src/test/java/com/ubisam/ubgitea/feature/notifications/` - `app/src/main/res/values/strings_notifications.xml` - `app/src/main/java/com/ubisam/ubgitea/MainActivity.kt` (route + wiring only) - `app/src/main/java/com/ubisam/ubgitea/ui/navigation/` (additive: a `Notifications` route case) - `tasks/015-notifications.md` ## Acceptance criteria - [x] Notification list is paginated and read-only; no mark-read/unread/pinned endpoint is reachable from any new code. - [x] Unread state is shown with a text label, never color alone. - [x] A notification whose subject cannot be resolved to a navigable target (parse failure, commit/repository type) still renders without crashing and without offering a broken navigation action. - [x] Loading, empty, offline, authentication-expired, and server-error states are localized and accessible. - [x] Tests cover subject-type/state mapping, unread/all toggle request construction, target-number parsing (success and failure), and error states. - [x] Resource names use the `notifications_` prefix. ## Required checks ```text ./gradlew assembleDebug ./gradlew testDebugUnitTest ./gradlew lintDebug ``` ## Evidence - Branch `claude/task-015-notifications`, based on the TASK-014 commit `4fe3bf1`, in worktree `.claude/worktrees/task-015-notifications`. - Domain: `domain/notifications/NotificationModels.kt`, `NotificationsRepository.kt` (read-only, single `listNotifications` method). - Data: `data/notifications/GiteaNotificationsApi.kt` — `GET /api/v1/notifications`, paginated via `page`/`limit`; unread-only is the endpoint's own default, "전체" requests both by repeating `status-types=unread` and `status-types=read`. Subject type/state fall back to `OTHER`/`UNKNOWN` instead of failing closed on an unrecognized value (matches the "future Gitea types shouldn't crash the app" posture from TASK-008/009). Subject/issue/PR number is parsed from the trailing path segment of the subject URL and is `null`, not guessed, when unparseable (e.g. commit/repository subjects). - Known limitation, recorded here rather than silently assumed: Gitea's public API docs describe `status-types` only on the per-repo mark-read endpoint (`unread`/`read`/`pinned`); applying it to the notification-list endpoint to request both unread and read together was not independently verified against a live Gitea instance. If Codex's review or manual testing shows it behaves differently, the fallback is to fetch unread and read as two separate calls and merge. - Feature: `feature/notifications/NotificationsViewModel.kt`, `NotificationsScreen.kt`. Sealed `NotificationsUiState` (Idle/Loading/Content/Empty/Error), `startOperation`-style single-job cancellation, unread-state shown via a `[읽지 않음]` text label (never color alone). Tapping a notification whose subject is an issue or PR with a resolvable number navigates to that repository's Work Items list (TASK-008/013's `Route.WorkItems`) — a disclosed scope reduction: it opens the issue/PR **list** for that repository, not the specific item's detail view, since `Route.WorkItems` carries no item number and deep-linking to a specific issue/PR detail was not part of TASK-013's route shape. Commit/repository-type and unparseable-number notifications render as non-clickable rows instead of a broken or guessed navigation action. - Self-review fix (same class of bug as the TASK-007 history-retry issue): the first draft's `NotificationsUiState.Error` did not carry which filter (`읽지 않음`/`전체`) was active when the failure happened, so `retry()` after a failure while viewing "전체" would have silently reset to "읽지 않음". Fixed by adding `unreadOnly` to `Error`, covered by `NotificationsViewModelTest`'s "a failure while viewing all retries with all rather than resetting to unread-only" test. - Navigation: added `Route.Notifications` (`Route.kt`, `RouteStackSaver.kt`, `TYPE_NOTIFICATIONS`), a `notificationsViewModel` instance in `MainActivity.kt`, and an entry point ("알림" button) in `RepositoryListScreen.kt`'s top-level repository header, next to refresh/logout. - Strings: `app/src/main/res/values/strings_notifications.xml`, all prefixed `notifications_`. - Tests added: `GiteaNotificationsApiTest.kt` (8 cases — mapping, null-number fallback on unparseable subject URL, unknown type/state fallback, unreadOnly query-param construction, missing-token auth-expired, malformed-field invalid-response, 403 permission-denied, no-next-page-under-page-size) and `NotificationsViewModelTest.kt` (6 cases — default unread-only open, showAll + next/previous pagination, empty-first-page, listing-failure + retry-same-filter, empty-vs-content boundary, and the unreadOnly-preserving-retry regression test above). - Checks run from the worktree root: ``` ./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks ``` Result: BUILD SUCCESSFUL, all unit tests passing (188 total across the module), no lint findings beyond the pre-existing native-symbol-strip notice unrelated to this change. - `git diff --cached --check`: clean. Sensitive-pattern scan (`client_secret|password|api[_-]?key|secret|token=...` against the staged diff): no matches. - Not verified on a physical device or emulator in this session (no device attached to this environment) — recorded as a known limitation, same as prior UI-only tasks this session; the acceptance criteria above were verified through unit tests and Compose source review only. ## Review findings None recorded. No review is pending: Codex was retired from the project (2026-09-29).