# Órbita

> Historical Orbita setup/design record. Its old Vercel/Turso/mesh instructions are not current Orb Web3 deployment instructions.

Portuguese-language app with invite-only private servers, text and voice rooms, persistent messages, and WebRTC calls of up to 12 participants. There is an Electron app for Windows x64 and a PWA for Chrome/Edge.

## Windows app

- The **Download app** button serves the NSIS installer at `/downloads/Orbita-Setup-1.0.5-x64.exe`. Per-user installation, no administrator required. Version 1.0.5 is the first with an updater: from it on, the app checks the HTTPS manifest on open, verifies a fixed Ed25519 signature and the installer's SHA-256 before offering to restart to install. The publishing private key is kept outside the repository. The executable is not yet Authenticode-signed; Windows may show an unknown publisher on the first install.
- **Watch 2 lives** lets you select two streams and watch them together in the card area or in full screen. In narrow areas, they stack. The players stay mounted when switching layouts, preserving volume and playback. When one selected live ends, the other remains highlighted. The controls disappear after three seconds of inactivity, keeping the viewer counters.
- Live start and end have their own sounds, respecting **App sounds**. The first list on joining a call is a silent reference for lives that were already open; repeated updates do not repeat the sounds. Switching only the layout or the quality does not notify again.
- The Electron window uses the official domain with an isolated persistent session, sandbox, and context isolation. It does not provide Node, files, shell, or arbitrary IPC to web content. External links open in the browser. There is a single instance and confirmation when closing during a call.
- Hardware graphics enabled by default. Web and desktop use standard WebRTC negotiation, without imposing an H.264 preference on viewers. **App** and **Live quality** identify the local encoders as **Sending to [recipient]**, only for people receiving your stream. Watching uses a decoder and does not require choosing an encoder. Acceleration availability depends on the GPU, driver, resolution, and negotiated codec.
- The join response for server and private calls already includes the authenticated participants, allowing the room to be shown without a second query. Connections and signals from different participants are processed in parallel, preserving the order of each person's signals. During up to 8 seconds of negotiation, the interval between queries drops from 1,300 to 400 ms; it returns to normal as soon as it connects or that window ends. This reduces app waiting, but does not eliminate network latency or ICE/TURN negotiation.
- HTTP submission of SDP offers/answers happens without blocking the reception of the other participants' signals. Live subscriptions wait for that submission when they are already included in it, avoiding redundant requests. `max-bundle` shares the audio, video, and data transport of each pair, reducing ICE candidates and TURN allocations during negotiation. The topology remains mesh, with one connection per recipient.
- With the call connected, watch/stop watching uses an already-negotiated reliable WebRTC control channel, without waiting for the streamer's polling. A handshake confirms support on both sides; older clients and failures of that channel continue using the API. Late messages do not undo a more recent choice. This does not advance video/audio delivery to those who have not yet chosen to watch.
- Call polling groups heartbeat, participants, signals, and cleanup into one transaction, reducing that stretch from four database round trips to one; authentication, expiration, and permissions continue to be verified. SDP/ICE is processed before optional updates, and the SDP response already includes the live subscription. Heartbeats without changes do not redraw the interface. Stable microphone output starts being sent while the noise suppression model loads.
- When clicking a call from the chat, the conversation and its scroll container stay mounted during join and WebRTC negotiation. Automatic navigation occurs when there is connected media (or the room is empty); manually choosing another channel cancels that pending navigation.
- App startup uses a snapshot of servers, channels, voice, members, and roles in a single read-only transaction; friends, conversations, and private invites also use one read batch. Presence registration happens after the response, when it needs to be renewed (30 seconds), without blocking startup. The database client is shared between routes in the same process and limits to four simultaneous operations, instead of the SDK's twenty per client. Reads that receive specifically the connection-limit error make up to two additional attempts with a short wait; writes are never automatically retried. The limit is per process, not a global quota across Vercel instances.

