Skip to content

feat: #74 — Bug: Copy/paste invite code shows "No mappable coordinates found" instead of connection progress animation#109

Open
luandro wants to merge 1 commit into
mainfrom
agent/comapeo-cloud-app/issue-74
Open

feat: #74 — Bug: Copy/paste invite code shows "No mappable coordinates found" instead of connection progress animation#109
luandro wants to merge 1 commit into
mainfrom
agent/comapeo-cloud-app/issue-74

Conversation

@luandro

@luandro luandro commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Automated implementation of #74.

Closes #74


Implemented by the agent implementation worker.

Greptile Summary

This PR moves the connection-progress animation from HomeScreen into AddArchiveServerDialog so that pasting/typing an invite code now shows the progress steps instead of navigating to the map (the "No mappable coordinates found" error from #74). The onAdded callback in HomeScreen is simplified to just close the dialog.

  • AddArchiveServerDialog gains a ConnectionProgressState slice, two useEffect hooks that drive the step sequence (verify → connect → sync → prepare), and a conditional render that replaces the form with the ConnectionProgress component while the flow is running.
  • HomeScreen removes the START_CONNECTION_PROGRESS dispatch from all four onAdded handlers but still redundantly re-invalidates the same query keys the dialog already invalidates.
  • Two bugs are present: the "Try Again" button is a no-op (its useEffect deps do not change on a failed retry), and cpState is never reset when the dialog closes after success, causing the next "Add Server" open to render the completed-progress screen immediately.

Confidence Score: 3/5

The core fix works, but two functional regressions are introduced: the error-recovery path is broken and reopening the dialog after a successful add shows a stale completed-progress screen.

The Try Again button is silently non-functional after a sync failure, and the dialog state is never cleaned up after a successful add, so every subsequent open of Add Archive Server renders the old completed-progress UI instead of the invite form. Both issues affect user flows directly reachable from the happy path.

src/screens/Home/AddArchiveServerDialog.tsx — the connection-progress useEffect dependency array and the missing cpState reset on dialog close.

Important Files Changed

Filename Overview
src/screens/Home/AddArchiveServerDialog.tsx Moves connection-progress animation inside the dialog; introduces two bugs: (1) Try Again never re-runs because the useEffect deps don't change on retry, and (2) stale cpState persists across dialog sessions when it is closed via onAdded rather than onClose.
src/screens/Home/HomeScreen.tsx Removes START_CONNECTION_PROGRESS dispatch from all four onAdded handlers; now redundantly re-invalidates the same three query keys already invalidated inside the dialog.
tests/unit/screens/Home/AddArchiveServerDialog.test.tsx Adds mocks for syncRemoteArchive and useQueryClient, expands waitFor timeouts to 5s, and adds four new tests covering the connection-progress UI; does not add a test for the failing Try Again path or dialog-reopen state.

Sequence Diagram

sequenceDiagram
    participant User
    participant Dialog as AddArchiveServerDialog
    participant API as redeemEncryptedInvite
    participant Sync as syncRemoteArchive
    participant QC as QueryClient
    participant HS as HomeScreen

    User->>Dialog: paste invite code and click Add
    Dialog->>API: redeemEncryptedInvite(code)
    API-->>Dialog: baseUrl and token
    Dialog->>Dialog: addServer(baseUrl, token)
    Dialog-->>Dialog: startConnectionProgress
    Dialog->>Sync: syncRemoteArchive(serverId)
    Sync-->>Dialog: success
    Dialog->>QC: invalidateQueries projects/observations/alerts
    Dialog-->>Dialog: isComplete true
    Dialog->>HS: onAdded(serverId) after 1500ms
    HS->>HS: dispatch CLOSE_ADD_SERVER_DIALOG
    HS->>QC: invalidateQueries projects/observations/alerts again
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
src/screens/Home/AddArchiveServerDialog.tsx:263-329
**"Try Again" never re-runs `runConnection`**

When `syncRemoteArchive` fails, `cpState.isActive` stays `true` and `cpState.isComplete` stays `false`. Clicking "Try Again" calls `startConnectionProgress`, which sets `isActive: true` and `isComplete: false` — identical to their current values. The `useEffect` dependency array `[cpState.isActive, cpState.isComplete]` sees no change and does not re-run `runConnection`. The retry button is a no-op: the error screen persists and the connection is never attempted again.

A counter or a distinct trigger field (e.g., a monotonically incrementing `retryCount`) needs to be part of the dependency array so the effect re-fires on each retry attempt.

### Issue 2 of 4
src/screens/Home/AddArchiveServerDialog.tsx:570-620
**Stale connection-progress state persists across dialog sessions**

After a successful connection, `onAdded` fires and `HomeScreen` closes the dialog by dispatching `CLOSE_ADD_SERVER_DIALOG` — it does not call `onClose()`, so `handleClose` (which resets UI state) is never invoked. `cpState` is left as `{ isActive: true, isComplete: true }`. The next time the user opens "Add Archive Server", the component mounts with that stale state, immediately renders the completed connection-progress screen, and never shows the invite-code input form.

`cpState` should be reset to `INITIAL_CP_STATE` whenever the dialog transitions from open to closed (e.g., by reacting to `isOpen` changing to `false` via a `useEffect`, or by resetting inside `handleClose` and ensuring `onAdded` also calls `handleClose` after firing).

### Issue 3 of 4
src/screens/Home/AddArchiveServerDialog.tsx:611-613
The "Try Again" button label is a hard-coded English string. Every other button in this component uses `intl.formatMessage`. A missing i18n message key here means this text will not be translated for non-English users.

```suggestion
                >
                  {intl.formatMessage(messages.tryAgain)}
                </Button>
```

### Issue 4 of 4
src/screens/Home/HomeScreen.tsx:1119-1126
**Duplicate `invalidateQueries` calls**

`AddArchiveServerDialog` already calls `queryClient.invalidateQueries` for `['projects']`, `['observations']`, and `['alerts']` during step 2 ("Syncing data") of the connection progress. The same three invalidations are repeated here in every `onAdded` handler (appears 4 times across `HomeScreen.tsx`). This doubles the refetch work roughly 1500 ms after the first invalidation, which is redundant. The `HomeScreen` handlers could simply close the dialog and let the dialog's own invalidation handle the refresh.

Reviews (1): Last reviewed commit: "feat: implement #74 — Bug: Copy/paste in..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

…coordinates found" instead of connection progress animation
@socket-security

Copy link
Copy Markdown

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @typescript-eslint/eslint-plugin is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: ?npm/@typescript-eslint/eslint-plugin@8.60.1

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@typescript-eslint/eslint-plugin@8.60.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Comment on lines +263 to +329
if (!cpState.isActive || cpState.isComplete) return;

let cancelled = false;

async function runConnection() {
const steps = [...cpState.steps];

// Step 1: Connecting to server (index 1)
steps[1] = { ...steps[1]!, status: 'active' };
setCpState((prev) => ({ ...prev, steps: [...steps] }));

const result = await syncRemoteArchive(cpState.serverId, {
baseUrl: cpState.baseUrl,
token: cpState.token,
});
if (cancelled) return;

if (!result.success) {
steps[1] = { ...steps[1]!, status: 'error' };
setCpState((prev) => ({
...prev,
steps: [...steps],
errorMessage: result.error ?? 'Sync failed',
}));
return;
}

// Step 1: Connecting complete
steps[1] = { ...steps[1]!, status: 'completed' };
setCpState((prev) => ({ ...prev, steps: [...steps] }));

// Step 2: Syncing data (index 2)
steps[2] = { ...steps[2]!, status: 'active' };
setCpState((prev) => ({ ...prev, steps: [...steps] }));

await Promise.all([
queryClient.invalidateQueries({ queryKey: ['projects'] }),
queryClient.invalidateQueries({ queryKey: ['observations'] }),
queryClient.invalidateQueries({ queryKey: ['alerts'] }),
]);
if (cancelled) return;

steps[2] = { ...steps[2]!, status: 'completed' };
setCpState((prev) => ({ ...prev, steps: [...steps] }));

// Step 3: Preparing dashboard (index 3)
steps[3] = { ...steps[3]!, status: 'active' };
setCpState((prev) => ({ ...prev, steps: [...steps] }));

// Brief pause for the "Preparing" step to be visible
await new Promise((resolve) => setTimeout(resolve, 500));
if (cancelled) return;

steps[3] = { ...steps[3]!, status: 'completed' };
setCpState((prev) => ({
...prev,
steps: [...steps],
isComplete: true,
}));
}

void runConnection();
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cpState.isActive, cpState.isComplete]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 "Try Again" never re-runs runConnection

