Skip to content

Make the Share / Sync website action the sync-entry default; remove the Labs card #3359

Closed as not planned

Description

@mstomar125

Summary

Make core's website entry action the default content of the database.sync-entry slot from #3358, as ADR-084 §1 (proposed) specifies:

  • a "Share / Sync this database…" button in Settings → Database;
  • a dialog with one sentence, Continue on nodespace.ai and Cancel;
  • Continue opens the constant URL https://nodespace.ai/pro?entry=sync-database in the system browser;
  • if the browser can't be opened, ADR-084's failure message, with the address selectable and a Copy link button.

In the same PR:

  • delete the disabled Labs "Team synchronization" card and its mailto: link;
  • replace the README's "Pro tier" section with a plain pointer to the website.

This action is the one place where core's UI mentions the paid product (ADR-081 §2b, proposed).

Context

The spec (ADR-084 §1):

  • The dialog sentence says that syncing and sharing need NodeSpace Pro, which is set up on the website and replaces this app while keeping its databases.
  • The URL comes only from compile-time constants.
  • Core adds no database id, node id, email, person id, app version or machine identifier.
  • Core makes no network request of its own (no reachability pre-check, no analytics), and does not record that the action was used.
  • On an opener rejection, core shows "Couldn't open your browser. To set up sync, visit https://nodespace.ai/pro", with the address selectable and a Copy link button. It retries nothing.
  • The action is shown on every platform core ships on.
  • A unit test pins the exact URL string.

Opening URLs already works.

  • openUrl() in packages/desktop-app/src/lib/utils/external-links.ts (lines 28-44 at af8ddb61) uses @tauri-apps/plugin-opener under Tauri, and window.open(..., 'noopener,noreferrer') in browser mode.
  • opener:default in src-tauri/capabilities/default.json already allows https:// URLs, so no capability change is needed.

Pattern for a dialog opened from two places. stores/import-modal.svelte.ts, plus <ImportOptionsModal bind:open={importModalStore.open} /> mounted once in app-shell.svelte. Settings and the File menu both call importModalStore.show(). #3360 reuses the same approach for the menu.

Clipboard. code-block-node.svelte checks window.navigator?.clipboard before calling writeText. Follow it.

The Labs card is the <!-- Team synchronization --> comment and its <Card> in labs-settings.svelte (lines 53-75 at af8ddb61). labs-settings.test.ts covers it in:

  • the doc comment (1-10);
  • the syncEnabled resets (22, 29);
  • the three-entry test (33-43);
  • five tests (105-162).

The README section is "### Pro tier" in README.md (lines 47-49). The Discord links on line 7 and under "Community" stay.

Ratchet. #3302 counts these markers on the lines this PR deletes:

  • labs-settings.svelte 62-63: cloudBindState ×2;
  • labs-settings.test.ts: cloudBindState ×6, proWording ×1, proSyncModule ×1;
  • README 47-49: proWording ×2, editionBranching ×1, tenantWording ×1.

The new explanation sentence adds one proWording line. That is the entry-point copy ADR-081 §2b allows, and the net count still goes down. Use the CLI's printed numbers, not these.