`work/test-startup-load.mjs` verifies 72 startup/friends reads in three bursts of 24, simultaneous with heartbeats of four calls, on an isolated local database; it also injects connection refusal to validate the limited recovery of reads and the absence of write retries. Local tests do not fully reproduce the load or the limits of the production database.

Performance validation: `work/test-live-start-latency.mjs` compares old/new clients with real WebRTC video and 100 ms simulated per signaling request, including mixed pairs and control channel failure. In the local trial, the first decoded frame went from approximately 1,470 ms to 64 ms, with the call already established and the click right after a poll. It is a local measurement, not a guarantee for internet, GPU, or TURN. `work/test-call-performance.mjs` verifies 25 heartbeats without extra renders, immediate state changes, microphone sent during filter initialization, and the absence of the redundant request before the SDP response. The Electron tests also separately delay the join API and the media signaling to verify that the chat does not disappear.

`work/test-audio-join-latency.mjs` compares the seventh participant's join with synthetic audio played over WebRTC, 100 ms simulated per request plus another 2,500 ms in the signaling of one of the pairs. In the local trial, the other five audios were ready in up to 1,875 ms, versus 3,471 ms before, and candidate signals dropped from 72 to 24. The delayed pair remained slow: this fix isolates the wait, without removing network latency. `work/test-call-bundle-compatibility.mjs` verifies old and current clients in both negotiation directions.
- **Games and windows** lists capturable windows with preview and search. Prefer borderless window in games. There is no DLL injection or game hook for exclusive full screen. Isolated tabs from external browsers remain available through site capture in Chrome/Edge.
- Window audio uses a small custom helper (`desktop/native/audio-capture.cpp`), compiled with MSVC and WASAPI process-loopback. It captures only the window's process and its children in 48 kHz stereo PCM. Updated Windows 10 version 2004+ (build 19041+) or Windows 11: the helper checks API activation and initialization with `--probe`, without starting capture. Unsupported systems remain blocked. Browsers may share a process between tabs. It does not record audio and does not use the network: it streams over a pipe to a limited-queue AudioWorklet, integrated with the WebRTC live audio. Closing the window, stopping the live, or leaving ends the capture. **Full screens** can use general audio with `restrictOwnAudio` on Electron 44.2.0.
- On the site, isolated window audio depends on the browser picker. The optional **Use computer audio (compatibility)** mode requests `windowAudio: system`; it may include other programs and the call. It is never activated automatically. To isolate a game on compatible Windows 10, use the native client; for sites with video, prefer tab capture.
- During calls, timers stay active when the window loses focus and the app avoids automatic suspension. Outside calls it returns to Chromium's default power saving. This does not change the mesh topology or the TURN quota.
- To keep the servers, open **Download app → Generate transfer code** in the browser and **App → Use my browser profile** on the desktop. Random code, hash in the database, five-minute expiration, atomic and single-use consumption. The previous session remains valid.

To build: `npm ci --prefix desktop`, then `npm run desktop:build`. Requires Visual Studio C++ Build Tools and the Windows SDK. Output in `desktop/release/`. The package includes only the Electron client and the audio helper; credentials, database, tests, and server dependencies stay out. Before publishing, copy only the final installer to `public/downloads/` and generate the SHA-256. The `0010_desktop_sessions.sql` migration adds transfer and profile sessions.

Previous native capture tests confirmed per-process audio isolation and Electron→Edge window streaming. The playback review tests frames actually decoded in both directions between Edge and Electron, four successive subscriptions to the same live, and recovery of a paused player. A sender failure does not block the other participants' signals or updates. Call join keeps the chat and the member list while waiting for the first room state. The tests use synthetic sources; games, anti-cheats, and drivers on other computers need their own validation.

## Publishing on Vercel

1. Create the project with this folder. The framework is Next.js.
2. Connect a Turso database through Vercel's Storage panel. Configure `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` in the project variables.
3. Configure `TURN_URL`, `TURN_USERNAME`, and `TURN_PASSWORD` with an authorized TURN service. `TURN_URL` accepts a comma-separated list.
4. Publish. The build command declared in `vercel.json` applies the pending migrations and builds the app. Applied migrations are recorded in the database and are not repeated.
5. Open the HTTPS domain in Chrome or Edge. Create a server, copy the invite, and share it with another person.