When syncRemoteArchive fails, cpState.isActive stays true and cpState.isComplete stays false. Clicking "Try Again" calls startConnectionProgress, which sets isActive: true and isComplete: false — identical to their current values. The useEffect dependency array [cpState.isActive, cpState.isComplete] sees no change and does not re-run runConnection. The retry button is a no-op: the error screen persists and the connection is never attempted again.

A counter or a distinct trigger field (e.g., a monotonically incrementing retryCount) needs to be part of the dependency array so the effect re-fires on each retry attempt.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/screens/Home/AddArchiveServerDialog.tsx
Line: 263-329

Comment:
**"Try Again" never re-runs `runConnection`**

When `syncRemoteArchive` fails, `cpState.isActive` stays `true` and `cpState.isComplete` stays `false`. Clicking "Try Again" calls `startConnectionProgress`, which sets `isActive: true` and `isComplete: false` — identical to their current values. The `useEffect` dependency array `[cpState.isActive, cpState.isComplete]` sees no change and does not re-run `runConnection`. The retry button is a no-op: the error screen persists and the connection is never attempted again.

A counter or a distinct trigger field (e.g., a monotonically incrementing `retryCount`) needs to be part of the dependency array so the effect re-fires on each retry attempt.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +570 to +620
// When connection progress is active, show it inside the dialog
if (cpState.isActive) {
return (
<Modal
open={isOpen}
onOpenChange={() => {
// Prevent closing during connection progress
}}
title={intl.formatMessage(messages.title)}
>
<div className="flex flex-col items-center gap-6 py-6">
<ConnectionProgress
steps={cpState.steps}
heading={intl.formatMessage(messages.connectionProgressHeading)}
isComplete={cpState.isComplete}
/>
{cpState.errorMessage && (
<div className="mt-2 flex flex-col items-center gap-3">
<p className="text-sm text-red-600" role="alert">
{cpState.errorMessage}
</p>
<div className="flex gap-2">
<Button
type="button"
variant="secondary"
size="sm"
onClick={handleClose}
>
{intl.formatMessage(messages.cancel)}
</Button>
<Button
type="button"
variant="primary"
size="sm"
onClick={() => {
startConnectionProgress(
cpState.serverId,
cpState.baseUrl,
cpState.token,
);
}}
>
Try Again
</Button>
</div>
</div>
)}
</div>
</Modal>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale connection-progress state persists across dialog sessions

After a successful connection, onAdded fires and HomeScreen closes the dialog by dispatching CLOSE_ADD_SERVER_DIALOG — it does not call onClose(), so handleClose (which resets UI state) is never invoked. cpState is left as { isActive: true, isComplete: true }. The next time the user opens "Add Archive Server", the component mounts with that stale state, immediately renders the completed connection-progress screen, and never shows the invite-code input form.

cpState should be reset to INITIAL_CP_STATE whenever the dialog transitions from open to closed (e.g., by reacting to isOpen changing to false via a useEffect, or by resetting inside handleClose and ensuring onAdded also calls handleClose after firing).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/screens/Home/AddArchiveServerDialog.tsx
Line: 570-620

Comment:
**Stale connection-progress state persists across dialog sessions**

After a successful connection, `onAdded` fires and `HomeScreen` closes the dialog by dispatching `CLOSE_ADD_SERVER_DIALOG` — it does not call `onClose()`, so `handleClose` (which resets UI state) is never invoked. `cpState` is left as `{ isActive: true, isComplete: true }`. The next time the user opens "Add Archive Server", the component mounts with that stale state, immediately renders the completed connection-progress screen, and never shows the invite-code input form.

`cpState` should be reset to `INITIAL_CP_STATE` whenever the dialog transitions from open to closed (e.g., by reacting to `isOpen` changing to `false` via a `useEffect`, or by resetting inside `handleClose` and ensuring `onAdded` also calls `handleClose` after firing).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +611 to +613
>
Try Again
</Button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The "Try Again" button label is a hard-coded English string. Every other button in this component uses intl.formatMessage. A missing i18n message key here means this text will not be translated for non-English users.

Suggested change
>
Try Again
</Button>
>
{intl.formatMessage(messages.tryAgain)}
</Button>
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/screens/Home/AddArchiveServerDialog.tsx
Line: 611-613

Comment:
The "Try Again" button label is a hard-coded English string. Every other button in this component uses `intl.formatMessage`. A missing i18n message key here means this text will not be translated for non-English users.

```suggestion
                >
                  {intl.formatMessage(messages.tryAgain)}
                </Button>
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +1119 to 1126
onAdded={(_serverId) => {
dispatch({ type: 'CLOSE_ADD_SERVER_DIALOG' });
const server = useAuthStore
.getState()
.servers.find((s) => s.id === serverId);
if (server) {
dispatch({
type: 'START_CONNECTION_PROGRESS',
serverId,
baseUrl: server.baseUrl,
token: server.token,
steps: buildConnectionProgressSteps(intl),
});
}
// Connection progress is now handled inside the dialog.
// Just invalidate queries to refresh data.
void queryClient.invalidateQueries({ queryKey: ['projects'] });
void queryClient.invalidateQueries({ queryKey: ['observations'] });
void queryClient.invalidateQueries({ queryKey: ['alerts'] });
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate invalidateQueries calls

AddArchiveServerDialog already calls queryClient.invalidateQueries for ['projects'], ['observations'], and ['alerts'] during step 2 ("Syncing data") of the connection progress. The same three invalidations are repeated here in every onAdded handler (appears 4 times across HomeScreen.tsx). This doubles the refetch work roughly 1500 ms after the first invalidation, which is redundant. The HomeScreen handlers could simply close the dialog and let the dialog's own invalidation handle the refresh.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/screens/Home/HomeScreen.tsx
Line: 1119-1126

Comment:
**Duplicate `invalidateQueries` calls**

`AddArchiveServerDialog` already calls `queryClient.invalidateQueries` for `['projects']`, `['observations']`, and `['alerts']` during step 2 ("Syncing data") of the connection progress. The same three invalidations are repeated here in every `onAdded` handler (appears 4 times across `HomeScreen.tsx`). This doubles the refetch work roughly 1500 ms after the first invalidation, which is redundant. The `HomeScreen` handlers could simply close the dialog and let the dialog's own invalidation handle the refresh.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Copy/paste invite code shows "No mappable coordinates found" instead of connection progress animation

1 participant