Profile picture management in Settings: upload, replace, or remove your avatar (includes camera capture and re-cropping).
Avatars everywhere: user avatars now appear across the app (network, particles, reactions, members, top bars).
Faster, secure avatar loading: avatar previews use signed download URLs with caching/refresh for smoother display.
Edit dialog UI and preview: clickable avatar opens editor with preview, validation, and size limits.
Closes #70
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **New Features**
* Profile picture management in Settings: upload, replace, or remove your avatar (includes camera capture and re-cropping).
* Avatars everywhere: user avatars now appear across the app (network, particles, reactions, members, top bars).
* Faster, secure avatar loading: avatar previews use signed download URLs with caching/refresh for smoother display.
* Edit dialog UI and preview: clickable avatar opens editor with preview, validation, and size limits.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
No actionable comments were generated in the recent review. 🎉
ℹ️ Recent review info⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 73480fa0-39ce-4438-aeef-71baef5ae49d
📥 Commits
Reviewing files that changed from the base of the PR and between 6f8349c62f and 6ce7cc8651.
📒 Files selected for processing (3)
go/internal/handler/handler.go
go/migrations/000017_human_avatar.down.sql
js/desktop/src/features/particles/stream-card.tsx
💤 Files with no reviewable changes (1)
js/desktop/src/features/particles/stream-card.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
go/internal/handler/handler.go
go/migrations/000017_human_avatar.down.sql
📝 Walkthrough
Walkthrough
Implements user-controlled profile avatars: DB migration and model field, repository/service update/delete support, HTTP handlers and routes, desktop API client methods, avatar utilities/hooks/components, settings edit dialog with upload/camera, and avatar rendering across particle and network UIs.
Changes
Avatar Management Feature
Layer / File(s)
Summary
Database schema and model go/migrations/000017_human_avatar.up.sql, go/migrations/000017_human_avatar.down.sql, go/internal/human/models.go
Adds nullable avatar_object_id column and maps it into the Go Human model as AvatarObjectID.
Repository and service layer go/internal/human/repository.go, go/internal/human/service.go, go/internal/human/service_test.go
Repository selects/scans avatar_object_id, adds updateAvatarObjectID; service exposes UpdateAvatar/DeleteAvatar, validates parameters and maps repo errors; tests cover avatar lifecycle.
HTTP handlers and routing go/internal/handler/handler.go, go/cmd/orion/main.go
Adds UpdateAvatar and DeleteAvatar handlers (5MB request limit, depot uploads, best-effort depot cleanup), maps avatar field into DTO, renames DownloadParticleMedia→GetObjectDownloadUrl, and wires settings/particles routes.
API client refactor and avatar methods js/desktop/src/api/client.ts, js/desktop/src/api/types.ts
Avatar data resolution and utilities js/desktop/src/hooks/use-avatar-url.ts, js/desktop/src/lib/avatar-image.ts, js/desktop/src/lib/humans.ts, js/desktop/src/hooks/use-presence-positions.ts, js/desktop/src/stores/auth-store.ts
Adds useAvatarUrl (React Query) to fetch signed URLs, toAvatarBlob to center-crop and encode images, surfaces avatarObjectId in human display/presence shapes, and adds refreshUser to auth store.
Adds AvatarEditDialog with upload + camera capture UI, file validation (MIME and 30MB source cap), image conversion via toAvatarBlob, API calls for save/remove, preview lifecycle, and CameraCapture subcomponent.
Makes profile avatar clickable HumanAvatar with camera overlay and opens AvatarEditDialog for editing.
Particle and network views avatar integration js/desktop/src/features/particles/*, js/desktop/src/features/network-settings.tsx
Replaces direct initials avatars with HumanAvatar across particle list, cards, reaction bar, presence indicators, members overlay, top bar, and network settings; memoized avatar resolution now includes avatarObjectId.
Sequence Diagram(s)
sequenceDiagram
participant Desktop as Desktop Client
participant Api as ApiServer (handler)
participant Depot as DepotService
participant DB as Database
Desktop->>Api: PUT /humans/me/avatar (blob)
Api->>Depot: Upload avatar blob -> returns objectId
Depot-->>Api: objectId
Api->>Api: humanSvc.UpdateAvatar(userId, objectId)
Api->>DB: repository.updateAvatarObjectID(userId, objectId)
DB-->>Api: OK
Api-->>Desktop: 204 No Content
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
🐰 I hopped to town with camera bright,
A tiny blob compressed just right,
Snap or drop — a portrait new,
Replaced the initials with a view,
Now profiles glow with me and you.
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name
Status
Explanation
Resolution
Docstring Coverage
⚠️ Warning
Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%.
Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name
Status
Explanation
Description Check
✅ Passed
Check skipped - CodeRabbit’s high-level summary is enabled.
Title check
✅ Passed
The title 'feat: avatars for humans' clearly summarizes the main objective of adding avatar functionality for users.
Linked Issues check
✅ Passed
The pull request implements the core requirements from issue #70: user-controlled avatar creation via photo snapping or uploading, persistent user-controlled avatars to ensure recognition and consent, and avoids arbitrary video frame usage.
Out of Scope Changes check
✅ Passed
All changes are within scope of the linked issue. Backend adds avatar storage/retrieval infrastructure, frontend implements UI for avatar upload/capture, and media handling ensures avatars are square-cropped and properly formatted.
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches📝 Generate docstrings
Create stacked PR
Commit on current branch
🧪 Generate unit tests (beta)
Create PR with unit tests
Commit unit tests in branch worktree-refactored-strolling-treasure
Warning
There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
🔧 golangci-lint (2.12.2)
level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"
Comment @coderabbitai help to get the list of available commands and usage tips.
<!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- review_stack_entry_start -->
[](https://app.coderabbit.ai/change-stack/flowy-live/llink/pull/273?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)
<!-- review_stack_entry_end -->
No actionable comments were generated in the recent review. 🎉
<details>
<summary>ℹ️ Recent review info</summary>
<details>
<summary>⚙️ Run configuration</summary>
**Configuration used**: defaults
**Review profile**: CHILL
**Plan**: Pro Plus
**Run ID**: `73480fa0-39ce-4438-aeef-71baef5ae49d`
</details>
<details>
<summary>📥 Commits</summary>
Reviewing files that changed from the base of the PR and between 6f8349c62f5c33eea90050e4cf45d8740322f316 and 6ce7cc865174662c305e04498f5d412b1a13223c.
</details>
<details>
<summary>📒 Files selected for processing (3)</summary>
* `go/internal/handler/handler.go`
* `go/migrations/000017_human_avatar.down.sql`
* `js/desktop/src/features/particles/stream-card.tsx`
</details>
<details>
<summary>💤 Files with no reviewable changes (1)</summary>
* js/desktop/src/features/particles/stream-card.tsx
</details>
<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>
* go/internal/handler/handler.go
* go/migrations/000017_human_avatar.down.sql
</details>
</details>
---
<!-- walkthrough_start -->
<details>
<summary>📝 Walkthrough</summary>
## Walkthrough
Implements user-controlled profile avatars: DB migration and model field, repository/service update/delete support, HTTP handlers and routes, desktop API client methods, avatar utilities/hooks/components, settings edit dialog with upload/camera, and avatar rendering across particle and network UIs.
## Changes
**Avatar Management Feature**
|Layer / File(s)|Summary|
|---|---|
|**Database schema and model** <br> `go/migrations/000017_human_avatar.up.sql`, `go/migrations/000017_human_avatar.down.sql`, `go/internal/human/models.go`|Adds nullable `avatar_object_id` column and maps it into the Go `Human` model as `AvatarObjectID`.|
|**Repository and service layer** <br> `go/internal/human/repository.go`, `go/internal/human/service.go`, `go/internal/human/service_test.go`|Repository selects/scans `avatar_object_id`, adds `updateAvatarObjectID`; service exposes `UpdateAvatar`/`DeleteAvatar`, validates parameters and maps repo errors; tests cover avatar lifecycle.|
|**HTTP handlers and routing** <br> `go/internal/handler/handler.go`, `go/cmd/orion/main.go`|Adds `UpdateAvatar` and `DeleteAvatar` handlers (5MB request limit, depot uploads, best-effort depot cleanup), maps avatar field into DTO, renames `DownloadParticleMedia`→`GetObjectDownloadUrl`, and wires settings/particles routes.|
|**API client refactor and avatar methods** <br> `js/desktop/src/api/client.ts`, `js/desktop/src/api/types.ts`|Introduces centralized `send` helper, JSON fetch delegation, and client methods `updateAvatar`, `deleteAvatar`, `getAvatarDownloadUrl`; `HumanSchema` adds optional `avatar_object_id`.|
|**Avatar data resolution and utilities** <br> `js/desktop/src/hooks/use-avatar-url.ts`, `js/desktop/src/lib/avatar-image.ts`, `js/desktop/src/lib/humans.ts`, `js/desktop/src/hooks/use-presence-positions.ts`, `js/desktop/src/stores/auth-store.ts`|Adds `useAvatarUrl` (React Query) to fetch signed URLs, `toAvatarBlob` to center-crop and encode images, surfaces `avatarObjectId` in human display/presence shapes, and adds `refreshUser` to auth store.|
|**HumanAvatar component** <br> `js/desktop/src/components/human-avatar.tsx`|New component resolves signed avatar URLs via `useAvatarUrl` and renders `AvatarImage` or `AvatarFallback` (initials) as fallback.|
|**Avatar edit dialog** <br> `js/desktop/src/features/settings/avatar-edit-dialog.tsx`|Adds AvatarEditDialog with upload + camera capture UI, file validation (MIME and 30MB source cap), image conversion via `toAvatarBlob`, API calls for save/remove, preview lifecycle, and CameraCapture subcomponent.|
|**Settings page avatar UI** <br> `js/desktop/src/features/settings-page.tsx`|Makes profile avatar clickable `HumanAvatar` with camera overlay and opens AvatarEditDialog for editing.|
|**Particle and network views avatar integration** <br> `js/desktop/src/features/particles/*`, `js/desktop/src/features/network-settings.tsx`|Replaces direct initials avatars with `HumanAvatar` across particle list, cards, reaction bar, presence indicators, members overlay, top bar, and network settings; memoized avatar resolution now includes `avatarObjectId`.|
## Sequence Diagram(s)
```mermaid
sequenceDiagram
participant Desktop as Desktop Client
participant Api as ApiServer (handler)
participant Depot as DepotService
participant DB as Database
Desktop->>Api: PUT /humans/me/avatar (blob)
Api->>Depot: Upload avatar blob -> returns objectId
Depot-->>Api: objectId
Api->>Api: humanSvc.UpdateAvatar(userId, objectId)
Api->>DB: repository.updateAvatarObjectID(userId, objectId)
DB-->>Api: OK
Api-->>Desktop: 204 No Content
```
## Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
## Poem
> 🐰 I hopped to town with camera bright,
> > A tiny blob compressed just right,
> > Snap or drop — a portrait new,
> > Replaced the initials with a view,
> > Now profiles glow with me and you.
</details>
<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->
<details>
<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>
### ❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
<details>
<summary>✅ Passed checks (4 passed)</summary>
| Check name | Status | Explanation |
| :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title 'feat: avatars for humans' clearly summarizes the main objective of adding avatar functionality for users. |
| Linked Issues check | ✅ Passed | The pull request implements the core requirements from issue `#70`: user-controlled avatar creation via photo snapping or uploading, persistent user-controlled avatars to ensure recognition and consent, and avoids arbitrary video frame usage. |
| Out of Scope Changes check | ✅ Passed | All changes are within scope of the linked issue. Backend adds avatar storage/retrieval infrastructure, frontend implements UI for avatar upload/capture, and media handling ensures avatars are square-cropped and properly formatted. |
</details>
<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>
</details>
<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->
<details>
<summary>✨ Finishing Touches</summary>
<details>
<summary>📝 Generate docstrings</summary>
- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR
- [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch
</details>
<details>
<summary>🧪 Generate unit tests (beta)</summary>
- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `worktree-refactored-strolling-treasure`
</details>
</details>
<!-- finishing_touch_checkbox_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->
> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
>
> <details>
> <summary>🔧 golangci-lint (2.12.2)</summary>
>
> level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"
>
>
>
>
> </details>
<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->
<!-- tips_start -->
---
<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>
<!-- tips_end -->
<!-- internal state start -->
<!-- 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= -->
<!-- internal state end -->
coderabbitai[bot]
(Migrated from github.com)
reviewed 2026-06-11 22:11:54 +00:00
coderabbitai[bot]
(Migrated from github.com)
left a comment
Copy Link
Copy Source
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
Line 789 only verifies authentication, then Line 799 returns a signed URL for any provided object ID. This is an IDOR risk and can expose private media/avatars across users or networks.
🤖 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 `@go/internal/handler/handler.go` around lines 787 - 800, GetObjectDownloadUrl
currently only checks authentication via middleware.EmailFromContext but then
issues a signed URL from h.depotSvc.GetDownloadURL for any object id; to fix,
inside Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify
the caller has access (ownership or visibility) before calling
h.depotSvc.GetDownloadURL—use the authenticated identity returned by
middleware.EmailFromContext to authorize (or call your particle service /
Firebase as noted in the TODO) and return http.StatusForbidden when the caller
is not permitted; ensure this authorization check happens prior to requesting
the signed URL.
Extract shared avatar-derivation logic to avoid drift with stream-card.tsx.
This memoized DM/creator/fallback avatar resolution is duplicated with near-identical logic in js/desktop/src/features/particles/stream-card.tsx (Line 44-84). A shared helper (e.g., resolveStreamAvatarDisplay) would keep behavior consistent and reduce future regressions.
As per coding guidelines, "Write simpler, maintainable code in the Electron app instead of clever behavior."
🤖 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 `@js/desktop/src/features/particles/particle-list-view.tsx` around lines 153 -
193, The avatar derivation logic inside the avatar useMemo should be extracted
into a shared helper (e.g., export function resolveStreamAvatarDisplay) and both
particle-list-view.tsx and stream-card.tsx should call that helper instead of
duplicating logic; implement resolveStreamAvatarDisplay to accept the same
inputs used in the memo (particle, userId, network, latestChild, isDM), return
the same shape ({ initials, avatarObjectId }), export it from a common utilities
module, replace the useMemo body in particle-list-view (the avatar variable) and
the duplicated block in stream-card.tsx to call the helper (keeping memoization
callers as needed), and update imports accordingly so behavior and return shape
remain identical.
Source: Coding guidelines
🤖 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 `@go/internal/handler/handler.go`:
- Around line 345-365: The handler currently clears avatar_object_id before
capturing the prior object and thus leaks the old object; also on upload the
code uploads to h.depotSvc but if the subsequent DB update
(h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned. Fix by
first calling h.humanSvc.GetByID(r.Context(), humanId) to capture
oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change);
after the DB operation succeeds, call h.depotSvc.Delete(r.Context(),
oldAvatarObjectID) to remove the previous blob; for uploads, perform
h.depotSvc.Upload(...) then attempt the DB update (e.g.,
h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call
h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it
isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID,
h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and
h.depotSvc.Delete in this new order and add error logging around the rollback
delete.
- Around line 381-391: The handler wraps the request body with
http.MaxBytesReader but treats any upload-size error from
h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and
return http.StatusRequestEntityTooLarge instead. Update the error handling after
calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or
if that sentinel isn't available, match the "request body too large" error
string) respond with http.Error(w, "request entity too large",
http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and
500 behavior. Ensure you import the errors package if needed and reference the
r.Body wrapping and h.depotSvc.CreateFromReader call in your change.
In `@go/migrations/000017_human_avatar.down.sql`:
- Line 4: The rollback migration SQL in 000017_human_avatar.down.sql contains a
trailing comma after the DROP COLUMN list ("DROP COLUMN IF EXISTS
avatar_object_id,") which makes the statement invalid; remove the trailing comma
so the DROP COLUMN clause is a properly terminated SQL statement (ensure the
DROP COLUMN list ends without a comma and the statement ends with the
appropriate semicolon or end-of-statement token).
In `@js/desktop/src/api/client.ts`:
- Around line 160-163: The getAvatarDownloadUrl method builds the path using
objectId directly, which can break for reserved characters; update
getAvatarDownloadUrl to encode the id before interpolation (e.g., use
encodeURIComponent on the objectId when constructing `/humans/avatar/${...}`) so
the call to this.fetch('GET', `/humans/avatar/${objectId}`) always sends a valid
path; ensure any callers that pass objectId still provide the raw id and let
this method perform encoding.
In `@js/desktop/src/lib/avatar-image.ts`:
- Around line 11-12: Validate and normalize the incoming size parameter at the
start of the avatar generation function (the function with signature "{ size =
512, mirror = false }: { size?: number; mirror?: boolean } = {}, ):
Promise<Blob>"). Ensure size is a positive integer before using it to create the
canvas: check Number.isInteger(size) && size > 0 (or coerce via Math.floor and
then verify >0), and if invalid either throw a clear error or fall back to a
safe default (e.g., 512); then use that validated/normalized value for the
canvas creation to prevent runtime errors or broken avatars.
---
Outside diff comments:
In `@go/internal/handler/handler.go`:
- Around line 787-800: GetObjectDownloadUrl currently only checks authentication
via middleware.EmailFromContext but then issues a signed URL from
h.depotSvc.GetDownloadURL for any object id; to fix, inside
Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify the
caller has access (ownership or visibility) before calling
h.depotSvc.GetDownloadURL—use the authenticated identity returned by
middleware.EmailFromContext to authorize (or call your particle service /
Firebase as noted in the TODO) and return http.StatusForbidden when the caller
is not permitted; ensure this authorization check happens prior to requesting
the signed URL.
---
Nitpick comments:
In `@js/desktop/src/features/particles/particle-list-view.tsx`:
- Around line 153-193: The avatar derivation logic inside the avatar useMemo
should be extracted into a shared helper (e.g., export function
resolveStreamAvatarDisplay) and both particle-list-view.tsx and stream-card.tsx
should call that helper instead of duplicating logic; implement
resolveStreamAvatarDisplay to accept the same inputs used in the memo (particle,
userId, network, latestChild, isDM), return the same shape ({ initials,
avatarObjectId }), export it from a common utilities module, replace the useMemo
body in particle-list-view (the avatar variable) and the duplicated block in
stream-card.tsx to call the helper (keeping memoization callers as needed), and
update imports accordingly so behavior and return shape remain identical.
🪄 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 Plus
Run ID: f1b2fa73-7efe-4b67-abc5-45c5f2ee1d57
📥 Commits
Reviewing files that changed from the base of the PR and between ca3bbf204e and fa9d54aab5.
**Actionable comments posted: 5**
> [!CAUTION]
> Some comments are outside the diff and can’t be posted inline due to platform limitations.
>
>
>
> <details>
> <summary>⚠️ Outside diff range comments (1)</summary><blockquote>
>
> <details>
> <summary>go/internal/handler/handler.go (1)</summary><blockquote>
>
> `787-800`: _⚠️ Potential issue_ | _🔴 Critical_ | _🏗️ Heavy lift_
>
> **Missing authorization check allows arbitrary object download URL issuance.**
>
> Line 789 only verifies authentication, then Line 799 returns a signed URL for any provided object ID. This is an IDOR risk and can expose private media/avatars across users or networks.
>
> <details>
> <summary>🤖 Prompt for AI Agents</summary>
>
> ```
> Verify each finding against current code. Fix only still-valid issues, skip the
> rest with a brief reason, keep changes minimal, and validate.
>
> In `@go/internal/handler/handler.go` around lines 787 - 800, GetObjectDownloadUrl
> currently only checks authentication via middleware.EmailFromContext but then
> issues a signed URL from h.depotSvc.GetDownloadURL for any object id; to fix,
> inside Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify
> the caller has access (ownership or visibility) before calling
> h.depotSvc.GetDownloadURL—use the authenticated identity returned by
> middleware.EmailFromContext to authorize (or call your particle service /
> Firebase as noted in the TODO) and return http.StatusForbidden when the caller
> is not permitted; ensure this authorization check happens prior to requesting
> the signed URL.
> ```
>
> </details>
>
> <!-- cr-comment:v1:f6198c13127b7e94f0db6355 -->
>
> </blockquote></details>
>
> </blockquote></details>
<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>
<details>
<summary>js/desktop/src/features/particles/particle-list-view.tsx (1)</summary><blockquote>
`153-193`: _⚡ Quick win_
**Extract shared avatar-derivation logic to avoid drift with `stream-card.tsx`.**
This memoized DM/creator/fallback avatar resolution is duplicated with near-identical logic in `js/desktop/src/features/particles/stream-card.tsx` (Line 44-84). A shared helper (e.g., `resolveStreamAvatarDisplay`) would keep behavior consistent and reduce future regressions.
As per coding guidelines, "Write simpler, maintainable code in the Electron app instead of clever behavior."
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@js/desktop/src/features/particles/particle-list-view.tsx` around lines 153 -
193, The avatar derivation logic inside the avatar useMemo should be extracted
into a shared helper (e.g., export function resolveStreamAvatarDisplay) and both
particle-list-view.tsx and stream-card.tsx should call that helper instead of
duplicating logic; implement resolveStreamAvatarDisplay to accept the same
inputs used in the memo (particle, userId, network, latestChild, isDM), return
the same shape ({ initials, avatarObjectId }), export it from a common utilities
module, replace the useMemo body in particle-list-view (the avatar variable) and
the duplicated block in stream-card.tsx to call the helper (keeping memoization
callers as needed), and update imports accordingly so behavior and return shape
remain identical.
```
</details>
<!-- cr-comment:v1:62302258cffac352c66c3d8d -->
_Source: Coding guidelines_
</blockquote></details>
</blockquote></details>
<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>
```
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 `@go/internal/handler/handler.go`:
- Around line 345-365: The handler currently clears avatar_object_id before
capturing the prior object and thus leaks the old object; also on upload the
code uploads to h.depotSvc but if the subsequent DB update
(h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned. Fix by
first calling h.humanSvc.GetByID(r.Context(), humanId) to capture
oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change);
after the DB operation succeeds, call h.depotSvc.Delete(r.Context(),
oldAvatarObjectID) to remove the previous blob; for uploads, perform
h.depotSvc.Upload(...) then attempt the DB update (e.g.,
h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call
h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it
isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID,
h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and
h.depotSvc.Delete in this new order and add error logging around the rollback
delete.
- Around line 381-391: The handler wraps the request body with
http.MaxBytesReader but treats any upload-size error from
h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and
return http.StatusRequestEntityTooLarge instead. Update the error handling after
calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or
if that sentinel isn't available, match the "request body too large" error
string) respond with http.Error(w, "request entity too large",
http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and
500 behavior. Ensure you import the errors package if needed and reference the
r.Body wrapping and h.depotSvc.CreateFromReader call in your change.
In `@go/migrations/000017_human_avatar.down.sql`:
- Line 4: The rollback migration SQL in 000017_human_avatar.down.sql contains a
trailing comma after the DROP COLUMN list ("DROP COLUMN IF EXISTS
avatar_object_id,") which makes the statement invalid; remove the trailing comma
so the DROP COLUMN clause is a properly terminated SQL statement (ensure the
DROP COLUMN list ends without a comma and the statement ends with the
appropriate semicolon or end-of-statement token).
In `@js/desktop/src/api/client.ts`:
- Around line 160-163: The getAvatarDownloadUrl method builds the path using
objectId directly, which can break for reserved characters; update
getAvatarDownloadUrl to encode the id before interpolation (e.g., use
encodeURIComponent on the objectId when constructing `/humans/avatar/${...}`) so
the call to this.fetch('GET', `/humans/avatar/${objectId}`) always sends a valid
path; ensure any callers that pass objectId still provide the raw id and let
this method perform encoding.
In `@js/desktop/src/lib/avatar-image.ts`:
- Around line 11-12: Validate and normalize the incoming size parameter at the
start of the avatar generation function (the function with signature "{ size =
512, mirror = false }: { size?: number; mirror?: boolean } = {}, ):
Promise<Blob>"). Ensure size is a positive integer before using it to create the
canvas: check Number.isInteger(size) && size > 0 (or coerce via Math.floor and
then verify >0), and if invalid either throw a clear error or fall back to a
safe default (e.g., 512); then use that validated/normalized value for the
canvas creation to prevent runtime errors or broken avatars.
---
Outside diff comments:
In `@go/internal/handler/handler.go`:
- Around line 787-800: GetObjectDownloadUrl currently only checks authentication
via middleware.EmailFromContext but then issues a signed URL from
h.depotSvc.GetDownloadURL for any object id; to fix, inside
Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify the
caller has access (ownership or visibility) before calling
h.depotSvc.GetDownloadURL—use the authenticated identity returned by
middleware.EmailFromContext to authorize (or call your particle service /
Firebase as noted in the TODO) and return http.StatusForbidden when the caller
is not permitted; ensure this authorization check happens prior to requesting
the signed URL.
---
Nitpick comments:
In `@js/desktop/src/features/particles/particle-list-view.tsx`:
- Around line 153-193: The avatar derivation logic inside the avatar useMemo
should be extracted into a shared helper (e.g., export function
resolveStreamAvatarDisplay) and both particle-list-view.tsx and stream-card.tsx
should call that helper instead of duplicating logic; implement
resolveStreamAvatarDisplay to accept the same inputs used in the memo (particle,
userId, network, latestChild, isDM), return the same shape ({ initials,
avatarObjectId }), export it from a common utilities module, replace the useMemo
body in particle-list-view (the avatar variable) and the duplicated block in
stream-card.tsx to call the helper (keeping memoization callers as needed), and
update imports accordingly so behavior and return shape remain identical.
```
</details>
<details>
<summary>🪄 Autofix (Beta)</summary>
Fix all unresolved CodeRabbit comments on this PR:
- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes
</details>
---
<details>
<summary>ℹ️ Review info</summary>
<details>
<summary>⚙️ Run configuration</summary>
**Configuration used**: defaults
**Review profile**: CHILL
**Plan**: Pro Plus
**Run ID**: `f1b2fa73-7efe-4b67-abc5-45c5f2ee1d57`
</details>
<details>
<summary>📥 Commits</summary>
Reviewing files that changed from the base of the PR and between ca3bbf204eb997627d682e97a2dfd3e03235bdfb and fa9d54aab5ba3fc28c8c8930154394e653515576.
</details>
<details>
<summary>📒 Files selected for processing (25)</summary>
* `go/cmd/orion/main.go`
* `go/internal/handler/handler.go`
* `go/internal/human/models.go`
* `go/internal/human/repository.go`
* `go/internal/human/service.go`
* `go/internal/human/service_test.go`
* `go/migrations/000017_human_avatar.down.sql`
* `go/migrations/000017_human_avatar.up.sql`
* `js/desktop/src/api/client.ts`
* `js/desktop/src/api/types.ts`
* `js/desktop/src/components/human-avatar.tsx`
* `js/desktop/src/features/network-settings.tsx`
* `js/desktop/src/features/particles/particle-list-view.tsx`
* `js/desktop/src/features/particles/playback-page-indicator.tsx`
* `js/desktop/src/features/particles/reaction-bar.tsx`
* `js/desktop/src/features/particles/stream-card.tsx`
* `js/desktop/src/features/particles/stream-members-overlay.tsx`
* `js/desktop/src/features/particles/stream-top-bar.tsx`
* `js/desktop/src/features/settings-page.tsx`
* `js/desktop/src/features/settings/avatar-edit-dialog.tsx`
* `js/desktop/src/hooks/use-avatar-url.ts`
* `js/desktop/src/hooks/use-presence-positions.ts`
* `js/desktop/src/lib/avatar-image.ts`
* `js/desktop/src/lib/humans.ts`
* `js/desktop/src/stores/auth-store.ts`
</details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->
Avatar blob lifecycle is inconsistent and leaks objects.
Line 345 clears avatar_object_id before reading prior state, so the old avatar object ID is lost and never deleted.
Also, if Line 383 upload succeeds but Line 394 DB update fails, the newly uploaded object is orphaned.
Suggested lifecycle shape
+// DeleteAvatar:
+// 1) read current human/avatar_object_id
+// 2) clear avatar_object_id in DB
+// 3) best-effort delete old object using captured ID
+// UpdateAvatar:
+// 1) read current avatar_object_id (oldID)
+// 2) upload new object (newID)
+// 3) update DB to newID
+// 4) if DB update fails, best-effort delete newID
+// 5) after successful DB update, best-effort delete oldID
Also applies to: 383-399
🤖 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 `@go/internal/handler/handler.go` around lines 345 - 365, The handler currently
clears avatar_object_id before capturing the prior object and thus leaks the old
object; also on upload the code uploads to h.depotSvc but if the subsequent DB
update (h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned.
Fix by first calling h.humanSvc.GetByID(r.Context(), humanId) to capture
oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change);
after the DB operation succeeds, call h.depotSvc.Delete(r.Context(),
oldAvatarObjectID) to remove the previous blob; for uploads, perform
h.depotSvc.Upload(...) then attempt the DB update (e.g.,
h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call
h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it
isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID,
h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and
h.depotSvc.Delete in this new order and add error logging around the rollback
delete.
_⚠️ Potential issue_ | _🟠 Major_ | _🏗️ Heavy lift_
**Avatar blob lifecycle is inconsistent and leaks objects.**
Line 345 clears `avatar_object_id` before reading prior state, so the old avatar object ID is lost and never deleted.
Also, if Line 383 upload succeeds but Line 394 DB update fails, the newly uploaded object is orphaned.
<details>
<summary>Suggested lifecycle shape</summary>
```diff
+// DeleteAvatar:
+// 1) read current human/avatar_object_id
+// 2) clear avatar_object_id in DB
+// 3) best-effort delete old object using captured ID
+// UpdateAvatar:
+// 1) read current avatar_object_id (oldID)
+// 2) upload new object (newID)
+// 3) update DB to newID
+// 4) if DB update fails, best-effort delete newID
+// 5) after successful DB update, best-effort delete oldID
```
</details>
Also applies to: 383-399
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/internal/handler/handler.go` around lines 345 - 365, The handler currently
clears avatar_object_id before capturing the prior object and thus leaks the old
object; also on upload the code uploads to h.depotSvc but if the subsequent DB
update (h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned.
Fix by first calling h.humanSvc.GetByID(r.Context(), humanId) to capture
oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change);
after the DB operation succeeds, call h.depotSvc.Delete(r.Context(),
oldAvatarObjectID) to remove the previous blob; for uploads, perform
h.depotSvc.Upload(...) then attempt the DB update (e.g.,
h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call
h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it
isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID,
h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and
h.depotSvc.Delete in this new order and add error logging around the rollback
delete.
```
</details>
<!-- fingerprinting:phantom:poseidon:hawk -->
<!-- cr-comment:v1:843b5388d0579f91608f888a -->
<!-- This is an auto-generated comment by CodeRabbit -->
Return 413 for oversized avatar uploads instead of 500.
When http.MaxBytesReader limit is exceeded, the handler currently falls into a generic 500. This should map to http.StatusRequestEntityTooLarge.
Suggested fix
object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{
Prefix: "avatars",
Name: fmt.Sprintf("%s-avatar", humanId),
ContentType: r.Header.Get("Content-Type"),
}, r.Body)
if err != nil {
+ var maxErr *http.MaxBytesError
+ if errors.As(err, &maxErr) {
+ http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
+ return
+ }
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
📝 Committable suggestion
‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{
Prefix: "avatars",
Name: fmt.Sprintf("%s-avatar", humanId),
ContentType: r.Header.Get("Content-Type"),
}, r.Body)
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
return
}
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
🤖 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 `@go/internal/handler/handler.go` around lines 381 - 391, The handler wraps the
request body with http.MaxBytesReader but treats any upload-size error from
h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and
return http.StatusRequestEntityTooLarge instead. Update the error handling after
calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or
if that sentinel isn't available, match the "request body too large" error
string) respond with http.Error(w, "request entity too large",
http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and
500 behavior. Ensure you import the errors package if needed and reference the
r.Body wrapping and h.depotSvc.CreateFromReader call in your change.
_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_
**Return 413 for oversized avatar uploads instead of 500.**
When `http.MaxBytesReader` limit is exceeded, the handler currently falls into a generic 500. This should map to `http.StatusRequestEntityTooLarge`.
<details>
<summary>Suggested fix</summary>
```diff
object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{
Prefix: "avatars",
Name: fmt.Sprintf("%s-avatar", humanId),
ContentType: r.Header.Get("Content-Type"),
}, r.Body)
if err != nil {
+ var maxErr *http.MaxBytesError
+ if errors.As(err, &maxErr) {
+ http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
+ return
+ }
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
```
</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{
Prefix: "avatars",
Name: fmt.Sprintf("%s-avatar", humanId),
ContentType: r.Header.Get("Content-Type"),
}, r.Body)
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
return
}
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
```
</details>
<!-- suggestion_end -->
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/internal/handler/handler.go` around lines 381 - 391, The handler wraps the
request body with http.MaxBytesReader but treats any upload-size error from
h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and
return http.StatusRequestEntityTooLarge instead. Update the error handling after
calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or
if that sentinel isn't available, match the "request body too large" error
string) respond with http.Error(w, "request entity too large",
http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and
500 behavior. Ensure you import the errors package if needed and reference the
r.Body wrapping and h.depotSvc.CreateFromReader call in your change.
```
</details>
<!-- fingerprinting:phantom:poseidon:hawk -->
<!-- cr-comment:v1:6fffad183715aaf0f26ca668 -->
<!-- This is an auto-generated comment by CodeRabbit -->
✅ Addressed in commits ffac812 to 6bf2c99
Line 4 has a trailing comma, which makes the down migration fail to execute.
Suggested fix
ALTER TABLE humans
- DROP COLUMN IF EXISTS avatar_object_id,
+ DROP COLUMN IF EXISTS avatar_object_id;
📝 Committable suggestion
‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
ALTER TABLE humans
DROP COLUMN IF EXISTS avatar_object_id;
🤖 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 `@go/migrations/000017_human_avatar.down.sql` at line 4, The rollback migration
SQL in 000017_human_avatar.down.sql contains a trailing comma after the DROP
COLUMN list ("DROP COLUMN IF EXISTS avatar_object_id,") which makes the
statement invalid; remove the trailing comma so the DROP COLUMN clause is a
properly terminated SQL statement (ensure the DROP COLUMN list ends without a
comma and the statement ends with the appropriate semicolon or end-of-statement
token).
_⚠️ Potential issue_ | _🔴 Critical_ | _⚡ Quick win_
**Fix invalid SQL in rollback migration.**
Line 4 has a trailing comma, which makes the down migration fail to execute.
<details>
<summary>Suggested fix</summary>
```diff
ALTER TABLE humans
- DROP COLUMN IF EXISTS avatar_object_id,
+ DROP COLUMN IF EXISTS avatar_object_id;
```
</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
ALTER TABLE humans
DROP COLUMN IF EXISTS avatar_object_id;
```
</details>
<!-- suggestion_end -->
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/migrations/000017_human_avatar.down.sql` at line 4, The rollback migration
SQL in 000017_human_avatar.down.sql contains a trailing comma after the DROP
COLUMN list ("DROP COLUMN IF EXISTS avatar_object_id,") which makes the
statement invalid; remove the trailing comma so the DROP COLUMN clause is a
properly terminated SQL statement (ensure the DROP COLUMN list ends without a
comma and the statement ends with the appropriate semicolon or end-of-statement
token).
```
</details>
<!-- fingerprinting:phantom:poseidon:hawk -->
<!-- cr-comment:v1:6d0227aabf7772fa5ddadda5 -->
<!-- This is an auto-generated comment by CodeRabbit -->
✅ Addressed in commits a45beb9 to a25fb83
‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@js/desktop/src/api/client.ts` around lines 160 - 163, The
getAvatarDownloadUrl method builds the path using objectId directly, which can
break for reserved characters; update getAvatarDownloadUrl to encode the id
before interpolation (e.g., use encodeURIComponent on the objectId when
constructing `/humans/avatar/${...}`) so the call to this.fetch('GET',
`/humans/avatar/${objectId}`) always sends a valid path; ensure any callers that
pass objectId still provide the raw id and let this method perform encoding.
_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_
**Encode `objectId` before building the avatar URL path.**
Line 161 interpolates `objectId` directly. Reserved characters can produce a malformed route and wrong request target.
<details>
<summary>Suggested fix</summary>
```diff
async getAvatarDownloadUrl(objectId: string): Promise<string> {
- const response = await this.fetch('GET', `/humans/avatar/${objectId}`);
+ const encodedObjectId = encodeURIComponent(objectId);
+ const response = await this.fetch('GET', `/humans/avatar/${encodedObjectId}`);
const data = await response.json();
return data.url;
}
```
</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
async getAvatarDownloadUrl(objectId: string): Promise<string> {
const encodedObjectId = encodeURIComponent(objectId);
const response = await this.fetch('GET', `/humans/avatar/${encodedObjectId}`);
const data = await response.json();
return data.url;
}
```
</details>
<!-- suggestion_end -->
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@js/desktop/src/api/client.ts` around lines 160 - 163, The
getAvatarDownloadUrl method builds the path using objectId directly, which can
break for reserved characters; update getAvatarDownloadUrl to encode the id
before interpolation (e.g., use encodeURIComponent on the objectId when
constructing `/humans/avatar/${...}`) so the call to this.fetch('GET',
`/humans/avatar/${objectId}`) always sends a valid path; ensure any callers that
pass objectId still provide the raw id and let this method perform encoding.
```
</details>
<!-- fingerprinting:phantom:poseidon:hawk -->
<!-- cr-comment:v1:7687b9b14bbed6dbbd798b45 -->
<!-- This is an auto-generated comment by CodeRabbit -->
Validate size as a positive integer before creating the canvas.
Line 22 depends on size; invalid values can yield broken avatar output or throw at runtime.
Suggested fix
export async function toAvatarBlob(
source: ImageBitmap,
{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},
): Promise<Blob> {
+ if (!Number.isInteger(size) || size <= 0) {
+ throw new Error('Avatar size must be a positive integer');
+ }
const side = Math.min(source.width, source.height);
if (side === 0) {
📝 Committable suggestion
‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},
): Promise<Blob> {
if (!Number.isInteger(size) || size <= 0) {
throw new Error('Avatar size must be a positive integer');
}
🤖 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 `@js/desktop/src/lib/avatar-image.ts` around lines 11 - 12, Validate and
normalize the incoming size parameter at the start of the avatar generation
function (the function with signature "{ size = 512, mirror = false }: { size?:
number; mirror?: boolean } = {}, ): Promise<Blob>"). Ensure size is a positive
integer before using it to create the canvas: check Number.isInteger(size) &&
size > 0 (or coerce via Math.floor and then verify >0), and if invalid either
throw a clear error or fall back to a safe default (e.g., 512); then use that
validated/normalized value for the canvas creation to prevent runtime errors or
broken avatars.
_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_
**Validate `size` as a positive integer before creating the canvas.**
Line 22 depends on `size`; invalid values can yield broken avatar output or throw at runtime.
<details>
<summary>Suggested fix</summary>
```diff
export async function toAvatarBlob(
source: ImageBitmap,
{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},
): Promise<Blob> {
+ if (!Number.isInteger(size) || size <= 0) {
+ throw new Error('Avatar size must be a positive integer');
+ }
const side = Math.min(source.width, source.height);
if (side === 0) {
```
</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},
): Promise<Blob> {
if (!Number.isInteger(size) || size <= 0) {
throw new Error('Avatar size must be a positive integer');
}
```
</details>
<!-- suggestion_end -->
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@js/desktop/src/lib/avatar-image.ts` around lines 11 - 12, Validate and
normalize the incoming size parameter at the start of the avatar generation
function (the function with signature "{ size = 512, mirror = false }: { size?:
number; mirror?: boolean } = {}, ): Promise<Blob>"). Ensure size is a positive
integer before using it to create the canvas: check Number.isInteger(size) &&
size > 0 (or coerce via Math.floor and then verify >0), and if invalid either
throw a clear error or fall back to a safe default (e.g., 512); then use that
validated/normalized value for the canvas creation to prevent runtime errors or
broken avatars.
```
</details>
<!-- fingerprinting:phantom:poseidon:hawk -->
<!-- cr-comment:v1:3412b6e39458ff9b8c5d8ef9 -->
<!-- This is an auto-generated comment by CodeRabbit -->
✅ Addressed in commits ffac812 to 6bf2c99
coderabbitai[bot]
(Migrated from github.com)
reviewed 2026-06-11 22:21:17 +00:00
coderabbitai[bot]
(Migrated from github.com)
left a comment
Copy Link
Copy Source
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
go/internal/handler/handler.go (1)
793-805: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
Enforce object-level authorization before minting signed URLs.
This handler only checks that the caller is authenticated, then signs whatever object ID is in the path. With this layer now serving avatar/object downloads, any authenticated user who learns another object's ID can mint a valid URL without a membership or ownership check.
🤖 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 `@go/internal/handler/handler.go` around lines 793 - 805, In
GetObjectDownloadUrl, do an object-level authorization check before calling
h.depotSvc.GetDownloadURL: extract the caller identity using
middleware.EmailFromContext(r.Context()), fetch the object's metadata (e.g., via
an existing particle/object lookup like a particle service or depot metadata
method), verify the object's visibility or that the caller is an owner/member
(compare the fetched metadata's owner/members/visibility fields to the
email/identity), and if the check fails return http.Error(...,
http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(),
objectID) and write the URL on success. Ensure you use the existing symbols
Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and
h.depotSvc.GetDownloadURL to locate the code to change.
🤖 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 `@go/internal/handler/handler.go`:
- Around line 388-398: The UpdateAvatar function contains an extra closing brace
'}' after the error handling block (the one closing the function early) which
causes a parse error; remove that unmatched '}' so the function body continues
normally (locate the error handling block in UpdateAvatar around the errors.As
check and the flog.Error/http.Error calls and delete the extraneous closing
brace), then run go build to confirm parsing succeeds.
---
Outside diff comments:
In `@go/internal/handler/handler.go`:
- Around line 793-805: In GetObjectDownloadUrl, do an object-level authorization
check before calling h.depotSvc.GetDownloadURL: extract the caller identity
using middleware.EmailFromContext(r.Context()), fetch the object's metadata
(e.g., via an existing particle/object lookup like a particle service or depot
metadata method), verify the object's visibility or that the caller is an
owner/member (compare the fetched metadata's owner/members/visibility fields to
the email/identity), and if the check fails return http.Error(...,
http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(),
objectID) and write the URL on success. Ensure you use the existing symbols
Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and
h.depotSvc.GetDownloadURL to locate the code to change.
🪄 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 Plus
Run ID: 5eee9ab3-504d-4409-8a31-6fdeca6719b6
📥 Commits
Reviewing files that changed from the base of the PR and between fa9d54aab5 and 6bf2c9983a.
📒 Files selected for processing (2)
go/internal/handler/handler.go
js/desktop/src/lib/avatar-image.ts
🚧 Files skipped from review as they are similar to previous changes (1)
js/desktop/src/lib/avatar-image.ts
**Actionable comments posted: 1**
> [!CAUTION]
> Some comments are outside the diff and can’t be posted inline due to platform limitations.
>
>
>
> <details>
> <summary>⚠️ Outside diff range comments (1)</summary><blockquote>
>
> <details>
> <summary>go/internal/handler/handler.go (1)</summary><blockquote>
>
> `793-805`: _⚠️ Potential issue_ | _🟠 Major_ | _🏗️ Heavy lift_
>
> **Enforce object-level authorization before minting signed URLs.**
>
> This handler only checks that the caller is authenticated, then signs whatever object ID is in the path. With this layer now serving avatar/object downloads, any authenticated user who learns another object's ID can mint a valid URL without a membership or ownership check.
>
> <details>
> <summary>🤖 Prompt for AI Agents</summary>
>
> ```
> Verify each finding against current code. Fix only still-valid issues, skip the
> rest with a brief reason, keep changes minimal, and validate.
>
> In `@go/internal/handler/handler.go` around lines 793 - 805, In
> GetObjectDownloadUrl, do an object-level authorization check before calling
> h.depotSvc.GetDownloadURL: extract the caller identity using
> middleware.EmailFromContext(r.Context()), fetch the object's metadata (e.g., via
> an existing particle/object lookup like a particle service or depot metadata
> method), verify the object's visibility or that the caller is an owner/member
> (compare the fetched metadata's owner/members/visibility fields to the
> email/identity), and if the check fails return http.Error(...,
> http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(),
> objectID) and write the URL on success. Ensure you use the existing symbols
> Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and
> h.depotSvc.GetDownloadURL to locate the code to change.
> ```
>
> </details>
>
> <!-- cr-comment:v1:bd0b7c6ddefafc5436932dca -->
>
> </blockquote></details>
>
> </blockquote></details>
<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>
```
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 `@go/internal/handler/handler.go`:
- Around line 388-398: The UpdateAvatar function contains an extra closing brace
'}' after the error handling block (the one closing the function early) which
causes a parse error; remove that unmatched '}' so the function body continues
normally (locate the error handling block in UpdateAvatar around the errors.As
check and the flog.Error/http.Error calls and delete the extraneous closing
brace), then run go build to confirm parsing succeeds.
---
Outside diff comments:
In `@go/internal/handler/handler.go`:
- Around line 793-805: In GetObjectDownloadUrl, do an object-level authorization
check before calling h.depotSvc.GetDownloadURL: extract the caller identity
using middleware.EmailFromContext(r.Context()), fetch the object's metadata
(e.g., via an existing particle/object lookup like a particle service or depot
metadata method), verify the object's visibility or that the caller is an
owner/member (compare the fetched metadata's owner/members/visibility fields to
the email/identity), and if the check fails return http.Error(...,
http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(),
objectID) and write the URL on success. Ensure you use the existing symbols
Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and
h.depotSvc.GetDownloadURL to locate the code to change.
```
</details>
<details>
<summary>🪄 Autofix (Beta)</summary>
Fix all unresolved CodeRabbit comments on this PR:
- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes
</details>
---
<details>
<summary>ℹ️ Review info</summary>
<details>
<summary>⚙️ Run configuration</summary>
**Configuration used**: defaults
**Review profile**: CHILL
**Plan**: Pro Plus
**Run ID**: `5eee9ab3-504d-4409-8a31-6fdeca6719b6`
</details>
<details>
<summary>📥 Commits</summary>
Reviewing files that changed from the base of the PR and between fa9d54aab5ba3fc28c8c8930154394e653515576 and 6bf2c9983a0f6d4a00f24c531ca201850f7a6537.
</details>
<details>
<summary>📒 Files selected for processing (2)</summary>
* `go/internal/handler/handler.go`
* `js/desktop/src/lib/avatar-image.ts`
</details>
<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>
* js/desktop/src/lib/avatar-image.ts
</details>
</details>
<!-- This is an auto-generated comment by CodeRabbit for review status -->
Line 398 closes the function early, so Line 400 starts with a bare assignment and the file no longer parses. That matches the CI failure exactly.
Suggested fix
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
return
}
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
- }
err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID)
📝 Committable suggestion
‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
return
}
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID)
🤖 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 `@go/internal/handler/handler.go` around lines 388 - 398, The UpdateAvatar
function contains an extra closing brace '}' after the error handling block (the
one closing the function early) which causes a parse error; remove that
unmatched '}' so the function body continues normally (locate the error handling
block in UpdateAvatar around the errors.As check and the flog.Error/http.Error
calls and delete the extraneous closing brace), then run go build to confirm
parsing succeeds.
Source: Pipeline failures
_⚠️ Potential issue_ | _🔴 Critical_ | _⚡ Quick win_
**Remove the unmatched `}` in `UpdateAvatar`.**
Line 398 closes the function early, so Line 400 starts with a bare assignment and the file no longer parses. That matches the CI failure exactly.
<details>
<summary>Suggested fix</summary>
```diff
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
return
}
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
- }
err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID)
```
</details>
<!-- suggestion_start -->
<details>
<summary>📝 Committable suggestion</summary>
> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
```suggestion
if err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
return
}
flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID)
```
</details>
<!-- suggestion_end -->
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/internal/handler/handler.go` around lines 388 - 398, The UpdateAvatar
function contains an extra closing brace '}' after the error handling block (the
one closing the function early) which causes a parse error; remove that
unmatched '}' so the function body continues normally (locate the error handling
block in UpdateAvatar around the errors.As check and the flog.Error/http.Error
calls and delete the extraneous closing brace), then run go build to confirm
parsing succeeds.
```
</details>
<!-- fingerprinting:phantom:medusa:grasshopper -->
<!-- cr-comment:v1:30a7d713243d8df97f8f25a3 -->
_Source: Pipeline failures_
<!-- This is an auto-generated comment by CodeRabbit -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #70
Summary by CodeRabbit
No actionable comments were generated in the recent review. 🎉
ℹ️ Recent review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID:
73480fa0-39ce-4438-aeef-71baef5ae49d📥 Commits
Reviewing files that changed from the base of the PR and between
6f8349c62fand6ce7cc8651.📒 Files selected for processing (3)
go/internal/handler/handler.gogo/migrations/000017_human_avatar.down.sqljs/desktop/src/features/particles/stream-card.tsx💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 Walkthrough
Walkthrough
Implements user-controlled profile avatars: DB migration and model field, repository/service update/delete support, HTTP handlers and routes, desktop API client methods, avatar utilities/hooks/components, settings edit dialog with upload/camera, and avatar rendering across particle and network UIs.
Changes
Avatar Management Feature
go/migrations/000017_human_avatar.up.sql,go/migrations/000017_human_avatar.down.sql,go/internal/human/models.goavatar_object_idcolumn and maps it into the GoHumanmodel asAvatarObjectID.go/internal/human/repository.go,go/internal/human/service.go,go/internal/human/service_test.goavatar_object_id, addsupdateAvatarObjectID; service exposesUpdateAvatar/DeleteAvatar, validates parameters and maps repo errors; tests cover avatar lifecycle.go/internal/handler/handler.go,go/cmd/orion/main.goUpdateAvatarandDeleteAvatarhandlers (5MB request limit, depot uploads, best-effort depot cleanup), maps avatar field into DTO, renamesDownloadParticleMedia→GetObjectDownloadUrl, and wires settings/particles routes.js/desktop/src/api/client.ts,js/desktop/src/api/types.tssendhelper, JSON fetch delegation, and client methodsupdateAvatar,deleteAvatar,getAvatarDownloadUrl;HumanSchemaadds optionalavatar_object_id.js/desktop/src/hooks/use-avatar-url.ts,js/desktop/src/lib/avatar-image.ts,js/desktop/src/lib/humans.ts,js/desktop/src/hooks/use-presence-positions.ts,js/desktop/src/stores/auth-store.tsuseAvatarUrl(React Query) to fetch signed URLs,toAvatarBlobto center-crop and encode images, surfacesavatarObjectIdin human display/presence shapes, and addsrefreshUserto auth store.js/desktop/src/components/human-avatar.tsxuseAvatarUrland rendersAvatarImageorAvatarFallback(initials) as fallback.js/desktop/src/features/settings/avatar-edit-dialog.tsxtoAvatarBlob, API calls for save/remove, preview lifecycle, and CameraCapture subcomponent.js/desktop/src/features/settings-page.tsxHumanAvatarwith camera overlay and opens AvatarEditDialog for editing.js/desktop/src/features/particles/*,js/desktop/src/features/network-settings.tsxHumanAvataracross particle list, cards, reaction bar, presence indicators, members overlay, top bar, and network settings; memoized avatar resolution now includesavatarObjectId.Sequence Diagram(s)
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
#70: user-controlled avatar creation via photo snapping or uploading, persistent user-controlled avatars to ensure recognition and consent, and avoids arbitrary video frame usage.✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
📝 Generate docstrings
🧪 Generate unit tests (beta)
worktree-refactored-strolling-treasureComment
@coderabbitai helpto get the list of available commands and usage tips.Actionable comments posted: 5
🧹 Nitpick comments (1)
🤖 Prompt for all review comments with AI agents
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID:
f1b2fa73-7efe-4b67-abc5-45c5f2ee1d57📥 Commits
Reviewing files that changed from the base of the PR and between
ca3bbf204eandfa9d54aab5.📒 Files selected for processing (25)
go/cmd/orion/main.gogo/internal/handler/handler.gogo/internal/human/models.gogo/internal/human/repository.gogo/internal/human/service.gogo/internal/human/service_test.gogo/migrations/000017_human_avatar.down.sqlgo/migrations/000017_human_avatar.up.sqljs/desktop/src/api/client.tsjs/desktop/src/api/types.tsjs/desktop/src/components/human-avatar.tsxjs/desktop/src/features/network-settings.tsxjs/desktop/src/features/particles/particle-list-view.tsxjs/desktop/src/features/particles/playback-page-indicator.tsxjs/desktop/src/features/particles/reaction-bar.tsxjs/desktop/src/features/particles/stream-card.tsxjs/desktop/src/features/particles/stream-members-overlay.tsxjs/desktop/src/features/particles/stream-top-bar.tsxjs/desktop/src/features/settings-page.tsxjs/desktop/src/features/settings/avatar-edit-dialog.tsxjs/desktop/src/hooks/use-avatar-url.tsjs/desktop/src/hooks/use-presence-positions.tsjs/desktop/src/lib/avatar-image.tsjs/desktop/src/lib/humans.tsjs/desktop/src/stores/auth-store.ts@@ -334,6 +335,83 @@ func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {w.WriteHeader(http.StatusNoContent)⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift
Avatar blob lifecycle is inconsistent and leaks objects.
Line 345 clears
avatar_object_idbefore reading prior state, so the old avatar object ID is lost and never deleted.Also, if Line 383 upload succeeds but Line 394 DB update fails, the newly uploaded object is orphaned.
Suggested lifecycle shape
Also applies to: 383-399
🤖 Prompt for AI Agents
@@ -337,0 +400,4 @@flog.Error("failed to update human avatar", "error", err, "humanId", humanId)http.Error(w, "internal server error", http.StatusInternalServerError)// best effort⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Return 413 for oversized avatar uploads instead of 500.
When
http.MaxBytesReaderlimit is exceeded, the handler currently falls into a generic 500. This should map tohttp.StatusRequestEntityTooLarge.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
✅ Addressed in commits
ffac812to6bf2c99@@ -0,0 +1,6 @@BEGIN;⚠️ Potential issue | 🔴 Critical | ⚡ Quick win
Fix invalid SQL in rollback migration.
Line 4 has a trailing comma, which makes the down migration fail to execute.
Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
✅ Addressed in commits
a45beb9toa25fb83⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Encode
objectIdbefore building the avatar URL path.Line 161 interpolates
objectIddirectly. Reserved characters can produce a malformed route and wrong request target.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
@@ -0,0 +9,4 @@export async function toAvatarBlob(source: ImageBitmap,{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},): Promise<Blob> {⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
Validate
sizeas a positive integer before creating the canvas.Line 22 depends on
size; invalid values can yield broken avatar output or throw at runtime.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
✅ Addressed in commits
ffac812to6bf2c99Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID:
5eee9ab3-504d-4409-8a31-6fdeca6719b6📥 Commits
Reviewing files that changed from the base of the PR and between
fa9d54aab5and6bf2c9983a.📒 Files selected for processing (2)
go/internal/handler/handler.gojs/desktop/src/lib/avatar-image.ts🚧 Files skipped from review as they are similar to previous changes (1)
@@ -334,6 +335,83 @@ func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {w.WriteHeader(http.StatusNoContent)⚠️ Potential issue | 🔴 Critical | ⚡ Quick win
Remove the unmatched
}inUpdateAvatar.Line 398 closes the function early, so Line 400 starts with a bare assignment and the file no longer parses. That matches the CI failure exactly.
Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Pipeline failures