Vercel hosts pages and APIs. Turso stores profiles, servers, members, rooms, messages, and temporary signaling. Media uses WebRTC connections between the participants; TURN relays when a direct connection is not possible. There is no audio or video recording in the app.

The published project uses Metered's free 20 GB/month plan. The credentials live in Vercel's production secret variables and are delivered only when joining an authorized call. There are UDP, TCP, and TLS alternatives on port 443. The quota is shared by the connections that need the relay; direct connections do not consume TURN. The panel reports an outage when the quota is exhausted, without overage charges. The free service has fewer regions and may present instability.

Media connections without progress try to renegotiate twice, at 15-second intervals, preserving microphone, camera, and screen. If they do not connect, the card offers **Try to reconnect**, without leaving the connecting indication indefinitely. Tests with two real Edge sessions confirmed audio and data over TURN UDP and TLS 443, plus voice and live in Órbita's own engine using mandatory relay. The media sources of these tests are synthetic; the friend's network and devices still need testing on their side.

## Mentions, unread messages, and search

- Type `@` in the chat to choose a person by name or `@everyone`. Use the arrows and Enter/Tab, or click a suggestion. Mentions store the profile ID and display the name used on that server. The recipient gets a highlight on the message and a counter on the channel and on the server.
- Channels with unread received messages are highlighted; the server also has a side indicator. Reading is saved in the database per profile and channel when the chat is visible, focused, and on recent messages. Reading old search results does not mark recent messages as read.
- New messages from other people have a short sound alert. History loading and your own messages do not trigger sound. The **App sounds** control also disables this alert. Tabs of the same profile coordinate the alert to avoid duplication, respecting the browser's playback restrictions.
- The search bar searches text and captions across the entire history of the current server, ignoring accents and with pagination. Type **@** and choose a member or role to filter the authors; **@dono (owner)** shows messages from the owner. Filters can be combined with text, such as **@dono reunião** (owner meeting). Names with spaces also work through the suggestions or in quotes. The role filter considers the members who currently hold the role. Click a result to open the context, continue loading earlier or later messages, or use **Back to recent messages**. Removed messages and channels without access are excluded from the results.
- Right-click the name or the photo of a profile in the messages or in the member list to open the profile and the available moderation actions. The message menu keeps **Reply** and **Remove message** and also offers moderation of the author. Temporary mute, microphone/listen lock, disconnection from the call, kick, and ban respect the server's permissions and hierarchy. Temporary mute, kick, and ban open a confirmation.
- The list on the right shows the members grouped by role, from the owner to the members, with colors and an indication of who is in a call. It can be closed and reopened by the members icon; on small screens it opens over the chat.
- The `0008_chat_activity.sql` migration adds the read records, mentions, and the FTS5 index. `test-chat-activity.mjs` verifies APIs and interface; `test-call-retry.mjs` covers the limited recovery; `test-call-relay-ui.mjs` validates real media over TURN with synthetic sources.

## Development

### Economical synchronization

- Current clients receive friends, private invites, and server state in the same authenticated query every 2.5 seconds. The two previous loops (2.5 and 3.5 seconds) were combined: approximately 42% fewer periodic queries in those two functions, before considering latency and manual actions. Call signaling polling keeps its intervals.
- `0017_efficient_sync.sql` adds transactional revisions: profile/server/member/role changes invalidate the stable data; message insertions, changes, and deletions invalidate only the corresponding history. Heartbeats and read marks do not invalidate the history. Member/role/server queries are protected by a revision comparison; unchanged lists and history are not fully re-read/re-sent. Cursors do not grant access: session, visible channels, and permissions continue to be verified on every request.
- The revisions live in the database and work across Vercel instances. The browser keeps the data only in memory, associated with the profile; old versions continue receiving complete responses. Room/anchor and identity changes load the appropriate context. There is no public cache of private data.
- Private chat uses the conversation revision to fetch new messages and old deletions, including when the last message ID remains the same. In the background it defers history until the window returns; alerts and invites stay synchronized. Concurrent manual updates share one fresh read after any previous read still in flight.
- `work/test-efficient-sync.mjs` validates revisions, deletions, permissions, compatibility, and concurrency. In a synthetic chat with 100 returned messages, an unchanged response dropped from 92,414 to 1,172 bytes. `work/test-efficient-sync-ui.mjs` confirmed five general queries and zero separate friends/private-history queries in 11 idle seconds, preservation of paginated history, and browser updates. These numbers do not represent a production CPU measurement nor guarantee remaining within the free tier.

Requires Node.js 22.13+ and npm.

```powershell
npm ci
$env:TURSO_DATABASE_URL='file:./work/local.db'
New-Item -ItemType Directory -Force work
npm run db:migrate
npm run dev
```

For the online database, configure the variables in `.env.local` and run the migrations with those same variables exported in the environment. Do not publish `.env.local` or tokens in a repository.

## Usage

- In **User settings → Noise suppression**, turn RNNoise on/off for the microphone. On by default; persistent local preference, shared between tabs. It uses a mono AudioWorklet at 48 kHz with WebAssembly SIMD when available. The switch keeps the same track sent over WebRTC, without recapturing the microphone or changing mute, listen, volumes, or lives. Echo cancellation remains on the original microphone; native suppression stays off while RNNoise processes, avoiding duplicate filters. When turned off, audio passes directly to the output; when leaving the call, the nodes, context, and capture are shut down.
- The model is served by the site itself, without sending voice to processing services. The code and licenses are in `public/audio/rnnoise-2022.2.0/`. `scripts/build-noise-suppression.mjs` generates the assets before dev/build; it adapts the published loader for the AudioWorklet environment and in-memory binary input, keeping the upstream binaries. Loading or processing failures restore microphone passthrough and try native suppression, with an indication in the settings. RNNoise is an open alternative, not the Krisp product nor a guarantee of equivalent quality.

