From 11d96a87711deb8415ca405724d312128759a2fb Mon Sep 17 00:00:00 2001 From: Samuel Olabode Date: Fri, 7 Aug 2026 09:52:58 -0600 Subject: [PATCH 1/4] feat: add viz_ prefix helper for Vizably-created repos Centralize applyVizablyRepoPrefix so create and availability checks share one idempotent naming convention. --- backend/tests/githubRepoName.test.js | 25 +++++++++++++++++++++++-- shared/githubRepoName.js | 27 ++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/backend/tests/githubRepoName.test.js b/backend/tests/githubRepoName.test.js index 105255f..ce26045 100644 --- a/backend/tests/githubRepoName.test.js +++ b/backend/tests/githubRepoName.test.js @@ -1,9 +1,13 @@ /** - * Unit tests for shared GitHub repo name normalization (#85). + * Unit tests for shared GitHub repository name helpers. */ const test = require('node:test'); const assert = require('node:assert/strict'); -const { normalizeGitHubRepoName } = require('../../shared/githubRepoName'); +const { + normalizeGitHubRepoName, + applyVizablyRepoPrefix, + VIZABLY_REPO_PREFIX, +} = require('../../shared/githubRepoName'); test('normalizeGitHubRepoName trims leading and trailing whitespace', () => { assert.equal(normalizeGitHubRepoName(' vizably-scans '), 'vizably-scans'); @@ -23,3 +27,20 @@ test('normalizeGitHubRepoName returns empty for whitespace-only input', () => { assert.equal(normalizeGitHubRepoName(null), ''); assert.equal(normalizeGitHubRepoName(undefined), ''); }); + +test('applyVizablyRepoPrefix prepends viz_ after normalizing', () => { + assert.equal(applyVizablyRepoPrefix('scans'), 'viz_scans'); + assert.equal(applyVizablyRepoPrefix(' accessibility results '), 'viz_accessibility-results'); + assert.equal(applyVizablyRepoPrefix('reports'), `${VIZABLY_REPO_PREFIX}reports`); +}); + +test('applyVizablyRepoPrefix is idempotent when the prefix is already present', () => { + assert.equal(applyVizablyRepoPrefix('viz_scans'), 'viz_scans'); + assert.equal(applyVizablyRepoPrefix('VIZ_reports'), 'viz_reports'); + assert.equal(applyVizablyRepoPrefix(' viz_my-repo '), 'viz_my-repo'); +}); + +test('applyVizablyRepoPrefix returns empty for blank input', () => { + assert.equal(applyVizablyRepoPrefix(' '), ''); + assert.equal(applyVizablyRepoPrefix(null), ''); +}); diff --git a/shared/githubRepoName.js b/shared/githubRepoName.js index a411cea..02f580e 100644 --- a/shared/githubRepoName.js +++ b/shared/githubRepoName.js @@ -20,4 +20,29 @@ function normalizeGitHubRepoName(name) { .replace(/^-+|-+$/g, ''); } -module.exports = { normalizeGitHubRepoName }; +/** Prefix applied to every repository Vizably creates. */ +const VIZABLY_REPO_PREFIX = 'viz_'; + +/** + * Normalize then ensure the Vizably create-path prefix. + * Idempotent: names that already start with `viz_` (any casing) keep a single prefix. + * + * @param {unknown} name + * @returns {string} + */ +function applyVizablyRepoPrefix(name) { + const normalized = normalizeGitHubRepoName(name); + if (!normalized) { + return ''; + } + if (normalized.toLowerCase().startsWith(VIZABLY_REPO_PREFIX)) { + return `${VIZABLY_REPO_PREFIX}${normalized.slice(VIZABLY_REPO_PREFIX.length)}`; + } + return `${VIZABLY_REPO_PREFIX}${normalized}`; +} + +module.exports = { + VIZABLY_REPO_PREFIX, + normalizeGitHubRepoName, + applyVizablyRepoPrefix, +}; From b8506859d778c2c3cca393a247de200511bf26cd Mon Sep 17 00:00:00 2001 From: Samuel Olabode Date: Fri, 7 Aug 2026 09:53:45 -0600 Subject: [PATCH 2/4] feat: apply viz_ prefix on GitHub create and availability checks StorageService now normalizes create-path names through applyVizablyRepoPrefix so lookups and creates target the same Vizably-namespaced repository. --- backend/services/storageService.js | 6 +-- backend/tests/storageService.test.js | 63 +++++++++++++++++++--------- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/backend/services/storageService.js b/backend/services/storageService.js index 55de330..68537b6 100644 --- a/backend/services/storageService.js +++ b/backend/services/storageService.js @@ -7,7 +7,7 @@ */ const crypto = require('crypto'); const { randomUUID } = require('crypto'); -const { normalizeGitHubRepoName } = require('../../shared/githubRepoName'); +const { applyVizablyRepoPrefix } = require('../../shared/githubRepoName'); const MANIFEST_PATH = 'vizably.json'; /** Pre-rename store root — still loadable; rewritten to `MANIFEST_PATH` on load. */ @@ -155,7 +155,7 @@ class StorageService { * Create a private empty GitHub repo for the signed-in user (App UAT). * Does not initialize a Vizably store — caller runs fit-check then init. * - * @param {string} name repository name (not owner/name) + * @param {string} name repository name (not owner/name); stored as `viz_` * @param {StorageClients} clients must include githubUserClient (or githubClient as UAT) * @param {object} [options] * @param {string} [options.installUrl] App install URL when needsInstall @@ -374,7 +374,7 @@ class StorageService { throw err; } - const normalized = normalizeGitHubRepoName(name); + const normalized = applyVizablyRepoPrefix(name); if (!normalized) { const err = new Error('Repository name is required'); err.status = 400; diff --git a/backend/tests/storageService.test.js b/backend/tests/storageService.test.js index 07abd03..14a645d 100644 --- a/backend/tests/storageService.test.js +++ b/backend/tests/storageService.test.js @@ -309,7 +309,8 @@ test('checkGitHubRepoNameAvailability returns available on 404', async () => { githubUserClient: client, }); assert.equal(result.status, 'available'); - assert.equal(result.full_name, 'sam/fresh-repo'); + assert.equal(result.normalizedName, 'viz_fresh-repo'); + assert.equal(result.full_name, 'sam/viz_fresh-repo'); }); test('checkGitHubRepoNameAvailability returns taken when repo exists', async () => { @@ -323,7 +324,8 @@ test('checkGitHubRepoNameAvailability returns taken when repo exists', async () githubUserClient: client, }); assert.equal(result.status, 'taken'); - assert.match(result.message, /already exists/); + assert.equal(result.normalizedName, 'viz_site-audits'); + assert.match(result.message, /viz_site-audits/); }); test('checkGitHubRepoNameAvailability returns invalid for bad names', async () => { @@ -343,14 +345,15 @@ test('createGitHubRepository creates a private empty repo and returns storageRef { id: 1, contents: 'write', - repos: ['sam/vizably-new'], + repos: ['sam/viz_scans'], }, ], }); - const result = await storageService.createGitHubRepository('vizably-new', { + const result = await storageService.createGitHubRepository('scans', { githubUserClient: client, }); - assert.equal(result.storageRef.full_name, 'sam/vizably-new'); + assert.equal(result.storageRef.full_name, 'sam/viz_scans'); + assert.equal(result.storageRef.name, 'viz_scans'); assert.equal(result.storageRef.id, 'R_kgNew'); assert.equal(result.needsInstall, false); assert.equal(result.installUrl, null); @@ -368,11 +371,12 @@ test('createGitHubRepository sets needsInstall when App cannot write yet', async ], }); const result = await storageService.createGitHubRepository( - 'vizably-new', + 'scans', { githubUserClient: client }, { installUrl: 'https://github.com/apps/vizably/installations/new' }, ); assert.equal(result.needsInstall, true); + assert.equal(result.storageRef.name, 'viz_scans'); assert.equal( result.installUrl, 'https://github.com/apps/vizably/installations/new', @@ -391,10 +395,11 @@ test('createGitHubRepository skips install hop when installation covers all repo }, ], }); - const result = await storageService.createGitHubRepository('vizably-new', { + const result = await storageService.createGitHubRepository('scans', { githubUserClient: client, }); assert.equal(result.needsInstall, false); + assert.equal(result.storageRef.name, 'viz_scans'); }); test('createGitHubRepository surfaces rate limits instead of needsInstall', async () => { @@ -407,13 +412,13 @@ test('createGitHubRepository surfaces rate limits instead of needsInstall', asyn }; const client = createMockGitHubClient({ installationProbeError: probeErr }); await assert.rejects( - () => storageService.createGitHubRepository('vizably-new', { githubUserClient: client }), + () => storageService.createGitHubRepository('scans', { githubUserClient: client }), (err) => { assert.equal(err.code, 'GITHUB_RATE_LIMITED'); assert.equal(err.status, 429); assert.match(err.message, /rate-limited/i); assert.match(err.message, /do not reinstall/i); - assert.equal(err.storageRef?.full_name, 'sam/vizably-new'); + assert.equal(err.storageRef?.full_name, 'sam/viz_scans'); return true; }, ); @@ -425,12 +430,12 @@ test('createGitHubRepository surfaces network failures instead of needsInstall', probeErr.code = 'ENOTFOUND'; const client = createMockGitHubClient({ installationProbeError: probeErr }); await assert.rejects( - () => storageService.createGitHubRepository('vizably-new', { githubUserClient: client }), + () => storageService.createGitHubRepository('scans', { githubUserClient: client }), (err) => { assert.equal(err.code, 'GITHUB_NETWORK_ERROR'); assert.equal(err.status, 503); assert.match(err.message, /network/i); - assert.equal(err.storageRef?.name, 'vizably-new'); + assert.equal(err.storageRef?.name, 'viz_scans'); return true; }, ); @@ -443,7 +448,7 @@ test('createGitHubRepository surfaces auth failures instead of needsInstall', as probeErr.response = { data: { message: 'Bad credentials' }, headers: {} }; const client = createMockGitHubClient({ installationProbeError: probeErr }); await assert.rejects( - () => storageService.createGitHubRepository('vizably-new', { githubUserClient: client }), + () => storageService.createGitHubRepository('scans', { githubUserClient: client }), (err) => { assert.equal(err.code, 'GITHUB_AUTH_FAILED'); assert.equal(err.status, 401); @@ -460,7 +465,7 @@ test('createGitHubRepository surfaces GitHub outages instead of needsInstall', a probeErr.response = { data: { message: 'Server Error' }, headers: {} }; const client = createMockGitHubClient({ installationProbeError: probeErr }); await assert.rejects( - () => storageService.createGitHubRepository('vizably-new', { githubUserClient: client }), + () => storageService.createGitHubRepository('scans', { githubUserClient: client }), (err) => { assert.equal(err.code, 'GITHUB_UNAVAILABLE'); assert.equal(err.status, 503); @@ -474,7 +479,7 @@ test('createGitHubRepository rejects invalid names', async () => { const storageService = new StorageService(); const client = createMockGitHubClient(); await assert.rejects( - () => storageService.createGitHubRepository('sam/vizably-new', { githubUserClient: client }), + () => storageService.createGitHubRepository('sam/scans', { githubUserClient: client }), /name only/, ); await assert.rejects( @@ -487,22 +492,40 @@ test('createGitHubRepository rejects invalid names', async () => { ); }); -test('createGitHubRepository normalizes whitespace before create', async () => { +test('createGitHubRepository prefixes and normalizes whitespace before create', async () => { const storageService = new StorageService(); const client = createMockGitHubClient({ installationProbe: [ { id: 1, contents: 'write', - repos: ['sam/vizably-new'], + repos: ['sam/viz_accessibility-results'], }, ], }); - const result = await storageService.createGitHubRepository(' vizably new ', { + const result = await storageService.createGitHubRepository(' accessibility results ', { githubUserClient: client, }); - assert.equal(result.storageRef.full_name, 'sam/vizably-new'); - assert.equal(result.storageRef.name, 'vizably-new'); + assert.equal(result.storageRef.full_name, 'sam/viz_accessibility-results'); + assert.equal(result.storageRef.name, 'viz_accessibility-results'); +}); + +test('createGitHubRepository does not double-prefix an existing viz_ name', async () => { + const storageService = new StorageService(); + const client = createMockGitHubClient({ + installationProbe: [ + { + id: 1, + contents: 'write', + repository_selection: 'all', + repos: [], + }, + ], + }); + const result = await storageService.createGitHubRepository('viz_reports', { + githubUserClient: client, + }); + assert.equal(result.storageRef.name, 'viz_reports'); }); test('createGitHubRepository maps name-taken conflicts', async () => { @@ -518,7 +541,7 @@ test('createGitHubRepository maps name-taken conflicts', async () => { const client = createMockGitHubClient({ createRepoError: conflict }); await assert.rejects( () => storageService.createGitHubRepository('taken', { githubUserClient: client }), - /already exists/, + /viz_taken/, ); }); From c2c9c497be0730ac4def20f78574ecf5112f9a2c Mon Sep 17 00:00:00 2001 From: Samuel Olabode Date: Fri, 7 Aug 2026 09:56:38 -0600 Subject: [PATCH 3/4] feat: show viz_ prefix in Connect create UI Surface the naming convention in the input, apply it before create, and default the suggested suffix to scans. --- frontend/src/__tests__/connectView.test.jsx | 83 ++++++++++--------- frontend/src/__tests__/githubRepoName.test.js | 23 ++++- frontend/src/data/placeholders.js | 2 +- frontend/src/utils/githubRepoName.js | 21 +++++ frontend/src/views/ConnectView.jsx | 49 +++++++++-- 5 files changed, 128 insertions(+), 50 deletions(-) diff --git a/frontend/src/__tests__/connectView.test.jsx b/frontend/src/__tests__/connectView.test.jsx index 3d91315..cccd9fc 100644 --- a/frontend/src/__tests__/connectView.test.jsx +++ b/frontend/src/__tests__/connectView.test.jsx @@ -32,10 +32,10 @@ function mockClient(overrides = {}) { provider: 'github', storageRef: { id: 'R_kgNew', - name: 'vizably-new', - full_name: 'sam/vizably-new', + name: 'viz_scans', + full_name: 'sam/viz_scans', private: true, - html_url: 'https://github.com/sam/vizably-new', + html_url: 'https://github.com/sam/viz_scans', }, needsInstall: false, installUrl: null, @@ -46,7 +46,7 @@ function mockClient(overrides = {}) { async function typeNewRepoName(value) { fireEvent.click(screen.getByText(/Create a new repository/i)) - const input = screen.getByDisplayValue('vizably-scans') + const input = screen.getByDisplayValue('scans') fireEvent.change(input, { target: { value } }) return input } @@ -196,10 +196,10 @@ describe('ConnectView', () => { it('creates a new repository then validates for init', async () => { const created = { id: 'R_kgNew', - name: 'vizably-new', - full_name: 'sam/vizably-new', + name: 'viz_new', + full_name: 'sam/viz_new', private: true, - html_url: 'https://github.com/sam/vizably-new', + html_url: 'https://github.com/sam/viz_new', } const client = mockClient({ createStorage: vi.fn().mockResolvedValue({ @@ -220,16 +220,16 @@ describe('ConnectView', () => { await waitForRepoPicker(client) - await typeNewRepoName('vizably-new') + await typeNewRepoName('new') expect(await screen.findByText(/is available/i)).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: /create repository/i })) - await waitFor(() => expect(client.createStorage).toHaveBeenCalledWith('vizably-new')) + await waitFor(() => expect(client.createStorage).toHaveBeenCalledWith('viz_new')) await waitFor(() => expect(client.validateStorage).toHaveBeenCalledWith( 'github', - expect.objectContaining({ id: 'R_kgNew', full_name: 'sam/vizably-new' }), + expect.objectContaining({ id: 'R_kgNew', full_name: 'sam/viz_new' }), ), ) expect(await screen.findByText('Ready to set up')).toBeInTheDocument() @@ -242,10 +242,10 @@ describe('ConnectView', () => { err.code = 'GITHUB_RATE_LIMITED' err.storageRef = { id: 'R_kgNew', - name: 'vizably-new', - full_name: 'sam/vizably-new', + name: 'viz_new', + full_name: 'sam/viz_new', private: true, - html_url: 'https://github.com/sam/vizably-new', + html_url: 'https://github.com/sam/viz_new', } const client = mockClient({ createStorage: vi.fn().mockRejectedValue(err), @@ -257,8 +257,8 @@ describe('ConnectView', () => { await waitForRepoPicker(client) fireEvent.click(screen.getByText(/Create a new repository/i)) - fireEvent.change(screen.getByDisplayValue('vizably-scans'), { - target: { value: 'vizably-new' }, + fireEvent.change(screen.getByDisplayValue('scans'), { + target: { value: 'new' }, }) // Availability must resolve so Create is enabled. expect(await screen.findByText(/is available/i)).toBeInTheDocument() @@ -268,16 +268,16 @@ describe('ConnectView', () => { expect(screen.queryByText(/Open GitHub App install/i)).not.toBeInTheDocument() }) - it('normalizes whitespace in the repository name before create', async () => { + it('normalizes whitespace and applies viz_ before create', async () => { const client = mockClient({ createStorage: vi.fn().mockResolvedValue({ provider: 'github', storageRef: { id: 'R_kgNew', - name: 'vizably-new', - full_name: 'sam/vizably-new', + name: 'viz_accessibility-results', + full_name: 'sam/viz_accessibility-results', private: true, - html_url: 'https://github.com/sam/vizably-new', + html_url: 'https://github.com/sam/viz_accessibility-results', }, needsInstall: false, installUrl: null, @@ -288,11 +288,11 @@ describe('ConnectView', () => { }), checkRepoNameAvailability: vi.fn().mockResolvedValue({ provider: 'github', - name: 'vizably-new', - normalizedName: 'vizably-new', - full_name: 'sam/vizably-new', + name: 'viz_accessibility-results', + normalizedName: 'viz_accessibility-results', + full_name: 'sam/viz_accessibility-results', status: 'available', - message: 'sam/vizably-new is available.', + message: 'sam/viz_accessibility-results is available.', }), }) @@ -302,14 +302,16 @@ describe('ConnectView', () => { await waitForRepoPicker(client) fireEvent.click(screen.getByText(/Create a new repository/i)) - fireEvent.change(screen.getByDisplayValue('vizably-scans'), { - target: { value: ' vizably new ' }, + fireEvent.change(screen.getByDisplayValue('scans'), { + target: { value: ' accessibility results ' }, }) expect(await screen.findByText(/is available/i)).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: /create repository/i })) - await waitFor(() => expect(client.createStorage).toHaveBeenCalledWith('vizably-new')) - expect(screen.getByDisplayValue('vizably-new')).toBeInTheDocument() + await waitFor(() => + expect(client.createStorage).toHaveBeenCalledWith('viz_accessibility-results'), + ) + expect(screen.getByDisplayValue('accessibility-results')).toBeInTheDocument() }) it('keeps focus on the repository name input while typing', async () => { @@ -321,17 +323,17 @@ describe('ConnectView', () => { await waitForRepoPicker(client) fireEvent.click(screen.getByText(/Create a new repository/i)) - const input = screen.getByDisplayValue('vizably-scans') + const input = screen.getByDisplayValue('scans') input.focus() expect(document.activeElement).toBe(input) - fireEvent.change(input, { target: { value: 'v' } }) + fireEvent.change(input, { target: { value: 's' } }) expect(document.activeElement).toBe(input) - fireEvent.change(input, { target: { value: 'vi' } }) + fireEvent.change(input, { target: { value: 'sc' } }) expect(document.activeElement).toBe(input) - fireEvent.change(input, { target: { value: 'viz' } }) + fireEvent.change(input, { target: { value: 'sca' } }) expect(document.activeElement).toBe(input) - expect(input).toHaveValue('viz') + expect(input).toHaveValue('sca') }) it('shows taken status and blocks create for an existing name', async () => { @@ -339,11 +341,11 @@ describe('ConnectView', () => { // Name is taken on GitHub but not in the local picker list. checkRepoNameAvailability: vi.fn().mockResolvedValue({ provider: 'github', - name: 'already-taken', - normalizedName: 'already-taken', - full_name: 'sam/already-taken', + name: 'viz_already-taken', + normalizedName: 'viz_already-taken', + full_name: 'sam/viz_already-taken', status: 'taken', - message: 'A repository named "already-taken" already exists on your account.', + message: 'A repository named "viz_already-taken" already exists on your account.', }), }) @@ -365,10 +367,10 @@ describe('ConnectView', () => { provider: 'github', storageRef: { id: 'R_kgNew', - name: 'vizably-new', - full_name: 'sam/vizably-new', + name: 'viz_new', + full_name: 'sam/viz_new', private: true, - html_url: 'https://github.com/sam/vizably-new', + html_url: 'https://github.com/sam/viz_new', }, needsInstall: true, installUrl: 'https://github.com/apps/vizably/installations/new', @@ -380,7 +382,7 @@ describe('ConnectView', () => { ) await waitForRepoPicker(client) - await typeNewRepoName('vizably-new') + await typeNewRepoName('new') expect(await screen.findByText(/is available/i)).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: /create repository/i })) @@ -389,5 +391,6 @@ describe('ConnectView', () => { 'https://github.com/apps/vizably/installations/new', ) expect(screen.getByText(/I've added it — refresh/i)).toBeInTheDocument() + expect(client.createStorage).toHaveBeenCalledWith('viz_new') }) }) diff --git a/frontend/src/__tests__/githubRepoName.test.js b/frontend/src/__tests__/githubRepoName.test.js index d99ce4a..eb312cf 100644 --- a/frontend/src/__tests__/githubRepoName.test.js +++ b/frontend/src/__tests__/githubRepoName.test.js @@ -1,5 +1,9 @@ -import { describe, it, expect } from 'vitest' -import { normalizeGitHubRepoName } from '../utils/githubRepoName' +import { describe, expect, it } from 'vitest' +import { + applyVizablyRepoPrefix, + normalizeGitHubRepoName, + VIZABLY_REPO_PREFIX, +} from '../utils/githubRepoName' describe('normalizeGitHubRepoName', () => { it('trims leading and trailing whitespace', () => { @@ -19,3 +23,18 @@ describe('normalizeGitHubRepoName', () => { expect(normalizeGitHubRepoName(' \t ')).toBe('') }) }) + +describe('applyVizablyRepoPrefix', () => { + it('prepends viz_ after normalizing', () => { + expect(applyVizablyRepoPrefix('scans')).toBe('viz_scans') + expect(applyVizablyRepoPrefix(' accessibility results ')).toBe( + 'viz_accessibility-results', + ) + expect(applyVizablyRepoPrefix('reports')).toBe(`${VIZABLY_REPO_PREFIX}reports`) + }) + + it('does not double-prefix when viz_ is already present', () => { + expect(applyVizablyRepoPrefix('viz_scans')).toBe('viz_scans') + expect(applyVizablyRepoPrefix('VIZ_reports')).toBe('viz_reports') + }) +}) diff --git a/frontend/src/data/placeholders.js b/frontend/src/data/placeholders.js index 76caa6f..0b2dc65 100644 --- a/frontend/src/data/placeholders.js +++ b/frontend/src/data/placeholders.js @@ -9,7 +9,7 @@ export const PROVIDERS = { name: 'GitHub', store: 'a private GitHub repo', storeShort: 'GitHub repo', - dest: 'vizably-scans', + dest: 'scans', destIcon: 'GitBranch', unit: 'repository', unitShort: 'repo', diff --git a/frontend/src/utils/githubRepoName.js b/frontend/src/utils/githubRepoName.js index 4474253..556af79 100644 --- a/frontend/src/utils/githubRepoName.js +++ b/frontend/src/utils/githubRepoName.js @@ -12,3 +12,24 @@ export function normalizeGitHubRepoName(name) { .replace(/-{2,}/g, '-') .replace(/^-+|-+$/g, '') } + +/** Prefix applied to every repository Vizably creates. */ +export const VIZABLY_REPO_PREFIX = 'viz_' + +/** + * Normalize then ensure the Vizably create-path prefix. + * Idempotent: names that already start with `viz_` (any casing) keep a single prefix. + * + * @param {unknown} name + * @returns {string} + */ +export function applyVizablyRepoPrefix(name) { + const normalized = normalizeGitHubRepoName(name) + if (!normalized) { + return '' + } + if (normalized.toLowerCase().startsWith(VIZABLY_REPO_PREFIX)) { + return `${VIZABLY_REPO_PREFIX}${normalized.slice(VIZABLY_REPO_PREFIX.length)}` + } + return `${VIZABLY_REPO_PREFIX}${normalized}` +} diff --git a/frontend/src/views/ConnectView.jsx b/frontend/src/views/ConnectView.jsx index 1e74e5f..1515420 100644 --- a/frontend/src/views/ConnectView.jsx +++ b/frontend/src/views/ConnectView.jsx @@ -3,7 +3,10 @@ import { Button, Card } from '../design-system' import { Ico, GoogleMark } from '../lib/icons' import { apiClient } from '../lib/apiClient' import { PROVIDERS } from '../data/placeholders' -import { normalizeGitHubRepoName } from '../utils/githubRepoName' +import { + applyVizablyRepoPrefix, + VIZABLY_REPO_PREFIX, +} from '../utils/githubRepoName' const STATUS_UI = { loadable: { @@ -73,10 +76,13 @@ function storageRefFromRepo(repo) { function findRepoByName(storages, name) { const trimmed = name.trim() if (!trimmed) return null + const prefixed = applyVizablyRepoPrefix(trimmed) || trimmed return ( storages.find((r) => r.name === trimmed) || + storages.find((r) => r.name === prefixed) || storages.find((r) => r.full_name === trimmed) || storages.find((r) => r.full_name.endsWith(`/${trimmed}`)) || + storages.find((r) => r.full_name.endsWith(`/${prefixed}`)) || null ) } @@ -365,12 +371,13 @@ export default function ConnectView({ (awaitingCreate ? nameUnavailable : confirmBlocked) const handleCreateRepo = async () => { - const name = normalizeGitHubRepoName(newRepoName) + const name = applyVizablyRepoPrefix(newRepoName) if (!name || creating || nameUnavailable) return - // Reflect normalized name in the input so users see what will be created. - if (name !== newRepoName) { - setNewRepoName(name) + // Keep the editable suffix in sync with whitespace normalization. + const suffix = name.slice(VIZABLY_REPO_PREFIX.length) + if (suffix !== newRepoName) { + setNewRepoName(suffix) } setCreating(true) @@ -675,6 +682,17 @@ export default function ConnectView({ }} > {Ico(pv.destIcon, 16)} + e.stopPropagation()} @@ -684,9 +702,10 @@ export default function ConnectView({ setNameAvailability(null) // Only clear create/install state when the typed name no longer // matches the repo we just created — avoids extra re-render churn. + const nextPrefixed = applyVizablyRepoPrefix(next) if ( createdStorageRef && - next.trim() !== createdStorageRef.name && + nextPrefixed !== createdStorageRef.name && next.trim() !== createdStorageRef.full_name ) { setCreatedStorageRef(null) @@ -695,7 +714,8 @@ export default function ConnectView({ } }} disabled={creating} - aria-describedby="repo-name-availability" + aria-describedby="repo-name-availability repo-name-prefix-hint" + placeholder="scans" style={{ flex: 1, border: 'none', @@ -775,6 +795,21 @@ export default function ConnectView({

)} +

+ Vizably creates the repo as{' '} + + {applyVizablyRepoPrefix(newRepoName) || `${VIZABLY_REPO_PREFIX}…`} + {' '} + so storage repos are easy to recognize. +

Date: Fri, 7 Aug 2026 09:58:40 -0600 Subject: [PATCH 4/4] docs: document viz_ prefix for Vizably-created GitHub repos Record the create-path naming convention in the auth/storage guide so Connect UI and API behavior stay aligned with the contract. --- .../githubGoogleAuthStorageImplementation.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/guides/auth_storage_guide/githubGoogleAuthStorageImplementation.md b/docs/guides/auth_storage_guide/githubGoogleAuthStorageImplementation.md index 5c05742..9a38d4e 100644 --- a/docs/guides/auth_storage_guide/githubGoogleAuthStorageImplementation.md +++ b/docs/guides/auth_storage_guide/githubGoogleAuthStorageImplementation.md @@ -83,7 +83,10 @@ scans/index scans/ dir (init or cancel) The user can also choose **"Create a new repo/folder"** instead of selecting an existing one — that is just the `initializable` path against a freshly created -store. +store. For GitHub, Vizably always creates the repository as `viz_` +(for example `viz_scans`, `viz_reports`) so Vizably-managed storage is easy to +recognize and harder to confuse with unrelated repos. The prefix is applied on +create and name-availability checks; selecting an existing repo is unchanged. ### Identity model — read this first @@ -539,8 +542,8 @@ await octokit.repos.listForAuthenticatedUser({ visibility: 'all', per_page: 100 // Existence / fit-check read — get the manifest blob await octokit.repos.getContent({ owner, repo, path: 'vizably.json' }); // 404 ⇒ no manifest -// Create a repo for the "new" path -await octokit.repos.createForAuthenticatedUser({ name, private: true }); +// Create a repo for the "new" path — name is always `viz_` +await octokit.repos.createForAuthenticatedUser({ name: 'viz_scans', private: true }); // Atomic-ish write (pass sha to update; omit to create) await octokit.repos.createOrUpdateFileContents({ owner, repo, path, message, content, branch, sha });