91 lines
7.5 KiB
Markdown
91 lines
7.5 KiB
Markdown
# Server Discovery
|
|
|
|
> **Area:** server-directory
|
|
> **Status:** Active
|
|
> **Last updated:** 2026-07-05
|
|
|
|
## Overview
|
|
|
|
Server discovery lets a signed-in user find public servers to join without knowing an exact name. It spans the signaling **server** (REST routes + CQRS query handlers that rank public servers) and the product **client** (`server-directory` domain API/facade plus the `/dashboard` landing and `/servers` browse page). It complements the existing free-text `GET /api/servers` search with two curated lists — **featured** and **trending**.
|
|
|
|
## Responsibilities
|
|
|
|
- Server: rank and return public servers as **featured** (most-populated) and **trending** (most-recently-active) lists, capped per request.
|
|
- Client: fetch those lists through `ServerDirectoryFacade` and render them via the reusable `app-server-browser` component on `/servers` and `/dashboard`.
|
|
- It does NOT own: free-text search (`GET /api/servers`), join/access checks (`/api/servers/:id/join`), invites, or room signal-affinity. Discovery is read-only browsing; joining flows through existing paths.
|
|
|
|
## Key concepts
|
|
|
|
- **Featured**: public servers ranked by membership count descending, ties broken by most recent `lastSeen` (`rankFeaturedServers`).
|
|
- **Trending**: public servers ranked by most recent `lastSeen` descending, ties broken by membership count (`rankTrendingServers`).
|
|
- **Discovery limit**: each route clamps `limit` to `[1, 50]` (`parseDiscoveryLimit`), default `12`.
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
Both endpoints live in `server/src/routes/servers.ts` and **must be registered before** the parameterised `/:id` route, otherwise Express resolves `featured`/`trending` as a server id.
|
|
|
|
### `GET /api/servers/featured`
|
|
|
|
- **Method**: GET
|
|
- **Authentication**: None (public discovery)
|
|
- **Rate Limiting**: No
|
|
- **Query params**: `limit` (optional integer; clamped to `[1, 50]`, default `12`)
|
|
|
|
### `GET /api/servers/trending`
|
|
|
|
- **Method**: GET
|
|
- **Authentication**: None (public discovery)
|
|
- **Rate Limiting**: No
|
|
- **Query params**: `limit` (optional integer; clamped to `[1, 50]`, default `12`)
|
|
|
|
### Response Schema (both)
|
|
|
|
```json
|
|
{
|
|
"servers": "ServerInfo[] — enriched public servers (icon, channels, sourceId/sourceName/sourceUrl filled by the client API layer)",
|
|
"total": "number — count of servers returned",
|
|
"limit": "number — the effective clamped limit"
|
|
}
|
|
```
|
|
|
|
`ServerInfo` matches the shape returned by `GET /api/servers` search results, so the client normalises and renders all three lists identically.
|
|
|
|
### Error Responses
|
|
|
|
- **500 Internal Server Error**: query handler / persistence failure.
|
|
|
|
---
|
|
|
|
## Server internals
|
|
|
|
- Routes delegate to CQRS query handlers `handleGetFeaturedServers` / `handleGetTrendingServers` (`server/src/cqrs/queries/handlers/`), dispatched via `GetFeaturedServers` / `GetTrendingServers` query types.
|
|
- Ranking lives in `server/src/cqrs/queries/handlers/server-ranking.util.ts` (`rankFeaturedServers`, `rankTrendingServers`, `loadMembershipCounts`). Membership counts load in a single grouped query.
|
|
- Results pass through the same `enrichServer()` step as search before serialisation.
|
|
|
|
## Client internals
|
|
|
|
- `ServerDirectoryApiService.getFeaturedServers()` / `getTrendingServers()` call the routes through a shared private `getDiscoveryServers(path)` helper and normalise into `ServerInfo[]`.
|
|
- **Multi-endpoint fan-out:** discovery queries **every online endpoint** (`getSearchableEndpoints()` + `forkJoin`), deduplicated by server ID — mirroring free-text search. Querying only the active endpoint made the default `/servers` view appear empty when populated servers lived on other endpoints.
|
|
- **Legacy 404 fallback:** when `GET /api/servers/featured` or `/trending` returns **404** (older signal servers resolve those paths as `/servers/:id`), `fetchDiscoveryFromEndpoint` falls back per-endpoint to the public `GET /api/servers` listing (`fetchPublicServerListForDiscovery`) instead of returning `[]`. Verified in `server-directory-api.service.spec.ts` (including production hosts like `signal.toju.app`).
|
|
- `ServerDirectoryService` → `ServerDirectoryFacade` expose `getFeaturedServers()` / `getTrendingServers()` as the domain boundary.
|
|
- `FindServersComponent` (`/servers`) composes **Recently active** (the user's saved rooms, capped at 6), **Featured**, and **Trending** sections, all rendered through `app-server-browser` with `[showMyServers]="true"`.
|
|
- `DashboardComponent` (`/dashboard`) is a single-column landing page (max-width centered, no in-page sidebars): a header greeting (no emoji), a global search with `Ctrl+K` focus and localStorage-backed **Recent Searches** chips shown beneath it, three primary action cards (Find People → `/people`, Find Servers → `/servers`, Create Server → `/create-server` — one link each), and discovery panels **People you might know**, **Popular Servers**, **Your Friends**, and **Recently Active Servers**. Each list is capped at 5 (`DISCOVERY_LIMIT`). It loads `popularServers` on init from `getFeaturedServers(5)`, falling back to `getTrendingServers(5)` when featured is empty; reuses `app-friend-button` for Add and `app-user-avatar` for people rows. `peopleYouMightKnow` excludes existing friends (via `FriendService.friendIds()`); `friends` lists discovered people who are friends. "See all" header links route to the matching `/people` or `/servers` page (no duplicated footer links). Recent searches are recorded on Enter (deduped, most-recent-first, capped at 8) and persisted under `metoyou_dashboard_recent_searches`.
|
|
- The servers-rail top button (`servers-rail.component`) is the **Dashboard** button (`lucideLayoutDashboard`, `title="Dashboard"`); its `goToDashboard()` handler deselects any active voice server and navigates to `/dashboard`. A **Create a server** button (`lucidePlus`, `data-testid="server-rail-create"`) sits below the saved-server icons and opens `app-create-server-dialog` (a Toju modal on desktop / bottom sheet on mobile) which dispatches `RoomsActions.createRoom` directly; the dashboard / `/create-server` route remains as an alternative entry point. Rail icons (`h-12 w-12`, `md:h-11 w-11`) animate their corner radius on hover and `:active` for a Discord-style squircle effect.
|
|
- On mobile (`ViewportService.isMobile()`), discovery routes (`/dashboard`, `/people`, `/servers`) render their page body full-width via `<ng-template #pageContent>` + `[ngTemplateOutlet]`. The **servers rail is global** in `app.html` (`shouldShowMobileAppServersRail` in `core/platform/mobile-shell-layout.rules.ts`) — discovery pages must **not** embed a second `<app-servers-rail>` or Swiper stack. Chat-room and DM-workspace routes keep their own embedded rail inside Swiper and hide the global shell rail (see `toju-app/AGENTS.md`).
|
|
|
|
## Related
|
|
|
|
- Product-client domain README: `toju-app/src/app/domains/server-directory/README.md`
|
|
- Full server-directory REST contract (CRUD, join, moderation): [server-directory.md](server-directory.md)
|
|
- People discovery (`/people`): `toju-app/src/app/domains/direct-message/README.md`
|
|
- Mobile shell: [mobile-capacitor.md](mobile-capacitor.md)
|
|
|
|
## Changelog
|
|
|
|
| Date | Change |
|
|
| ---------- | -------------------------------------------------------------------------------------------------------------- |
|
|
| 2026-07-05 | Added multi-endpoint fan-out and 404 fallback; corrected mobile shell layout (global rail, no per-page Swiper) |
|
|
| 2025-02-14 | Initial documentation |
|