- Choose your name when creating or joining a server.
- The owner and the administrators can create rooms and renew the invite. Renewing invalidates the previous code.
- Join a voice room; microphone and camera start off.
- The footer has buttons to mute your microphone and the call audio, also available while you chat in the text chat. Muting listen pauses your microphone and all audio received from the call, including lives; re-enabling restores the previous microphone state and keeps the chosen volumes. The moderation locks continue to apply.
- There are different sound alerts for join/leave, muting/unmuting the microphone, and silencing/re-enabling the speaker. In the **User settings** gear, turn off **App sounds** to disable all these alerts without muting people or lives. The preference is saved automatically in this browser and synchronized between your tabs.
- Each profile participates in a single call at a time. Joining in another tab or window replaces the previous connection, shutting down its capture and playback; the old tab shows a warning. The rule also applies when changing room or server and is enforced in the database even with simultaneous joins. If the new room is full or inaccessible, the other tab's connection is preserved.
- The side list shows the members below each voice channel, even before joining and while you chat in a text channel. The icons indicate a muted microphone, moderation locks, and an active live.
- In your call, the member who is speaking gets a green outline on the avatar in the side list and on the call card, with a voice icon. The activity follows only the microphone, including yours, and continues in the side list while you open the chat. Silence, mute, blocked listen, and leaving clear the indicator; personal playback volume does not change the detection.
- Right-click a member in the list or on the call card to adjust only that person's voice between 0% and 100%, with a default of 100%. The preference is personal, saved per profile in this browser, and does not change the audio of lives. **Mute for me** preserves the last non-zero volume; **Listen to this person** restores that value. The mute persists after reload and the volumes are synchronized between tabs. On mobile, tap the member; via keyboard, use Enter or Shift+F10.
- In the same menu, moderators, administrators, and the owner can mute the microphone, block listen, disconnect from the call, kick, or ban members of lower roles. Blocking listen also blocks the microphone and the audio received from lives. The locks are per server and persist when changing room or reconnecting; disconnecting from the call allows returning. Kick and ban ask for confirmation.
- Turn on webcam, microphone, and sharing separately.
- To stream with sound, select **Browser tab** and check **Share tab audio**. If no audio track comes through, the app stops the capture and explains how to fix it.
- Several people can share the screen at the same time in the same room. Each live appears on the person's card with the **Watch live** button; it is not played automatically. You can watch several at the same time and use **Leave** to stop one of them. Each card has an independent volume from 0% to 300%, default 100%, and full screen. The volume is saved per person and continues to apply when they restart the live or join another call session. The grid adapts to the width, height, and number of cards; in small windows there is scrolling. Someone sending webcam and screen at the same time appears in two separate cards. Leaving the live keeps the webcam and the voice.
- The viewer counter on each live opens the list of who is playing that stream. Joining only the call or viewing your own preview does not count. Leaving a live, opening the chat, or ending the call updates the list. In case of an abrupt close without notice, viewer presence expires in up to about 12 seconds, plus the update interval.
- The counter shows only the eye icon and the number. In full screen and in theater mode, the live bar and the cursor disappear after 3 seconds without movement, keeping the counter. Moving the mouse, touching, or using the keyboard brings the controls back; keyboard focus and dragging controls prevent them from disappearing during use. When returning to the normal grid, the controls remain visible.
- The **Theater mode** button on each player makes that live occupy the entire width and height of the card area, hiding the others. Use the same button or **Esc** to return to the grid and choose another live. The switch keeps the players, volumes, and viewers; full screen remains available. Leaving the stream, ending it, or opening another room/chat closes theater mode.
- When sharing a window or the monitor, **Send computer audio** allows streaming only the image. In a tab, the browser already sends only the tab's own audio when that option is checked in the browser picker.
- The **Live quality** selector, before sharing and in the call bar gear during the stream, offers **480p / 1,200 kbps** and **720p / 2,500 kbps** (default), always at 30 FPS. The bitrate is fixed per resolution; there is no manual adjustment nor game mode. Old 1080p values return to 720p. The value sets the encoder ceiling per recipient, not a constant rate: WebRTC may use less depending on content, source, and network. The switch updates the capture constraints and all screen video senders without changing webcam, microphone, or subscriptions.
- On compatible Chrome/Edge, Órbita's own tab is identified and the stream is stopped before re-sending the call audio and causing hiss.
- Use **Install app** or the Chrome/Edge install menu to open it in its own window.

## Limits of this version

- There is a web/PWA version and an Electron installer for Windows x64.
- Tab sharing with audio depends on the browser and requires Chrome/Edge on the computer. Use Window or Full screen mode for programs outside the browser or the full monitor. Tab mode with audio requires a tab and a sound track; the other modes allow streaming only the image, with an explicit indication.
- Live capture asks for echo cancellation, noise reduction, and automatic gain disabled, keeping echo cancellation on the microphone. The app requests `restrictOwnAudio` to exclude the audio of Órbita's own document when the capture contains system sound. This feature depends on the browser, system, and source, per the [Chrome documentation](https://developer.chrome.com/blog/chrome-141-beta?hl=en#support_restrictownaudio); it does not authorize arbitrarily excluding Discord or other apps. A specific tab continues to isolate that tab's sound. There is no live gain reduction triggered by voice activity in the app.
- Calls use a WebRTC mesh, limited to 12 people. It is not an infrastructure for hundreds of participants.
- Each participant can send one screen at a time; webcam and screen work together. Streams are bound to the voice room the person joined, like calls.
- Without TURN configured, calls may fail between restricted networks. Do not use the absence of a build error as proof of connectivity across networks.
- The profile is tied to an HttpOnly cookie valid for 30 days. There is no password, account recovery, or synchronization between browsers. Clearing the site data loses that profile's access.
- The chat initially loads up to 100 messages to open fast; scrolling up or using **Load earlier messages** allows querying the entire history, with no total limit of displayed messages. New pages preserve the reading position. The periodic update keeps the already-loaded messages and reflects old deletions.
- Signaling uses periodic HTTP queries; there is request consumption on Vercel and in the database.
- Presence and moderation are updated by the periodic queries, in a few seconds. In the WebRTC mesh, the API authorizes and stores the restrictions and the official clients apply the lock on the sent and received tracks. There is no intermediate media server to enforce these restrictions on tampered clients.
- The service worker does not store conversations or private responses for offline use.

