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)}
+
+ {VIZABLY_REPO_PREFIX}
+
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 });