Scope

  1. New packages/desktop-app/src/lib/services/sync-entry.ts. Its module doc cites ADR-081 §2b and ADR-084 §1, and says the URL must stay built from constants.

    export const SYNC_ENTRY_URL = 'https://nodespace.ai/pro?entry=sync-database';
    export const SYNC_ENTRY_FAILURE_ADDRESS = 'https://nodespace.ai/pro';
    export const SYNC_ENTRY_LABEL = 'Share / Sync this database…';
    export const SYNC_ENTRY_TITLE = 'Share / Sync this database';
    export const SYNC_ENTRY_EXPLANATION = '<one sentence, ADR-084 §1; see below>';
    export const SYNC_ENTRY_CONTINUE_LABEL = 'Continue on nodespace.ai';
    export const SYNC_ENTRY_FAILURE_TEXT = "Couldn't open your browser. To set up sync, visit";
    export async function openSyncEntryUrl(): Promise<boolean>;
    • The URL. Take its form from ADR-084 decision 1 as accepted. The recommended option, (b), is the URL above. Under option (a), use https://nodespace.ai/pro with no parameter.
    • The explanation. Unless ADR-084 as merged gives other text, use: "Syncing and sharing need NodeSpace Pro, which you set up on nodespace.ai and which replaces this app while keeping your databases." Keep it on one line: it is the one sanctioned "Pro" line in core's UI.
    • openSyncEntryUrl() takes no arguments, so nothing can be appended to the URL.
      • It awaits openUrl(SYNC_ENTRY_URL) and returns true.
      • On a rejection, it calls log.warn('Could not open the browser for the sync entry', err) through createLogger('SyncEntry') and returns false.
      • It never throws, logs nothing on success, and writes no state.
  2. New packages/desktop-app/src/lib/stores/sync-entry-dialog.svelte.ts, following import-modal.svelte.ts (an ADR-049 rune store):

    • open = $state(false), failed = $state(false) and copied = $state(false);
    • show() resets failed and copied, then sets open = true;
    • export the singleton as syncEntryDialog.
  3. New packages/desktop-app/src/lib/components/settings/sync-entry-dialog.svelte. Build it with the Dialog.* and Button components, like the Remove dialog in database-settings.svelte. It takes a bindable open prop.

    • Dialog.Title shows SYNC_ENTRY_TITLE, and Dialog.Description shows SYNC_ENTRY_EXPLANATION.
    • While failed is set, show a role="alert" block, styled like the databaseStore.error alert in database-settings.svelte. It contains:
      • SYNC_ENTRY_FAILURE_TEXT;
      • SYNC_ENTRY_FAILURE_ADDRESS in a <span class="select-all font-mono">;
      • a Copy link button. It writes the address with navigator.clipboard.writeText, guarded as in code-block-node.svelte, and then shows "Copied". If the write fails, it logs a warning and the address stays selectable.
    • Dialog.Footer has two buttons:
      • Cancel (variant="ghost") closes the dialog.
      • Continue on nodespace.ai (the default variant) awaits openSyncEntryUrl(). On true it closes the dialog; on false it sets failed. Nothing retries automatically, and Continue stays usable.
    • Use design tokens only, and no {@html}.
  4. packages/desktop-app/src/lib/components/layout/app-shell.svelte: mount <SyncEntryDialog bind:open={syncEntryDialog.open} /> once, next to <ImportOptionsModal ...>. Add a one-line comment that the Settings action and the File menu share this instance.

  5. New packages/desktop-app/src/lib/components/settings/sync-entry-action.svelte: <Button variant="outline" size="sm" onclick={() => syncEntryDialog.show()}>{SYNC_ENTRY_LABEL}</Button>. It takes the slot props and ignores them.

  6. database-settings.svelte: the syncEntryDefault snippet from Add the replaceable database.sync-entry action slot to the extension API #3358 renders <SyncEntryAction />.

  7. packages/desktop-app/src/lib/components/settings/sections/labs-settings.svelte: delete the <!-- Team synchronization --> comment and its <Card>. Keep the labsFlags import, which AI Chat and Playbooks still use.

  8. README.md: replace "### Pro tier" and its paragraph with:

    Sync and sharing

    NodeSpace keeps your data on your machine. To sync a database across your devices or share it with your team, choose Share / Sync this database… in Settings → Database, or visit nodespace.ai/pro. You start a trial or a plan there and download the edition with sync, which replaces this app and keeps your local databases.

    Don't mention nodespaced-pro, "the same desktop app", tenants, Discord-for-access or "end-to-end encrypted". Keep the Discord links on line 7 and under "Community".

Out of scope

Steps

  1. Run the startup sequence from CLAUDE.md, with EnterWorktree({name: "issue-<N>-sync-entry-action"}):
    • bun install;
    • bun run test for a baseline;
    • bun run gh:comment <N> "Frontend: X passed", bun run gh:assign <N> "@me", bun run gh:status <N> "In Progress".
  2. Pre-checks. If any fails, stop and report it:
  3. Write sync-entry.ts, the store, the dialog and the action, with their tests.
  4. Mount the dialog in app-shell.svelte, and fill the slot default in database-settings.svelte.
  5. Delete the Labs card and update labs-settings.test.ts.
  6. Rewrite the README section.
  7. Run bun run scripts/check-pro-boundary.ts and paste the printed BASELINES.
  8. Finish:
    • bun run test:changed;
    • bun run --cwd packages/desktop-app quality:fix, then commit;
    • git push origin HEAD:issue-<N>-sync-entry-action;
    • gh pr create --repo NodeSpaceAI/nodespace-core --base main --head issue-<N>-sync-entry-action. In the PR description:
      • name the explanation line as the ADR-081 §2b entry-point copy;
      • ask nodespace-sync to enable its contribution in the same change that bumps its pin past this PR.
    • After the user approves: bun run merge <PR#> in the background, then ExitWorktree({action: "remove", discard_changes: true}).

Standards:

  • Use createLogger, never console.*.
  • No lint suppression, no any, no {@html}.
  • Use bun run test, never bun test.
  • Mock Tauri and $lib/utils/external-links with vi.mock.
  • Code comments cite ADR-081 and ADR-084, never issue numbers.
  • No Pro, tenant, Supabase or nodespace-sync wording in comments. The explanation constant is the only "Pro" line.

