Skip to content

STU-384: Implement the account connection and single provider-key editor - #92

Merged
sgardoll merged 7 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-384-implement-the-account-connection-and-single-provider-key
Sep 22, 2026
Merged

sgardoll merged 7 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-384-implement-the-account-connection-and-single-provider-key

Conversation

@sgardoll

@sgardoll sgardoll commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

STU-384 — Implement the account connection and single provider-key editor

Linear: https://linear.app/stuart-gardoll/issue/STU-384/implement-the-account-connection-and-single-provider-key-editor

What changed (my slice)

The account connection card is now driven by the canonical connection state derived from a real listProjects response — never from stored bytes alone and never the placeholder token "set". Connected to FlutterFlow is only reachable after a genuine projects endpoint response.

  • app.js: canonical ffConnectionState (not-configured | validating | connected | no-projects | unauthorized | network-error); renderApiKeyConnection() renders the card + a .acct-dot.bad tone; validateFlutterFlowConnection() re-confirms on startup/after save/clear; fetchProjects reports outcomes; endpoint change (setFlutterFlowEndpoint) invalidates the stale stored project + dropdown and re-fetches; account default-project mirrors the same stored value the deploy confirmation pre-selects. Exposed validateFlutterFlowConnection for a visible retry path.
  • index.html: added .acct-dot.bad state token (the only html change).
  • e2e/stu384-connection.spec.js: 7 hermetic browser tests.

Acceptance criteria

  • Save/reload/remove and page/settings entry points share ONE value; masked keys never in screenshots or status labels — the same encrypted storage is written by the modal and read on reload; tests assert the card never contains the raw key or a masked •••• fragment; connected/reload/signed-out tests exercise the single shared value.
  • 401/403 differs from a genuine empty-projects response; endpoint change invalidates stale projects and selection — 401 test vs empty-list test vs network-error test assert three distinct labels/tones; endpoint test proves the stored project id is cleared and the list re-fetched on endpoint switch.
  • Selected target stays consistent with deploy confirmation — the account card's Default project reads flutterflowProjectId = getApiKey("flutterflow_project_id"), the exact value populateConfirmProjectSelect pre-selects; verified by the connected/endpoint tests. (Full deploy-confirm modal cross-assertion is not an automated e2e because it needs a generated candidate; source-level consistency is shared.)
  • npm run test:key-storage passes; browser fixtures exercise valid/invalid keys, failure retry and signed-out entry.

Full CI sequence (all green on final commit)

Step Result
npm test 306 pass / 0 fail
npm run build built in 457ms
npm run test:ui (full suite) 90 passed (incl. 7 STU-384)
npm run test:key-storage Browser key storage checks passed
npm run test:accessibility 7 shells, 360x500 viewport pass
npm run test:deploy-guard 9/9 passed
npm run test:proxies 30/30 proxy path cases passed
bundle leak scan No API key/endpoint leaks found

Falsification evidence (break -> FAIL -> restore -> PASS)

  1. Rejected key never shows Connected — changed the unauthorized render branch to label Connected to FlutterFlow. The 401 spec FAILED at e2e/stu384-connection.spec.js:182 (expect(...).toHaveText("API key rejected (401/403) ...")). Restored -> 2 passed.
  2. Endpoint change invalidates stored selection — removed clearStoredProjectSelection() from invalidateStaleProjectSelection(). The endpoint spec FAILED at :252 (expect(storedAfter).toBeNull()). Restored -> passed.
  3. Save persists the single value — commented out await saveApiKey("flutterflow", ...) in saveApiKeys(). The signed-out spec FAILED at :281 (reload placeholder was not Key saved). Restored -> passed.
  4. Failure retry recovers to connected — broke validateFlutterFlowConnection success branch to not flip state. The network-retry spec FAILED at :211 (final Connected to FlutterFlow never appeared). Restored -> passed.

All four restored edits are byte-identical to the green baseline (grep -c "#BROKE#" app.js -> 0).

Reference screenshots (changed surface)

  • screenshots/stu384-connected-1440x900.png
  • screenshots/stu384-connected-360x800.png

Gaps / not verified

  • Deploy-confirm modal cross-assertion is source-level only (shared flutterflow_project_id); if final acceptance requires a candidate-generation e2e it should be added by the reviewer.
  • Provider keys for Gemini/Anthropic/OpenAI/OpenRouter have no existing production API/storage; their BYOK model selection is preserved (untouched) and no fake 'set' state was rendered for them.

…invalidation

- Add canonical ffConnectionState driven by real listProjects outcomes
  (not-configured/validating/connected/no-projects/unauthorized/network-error).
- Render account connection card from that state; 'Connected' only after an
  actual projects response. Masked/raw keys never appear in status labels.
- Endpoint change invalidates stale project selection and re-fetches.
- Account card default project mirrors the deploy-confirmation stored value.
- Startup validates a stored key so the card is truthful on load.
7 hermetic browser tests covering not-configured, validating->connected,
no-projects vs 401/403 vs network-error, endpoint-change invalidation,
save/reload/remove single-value, and signed-out entry. Expose
validateFlutterFlowConnection for a visible retry path.
1440x900 and 360x800 captures of the account connection card showing a
response-driven Connected state, default project, and endpoint.
@sgardoll
sgardoll marked this pull request as ready for review September 22, 2026 17:06
@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 2/5

The PR is not safe to merge until endpoint-specific project loading, canonical-state request identity, and FlutterFlow-only credential removal are corrected.

Fix All in DevinFindings

  1. P1 Staging Reloads Production Projects ▶
  2. P1 Unsaved Keys Become Canonical ▶
Fix with agent prompt
### Issue 1
app.js:1403
Changing the endpoint clears the selected project and calls `fetchProjects`, but that function creates `FlutterFlowApiClient` without passing `getFlutterFlowEndpoint()`, so the client defaults to Production. Selecting Staging therefore repopulates the dropdown and connection state with Production projects while the account card says “Staging,” allowing users to select a Production project for a Staging configuration.

Greptile automatically discovered a related ticket stating that endpoint changes must invalidate stale projects and reload projects for the selected endpoint, which informed this comment.

### Issue 2
app.js:1498
Blurring a replacement key validates the unsaved input through `fetchProjects`, and any successful request unconditionally sets the global connection state to `connected`. A user can therefore close the modal without saving and still see the account card report that the persisted credential is connected. Because requests have no identity or stale-result guard, an older request can also overwrite the result for a newer credential.

Greptile automatically discovered a related ticket stating that the account card and editor must share one canonical credential value and only show connected after validating that value, which informed this comment.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR introduces response-driven FlutterFlow connection states, clears stale project selection after endpoint changes, updates the account connection presentation, and adds browser coverage for key persistence and connection outcomes. Greptile automatically discovered a related ticket that helped explain the purpose of this PR: make the account connection section the canonical FlutterFlow credential editor with truthful response-derived states.

  • Adds distinct validating, connected, empty-project, authorization-error, and network-error states.
  • Adds endpoint-change invalidation and account-card rendering from shared state.
  • Adds Playwright coverage for persistence, errors, endpoint changes, and signed-out entry.
  • The implementation still reloads Production after selecting Staging, permits unsaved or stale requests to become canonical, and lacks per-key removal.

Reviews (1) · Last reviewed commit: "chore(STU-384): restore untouched STU-38..."

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 5 potential issues.

Devin Review

Comment thread app.js Resolved
Comment thread app.js
Comment on lines +1383 to +1387
function clearStoredProjectSelection() {
flutterflowProjectId = "";
localStorage.removeItem(STORAGE_KEY_PREFIX + "flutterflow_project_id");
sessionStorage.removeItem(SESSION_STORAGE_KEY_PREFIX + "flutterflow_project_id");
}

@devin-ai-integration devin-ai-integration Bot Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Replacement keys retain stale projects

Saving a replacement key without selecting a project retains the old default. saveApiKeys then displays and deploys with a project unrelated to the new key.

Learn more