## Validation

`node test-api.mjs` only accepts a local address. It creates test profiles and verifies persistence, invites, per-server permissions, messages, signaling, the concurrent participant limit, and leaving. For another port, configure `ORBITA_TEST_URL`.

Media engine unit tests use simulated RTC objects. Real webcam, audio, and video streaming between two devices and different networks still requires validation with real devices and permissions.

`node test-voice-members.mjs` validates presence in all channels, cross-server privacy, hierarchy, voice/listen locks, reconnection, disconnection, kick, and ban. Use `ORBITA_TEST_URL` to point at the local server with the migrations applied.

`node test-single-call.mjs` covers session replacement, protection against old calls, room capacity, and concurrent joins. `node test-single-call-ui.mjs` verifies switching between tabs of the same profile in a real browser, shutdown of microphone/camera and RTC connections, plus the polling fallback when BroadcastChannel is unavailable. It uses the same Playwright configuration as the interface test below.

`node test-voice-ui.mjs` uses Playwright with Edge in isolated sessions and a synthetic microphone: it tests menus, native volume, real WebRTC packet reception, mute/deafen and recovery, persistence, keyboard, and mobile layout. Install Playwright in the test environment or define `ORBITA_PLAYWRIGHT_PATH` with the path to your `index.mjs`. This test does not validate physical microphones or distinct networks.

`node test-live-viewers.mjs` tests simultaneous viewers, cross-room privacy, leaving, session switching, and cleanup by moderation. `node test-call-experience-ui.mjs` tests three real Edge sessions with synthetic sources: voluntary watching of lives on the cards, two simultaneous WebRTC playbacks, viewer list, volumes, full screen, footer controls, microphone restoration, sounds, and persistent preference. It also verifies theater mode, highlight switching, Esc, preservation of the same players/audio sources and separate webcam, ending the live, and layouts from 320 to 2560 pixels, including mobile in landscape.

`node test-profiles-files.mjs` covers photo/bio, atomic validation, cross-server privacy, real GIF animation, binary files up to 3 MB, limits, and moderation. `node test-chat-profiles-ui.mjs` verifies photo cropping with grid/zoom/drag/keyboard/touch, saved crop pixel, cancellation and re-editing, plus profiles, emoji at the cursor position, GIFs, full download, mobile layout, sounds, and webcam/screen in separate cards. `node test-voice-activity-ui.mjs` uses controlled microphone signals in two real Edge sessions over WebRTC to verify indicators in the list and on the cards, silence, mute, moderation, independent volume, separate webcam, and isolation of the live audio. The interface tests use synthetic sources.

The optional WebMCP support opens the same server creation form. Registration depends on the browser and was not validated in a compatible WebMCP context.

## Images, members, and administration