Acceptance criteria

  • When no extension contributes to database.sync-entry, Settings → Database shows "Share / Sync this database…" as the last item of the Databases header row.
  • Clicking it opens a dialog with the ADR-084 sentence, Continue on nodespace.ai and Cancel. Cancel closes the dialog with no side effect.
  • Continue calls openUrl exactly once, with https://nodespace.ai/pro?entry=sync-database. The URL has one query parameter, entry, and no version or identifier.
  • Nothing in the flow calls fetch, invoke or localStorage, and nothing is logged on success.
  • If openUrl rejects, the dialog shows the failure text and https://nodespace.ai/pro in a selectable element, and Copy link writes that address to the clipboard. Nothing throws, and nothing retries.
  • With a fixture contribution registered to the slot, core's button does not render. This checks Add the replaceable database.sync-entry action slot to the extension API #3358's rule end to end.
  • The dialog is mounted once, in app-shell.svelte.
  • Labs renders exactly two cards, AI Chat and Playbooks. git grep -n "developer@nodespace.ai\|Team synchronization" -- packages/desktop-app/src returns nothing.
  • The README section is replaced as described. git grep -n "nodespaced-pro\|Pro tier" -- README.md returns nothing, and the Discord community links remain.
  • bun run scripts/check-pro-boundary.ts exits 0, and BASELINES are lowered to the printed counts.
  • bun run test:changed and quality:fix are clean, and quality:design-tokens reports no new violation.

Tests and verification

  • New src/tests/services/sync-entry.test.ts. Mock $lib/utils/external-links and the logger. Cover:
    • SYNC_ENTRY_URL equals the string literal 'https://nodespace.ai/pro?entry=sync-database', and [...new URL(SYNC_ENTRY_URL).searchParams.keys()] equals ['entry'];
    • SYNC_ENTRY_FAILURE_ADDRESS equals the literal 'https://nodespace.ai/pro';
    • openSyncEntryUrl() calls openUrl once with the literal and resolves true;
    • a rejected openUrl resolves false, logs one warning, and doesn't throw;
    • openSyncEntryUrl.length === 0.
  • New src/tests/components/settings/sync-entry-dialog.test.ts. Spy on globalThis.fetch, and stub navigator.clipboard with Object.defineProperty. Cover:
    • when open, the title, the explanation constant and both buttons render;
    • Cancel closes the dialog, and openUrl is not called;
    • Continue calls openUrl with the literal and closes the dialog;
    • a rejection renders the role="alert" block, with the address in a .select-all element;
    • Copy link calls writeText('https://nodespace.ai/pro') and shows "Copied";
    • a missing clipboard API doesn't throw;
    • fetch is never called.
  • src/tests/components/settings/database-settings.test.ts:
    • the header row's buttons are ['New', 'Open existing…', 'Share / Sync this database…'];
    • clicking the last one sets syncEntryDialog.open;
    • with the fixture contribution registered, that button is absent.
  • src/tests/components/settings/labs-settings.test.ts:
    • rewrite the doc comment (1-10) to describe two entries, with no Pro or proSync wording;
    • drop the labsFlags.syncEnabled = false resets (22, 29);
    • turn 33-43 into "renders exactly two entries: AI Chat, Playbooks" (toHaveLength(2));
    • delete the five Team synchronization tests (105-162);
    • add "has no mailto link": container.querySelector('a[href^="mailto:"]') is null.
  • Mutation checks. Each must fail a test; revert it afterwards:
    • change one character of SYNC_ENTRY_URL;
    • append &v=1 to it;
    • make openSyncEntryUrl rethrow;
    • drop the failed branch;
    • make Copy link copy SYNC_ENTRY_URL;
    • leave the Labs card in.
  • Commands:
    • bun run --cwd packages/desktop-app test src/tests/services/sync-entry.test.ts src/tests/components/settings
    • bun run test:changed
    • bun run --cwd packages/desktop-app quality:fix
    • bun run scripts/check-pro-boundary.ts
  • Manual check. Run bun run tauri:dev against an isolated NODESPACE_HOME, never the live ~/.nodespace:
    1. Open Settings → Database, click the action, then click Continue. The default browser opens the entry URL.
    2. Cancel does nothing.
    3. Settings → Labs shows two cards.
    4. In bun run dev:browser, Continue opens a new tab.

Dependencies

Risks and rollback

  • A paying user sees the purchase action if nodespace-sync bumps its pin past this PR without enabling its contribution. The SYNC gate and the note in the PR description cover this.
  • Users land on a 404 or the home page if the site isn't ready. The WEB gate and the human pre-check cover this.
  • Rollback. Revert the PR. This restores the Labs card and the old README, with no data or schema impact. The slot from Add the replaceable database.sync-entry action slot to the extension API #3358 stays.

Plan key: C9.1b (pro-boundary DAG).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Labels

pro-boundaryPro/core boundary: Pro code moves to nodespace-sync (ADR-081..085)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions