# TASK-014: Browse organizations and filter repositories by organization Status: implemented; all required checks pass; 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 the organizations their account belongs to and filter the repository list by one organization while still being able to return to "all repositories." This closes MVP outcome #2 in `docs/product.md` ("View the signed-in user and organizations"). ## Scope - Included: - Read-only `GET /user/orgs` and `GET /orgs/{org}/repos`, added to `GiteaRepositoryBrowserApi` as a third small interface, reusing its existing repository-mapping code. - An organization filter row on the repository list (ALL mode only — favorites are not org-scoped in Gitea), defaulting to "전체" (all), with the existing loading/empty/offline/auth-expired/server-error states applying to the filtered list the same way they already do for the unfiltered one. - Excluded: - Any organization administration or membership write (create/join/leave/manage team). - Filtering the Favorites tab by organization — starred repos are not org-scoped in Gitea's own model. ## Writable paths - `app/src/main/java/com/ubisam/ubgitea/domain/organizations/` - `app/src/main/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApi.kt` (additive: new interface implementation only) - `app/src/test/java/com/ubisam/ubgitea/data/repositories/GiteaRepositoryBrowserApiTest.kt` (additive) - `app/src/main/java/com/ubisam/ubgitea/feature/repositories/` - `app/src/test/java/com/ubisam/ubgitea/feature/repositories/` - `app/src/main/res/values/strings_repositories.xml` - `tasks/014-organizations.md` ## Acceptance criteria - [x] Organizations list and organization-scoped repository list are both paginated read-only calls; no membership/team-write endpoint is reachable — `GiteaRepositoryBrowserApi` only adds `get()` calls for these two new methods (grep-confirmed, same as every other read-only endpoint in that class). - [x] The repository list can be filtered to one organization and back to "all repositories" without losing the recent-push sort or paging behavior already built (TASK-017) — `listOrganizationRepositories` requests the same `sort=updated&order=desc`, uses the same `REPOSITORY_PAGE_SIZE`/page-based navigation, and reuses `mapRepository` so field mapping cannot drift between the two code paths. - [x] Loading, empty, offline, authentication-expired, and server-error states apply to the organization-filtered list the same way they already do for the unfiltered list — the org-scoped call goes through the exact same `loadRepositoryPage` try/catch and `RepositoryListUiState` cases as the unfiltered call; there is no separate, parallel state machine for it. - [x] Tests cover organization list mapping, organization-repository mapping (reusing existing repository mapping, not duplicating it), and filter switching — 2 data-layer tests plus 3 ViewModel tests (see Evidence). ## Required checks ```text ./gradlew assembleDebug ./gradlew testDebugUnitTest ./gradlew lintDebug ``` ## Evidence Implemented on branch `claude/task-014-organizations` from commit `7756ec4`. - Domain: `domain/organizations/OrganizationModels.kt` (`OrganizationSummary(name, displayName)`), `domain/organizations/OrganizationsRepository.kt`. Data: `GiteaRepositoryBrowserApi` gained a third interface implementation (`listOrganizations`, `listOrganizationRepositories`), reusing its existing `mapRepository` for the latter rather than duplicating repository-JSON mapping a third time. - Verified against Gitea's actual `Organization` Go struct (`name`/`full_name` fields) before implementing, not guessed. - `RepositoryListViewModel`: `Content`/`Empty` gained `organization: String?` (null = all) and `Content` gained `organizations: List` for the filter row. The organizations list is fetched once per session (`loadOrganizationsOnce`, cached in `cachedOrganizations`, cleared on logout) rather than on every page load — a failure to load it is treated as non-fatal (empty filter row) and does not block showing the repository list itself, since the list is the primary content and the filter is secondary. `selectOrganization(String?)` only applies in ALL mode (Gitea star is not organization-scoped, so Favorites ignores it, tested explicitly). - UI: `RepositoryListScreen` gained a horizontally-scrollable `FilterChip` row (전체 + one chip per organization) shown only in ALL mode when at least one organization exists, using each organization's `displayName` when present and falling back to `name`. Test coverage: 2 new data-layer tests (`organizations map name and full name`, `organization repositories reuse repository mapping and request recent-push order`) plus 3 new ViewModel tests (`selecting an organization filters the list and organizations are exposed for the picker`, `the organizations list is only fetched once and reused across page loads`, `organization filtering does not apply to the favorites tab`). Checks run from the new worktree (`local.properties` created locally, not committed): ```text ./gradlew testDebugUnitTest --tests "com.ubisam.ubgitea.feature.repositories.RepositoryListViewModelTest" → BUILD SUCCESSFUL ./gradlew assembleDebug testDebugUnitTest lintDebug --rerun-tasks → BUILD SUCCESSFUL - 169 unit tests across 28 suites, 0 failures/errors - 0 lint errors git diff --check → clean (CRLF notice only) sensitive-pattern scan of changed files → no matches ``` Known limitations: - No physical device was available to verify the filter chip row or an account with real multiple-organization membership interactively. - Organization membership itself (which orgs the account belongs to) is read once per session and not refreshed mid-session if the user's org membership changes on the server; a manual logout/login or app restart picks up changes. This matches the app's existing no-background-refresh posture elsewhere. No open P0-P3 findings from self-review. Requesting Codex read-only review.