The project ID is stored independently from the API key. The new helper clears it only when the endpoint changes, while saveApiKeys leaves it untouched when a replacement key produces no selected project. Deployment later falls back to that stored ID when the confirmation dropdown has no selection.

Example: Key A stores project alpha. The user enters key B, whose project list does not contain alpha, and saves without choosing another project. The account card still shows alpha, and a deployment can submit key B with project alpha.

Recommended fix: When a non-empty replacement API key is saved, clear the stored project unless the current dropdown supplies a valid project from that replacement key. Keep the key and project update ordered so a failed save cannot leave a partially updated pair.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines +1479 to +1484
// A real request is in flight; the account connection card shows validating
// until the outcome is known (never "connected" from storage alone).
if (hasStoredKey("flutterflow") || apiKey) {
ffConnectionState = "validating";
}
renderApiKeyConnection();

@devin-ai-integration devin-ai-integration Bot Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Unsaved keys replace connection status

Blurring an unsaved replacement key makes fetchProjects overwrite the stored key's status. Canceling leaves the account card describing the discarded key.

Learn more

The account card is gated by the stored in-memory key, but fetchProjects validates either that key or a newly typed candidate. Its result always writes the shared ffConnectionState. Closing the modal does not revalidate the stored key, so the candidate result persists.

Example: A valid stored key shows Connected. The user types an invalid replacement, blurs the field, then closes without saving. The card reports that the API key was rejected even though the valid stored key remains active.

Recommended fix: Separate candidate-key validation state from the canonical stored-key connection state. Only update ffConnectionState when the request validates the currently stored key; use modal-local state for unsaved input.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines 1491 to +1499
if (!projects || projects.length === 0) {
ffConnectionState = "no-projects";
renderApiKeyConnection();
select.innerHTML = '<option value="">No projects found</option>';
return;
}

ffConnectionState = "connected";
renderApiKeyConnection();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Stale project requests win races

Starting another fetchProjects request does not invalidate the first. A late response can replace the newer connection state and dropdown with stale projects.

Learn more

Opening the modal starts a request for the stored key. Blurring a replacement key or changing the endpoint can start another request before the first finishes. Unlike the deploy confirmation loader, fetchProjects has no token check before any state or dropdown write.

Example: The production request starts first. The user switches to staging, whose request finishes quickly. The slower production response then marks the card connected and replaces the dropdown with production projects.

Recommended fix: Add a monotonically increasing request token or abort controller to fetchProjects. Check it before every ffConnectionState, error, and dropdown update, including catch handling.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines +1366 to +1370
} catch (error) {
const msg = String((error && error.message) || "");
ffConnectionState = /(401|403)|denied|unauthorized|scoped|list permission/i.test(msg)
? "unauthorized"
: "network-error";

@devin-ai-integration devin-ai-integration Bot Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Failure classification depends on prose

Authorization state relies on broad error-message matching. Structured status metadata would keep future client wording changes from altering connection classification.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
}
if (hasStoredKey("flutterflow")) {
ffConnectionState = "validating";
fetchProjects(flutterflowApiKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Staging Reloads Production Projects

Changing the endpoint clears the selected project and calls fetchProjects, but that function creates FlutterFlowApiClient without passing getFlutterFlowEndpoint(), so the client defaults to Production. Selecting Staging therefore repopulates the dropdown and connection state with Production projects while the account card says “Staging,” allowing users to select a Production project for a Staging configuration.

Greptile automatically discovered a related ticket stating that endpoint changes must invalidate stale projects and reload projects for the selected endpoint, which informed this comment.

Source Used: Linear — Implement the account connection and single provider-key editor

Prompt To Fix With AI
This is a comment left during a code review.
Path: app.js
Line: 1403

Comment:
**Staging Reloads Production Projects**

Changing the endpoint clears the selected project and calls `fetchProjects`, but that function creates `FlutterFlowApiClient` without passing `getFlutterFlowEndpoint()`, so the client defaults to Production. Selecting Staging therefore repopulates the dropdown and connection state with Production projects while the account card says “Staging,” allowing users to select a Production project for a Staging configuration.

Greptile automatically discovered a related ticket stating that endpoint changes must invalidate stale projects and reload projects for the selected endpoint, which informed this comment.

**Source Used:** Linear — [Implement the account connection and single provider-key editor](https://linear.app/stuart-gardoll/issue/STU-384/implement-the-account-connection-and-single-provider-key-editor)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex Fix in Devin

Comment thread app.js
return;
}

ffConnectionState = "connected";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Unsaved Keys Become Canonical

Blurring a replacement key validates the unsaved input through fetchProjects, and any successful request unconditionally sets the global connection state to connected. A user can therefore close the modal without saving and still see the account card report that the persisted credential is connected. Because requests have no identity or stale-result guard, an older request can also overwrite the result for a newer credential.

Greptile automatically discovered a related ticket stating that the account card and editor must share one canonical credential value and only show connected after validating that value, which informed this comment.

Source Used: Linear — Implement the account connection and single provider-key editor

Prompt To Fix With AI
This is a comment left during a code review.
Path: app.js
Line: 1498

Comment:
**Unsaved Keys Become Canonical**

Blurring a replacement key validates the unsaved input through `fetchProjects`, and any successful request unconditionally sets the global connection state to `connected`. A user can therefore close the modal without saving and still see the account card report that the persisted credential is connected. Because requests have no identity or stale-result guard, an older request can also overwrite the result for a newer credential.

Greptile automatically discovered a related ticket stating that the account card and editor must share one canonical credential value and only show connected after validating that value, which informed this comment.

**Source Used:** Linear — [Implement the account connection and single provider-key editor](https://linear.app/stuart-gardoll/issue/STU-384/implement-the-account-connection-and-single-provider-key-editor)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex Fix in Devin

…; add remove-leg browser test

- validateFlutterFlowConnection, fetchProjects and populateConfirmProjectSelect
  now pass getFlutterFlowEndpoint() into FlutterFlowApiClient instead of
  relying on the production default, so the account card and project dropdown
  reflect the configured (staging) server like the deploy flow does.
- Endpoint-change test now feeds DIFFERENT lists to production vs staging and
  asserts the staging-only project appears, so it fails if the client falls
  back to production.
- New browser test removes the stored key and asserts the connection card
  returns to not-configured with no raw/masked key material remaining.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Devin Review

Comment thread app.js
apiKey,
"",
"main",
getFlutterFlowEndpoint(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Staging fallback loads production projects

When staging returns 404, listProjects retries its hard-coded production legacy URL. Connection, settings, and deploy selectors can accept production projects while later operations target staging.

Learn more

The configured endpoint now reaches listProjects, but the fallback URL remains fixed to production. A 404 from the staging convention path therefore changes environments rather than trying a staging-compatible path or preserving the failure. Every changed caller uses the same method, so the account check, API-key project selector, and deploy selector all inherit this behavior.

Example: With staging selected, https://api.flutterflow.io/v2-staging/listProjects returns 404. The client then requests https://api.flutterflow.io/v2/l/listProjects, accepts its production project list, and lets the user select a project unavailable on staging.

Recommended fix: Derive any legacy fallback from the configured endpoint, or disable the production legacy fallback whenever baseUrl is not the production endpoint. Add a staging-404 test that ensures no production request occurs.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

The pull_request CI run for the preceding commit was never created (no 'CI'
check-run exists for it, while the sibling Code scanning job did run). Re-pushing
to obtain a CI verdict; no source change.

Unrelated: the 'Code scanning AI findings' job fails with GitHub's own
'CAPIError: 400 The requested model is not supported' from the Copilot autofind
agent. It fails identically on other PRs and on this branch's earlier sha, so it
is not caused by this change.
…ui-ux' into sgardoll/stu-384-implement-the-account-connection-and-single-provider-key

# Conflicts:
#	dist/assets/index-CleimGxe.css
#	dist/assets/index-D6csqwUm.js
#	dist/index.html
@sgardoll
sgardoll merged commit ba53422 into milestone/stu-implement-updated-ui-ux Sep 22, 2026
3 checks passed
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.

1 participant