- The **Emoji** picker allows searching and inserting emoji at the cursor position. They are sent as normal text.
- Use the paperclip to send files, the **GIF** button to choose an animation from the computer, or paste a file with Ctrl+V. Up to 10 MB per upload, with an optional caption. Uploads above 3 MB are automatically split into 1 MB parts, with progress and part retry, to respect Vercel's request limit. There is no dependency on a catalog or external GIF search.
- PNG/JPG/WebP/GIF are decoded and normalized to WebP, without metadata. Static images stay at up to 1600 px and 750 KB; animations preserve frames, duration, and repeat, up to 720 px, 240 frames, 60 million input pixels, and 3 MB of output. Files of other formats keep their original bytes and are served as a download, without execution on the site.
- The attachments live in the existing Turso. Each server has a total limit of 100 MB and a 2-second interval between uploads. Removing a message revokes access and frees the space of its attachment. Old image URLs remain compatible. Reading requires membership in the server; there is no public cache.
- In **User settings**, edit the name, a bio of up to 500 characters, and the profile photo. Photos, profile GIFs, banners, and server icons accept files up to 10 MB. When changing or adjusting the photo, use the grid, drag, and zoom from 100% to 400% to choose the crop; the arrows also reposition the image. The circular preview shows the framing. **Apply crop** prepares the photo and **Save profile** publishes the change. The crop is normalized to 384×384 WebP: static photos up to 200 KB; GIFs and animated WebPs keep their frames, duration, and repeat, up to 1 MB, 240 frames, and 60 million input pixels. The crop preview also animates. Previous photos are replaced or removed. Photo and name appear in the chat, in calls, and in the members; clicking the name/photo in the chat or members, or choosing **View profile** in the call menu, opens the bio. Only your own profile can be edited; reading requires a server in common.
- The Members and roles option opens the Members, Roles, and Administration tabs. The owner can create Administrator, Moderator, and Member roles, with custom names and colors. Administrators manage only lower roles.
- Moderators can remove messages from people with a lower role, kick, ban, unban, and apply a mute of up to 7 days. The mute blocks text, images, and calls. Kick and ban remove the profile from open calls.
- Commands: /ajuda (/help), /expulsar (/kick) @ID motivo (reason), /banir (/ban) @ID motivo (reason), /desbanir (/unban) @ID, /silenciar (/mute) @ID minutos (minutes), /dessilenciar (/unmute) @ID, /apagar (/delete) ID-mensagem (message ID) and /cargo (/role) @ID ID-cargo (role ID) (ou membro (or member)). The IDs can be copied in the Members/Roles tab. Commands are not published as messages.
- Administration shows the banned and the history of the actions for moderators and above. Bans are per profile, not per person/device; the current version continues using cookie-based profiles.
- test-community.mjs tests these permissions and real image processing in a local environment; test-display.mjs covers the capture options and the behavior with and without audio, without simulating real hardware validation.

## Profiles and attachments in this update

- The global profile and the per-server profiles accept a banner (1200×400, 3:1 framing), with a preview before saving. Photos, banners, and icons accept inputs up to 10 MB.
- The crop sends originals above 3 MB in authenticated 1 MB parts, respecting Vercel's request limit. The temporary uploads expire in 15 minutes and are removed on finish/cancel; animations remain subject to the frame and pixel limits. The optimized result goes to the profile form.
- Capture offers **Compatibility mode without audio** for Windows/browser failures, with a new source selection. Per-app audio failures preserve the video and show a warning; quality limits are applied after opening the source. The call no longer shows the TURN/direct route indicators.
- In **User settings → Edit profile**, choose the global profile or a server. Local name, bio, photo, and banner do not change the other servers. **Use global profile on this server** removes the customizations. The local profile appears in the chat, members, the call, and the public card; fields without customization use the global one.
- The owner and administrators can change or remove the server image in **Members and roles → Administration**. The icon uses the same grid crop and supports animation. The API checks the role.
- The attachment parts live in the existing Turso; large downloads use a streamed response. Each part and the final publication require a session, membership, and permission to speak. Finalizing again is idempotent. Up to two pending uploads per profile, 30-minute expiration, and a reservation against the server's 100 MB quota. Deleting the message also frees the parts.
- `test-release-api.mjs` verifies 10 MB byte by byte, limits, incomplete/undue parts, idempotent finalization, cropped animated GIF, banners, per-server profiles, reset, and icon permissions. `test-release-ui.mjs` confirms visible animation and a 10 MB download through the browser. `test-audio-preferences-ui.mjs` covers volume migration, mute/restoration after reload, new sessions, cross-tab synchronization, and the bitrate ceiling on the real WebRTC sender. The media sources are synthetic; they do not replace testing system sound capture on physical devices.
