From f40d9b3945b30dd1cf6049a0f271e6c68552699a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 05:30:56 +0000 Subject: [PATCH 1/3] client(admin): pick primary slug, not auto-generated, when rendering 'the link' Four admin UI sites picked the link's slug via `link.slugs.find(s => !s.is_custom)`, which returns the auto-generated slug specifically. After SlugRepository.setPrimary flips primary to a custom slug (or after a primary custom slug is removed and the auto-slug is auto-promoted back), this diverges from the canonical "primary slug, falling back to slugs[0]" pattern already used at line 1508 and in the redirect path. Switch all four to `s.is_primary` with the same `|| slugs[0]` fallback. For the post-create paths (lines 153, 250) only one slug exists at creation, so behavior is unchanged. For the dashboard recent/top-links rendering (lines 1130, 1166) the chip now reflects the user's chosen primary, matching what shows on the link detail page and what the redirect actually serves. --- src/client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.ts b/src/client.ts index 442e699..7aa6ebc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -150,7 +150,7 @@ function quickShorten() { window.location.href = '/_/admin/links/' + link.id; } } else { - var primary = link.slugs.find(function(s) { return !s.is_custom; }); + var primary = link.slugs.find(function(s) { return s.is_primary; }) || link.slugs[0]; if (primary) copyUrl(primary.slug); toast(t('client.linkCreatedCopied')); window.location.href = '/_/admin/links/' + link.id; @@ -247,7 +247,7 @@ function createDuplicate(url) { api('/links', { method: 'POST', body: JSON.stringify({ url: url, allow_duplicate: true }) }).then(function(res) { if (res.ok) { return res.json().then(function(link) { - var primary = link.slugs.find(function(s) { return !s.is_custom; }); + var primary = link.slugs.find(function(s) { return s.is_primary; }) || link.slugs[0]; if (primary) copyUrl(primary.slug); toast(t('client.linkCreatedCopied')); window.location.href = '/_/admin/links/' + link.id; @@ -1127,7 +1127,7 @@ function pollDashboard() { var link = d.recent_links[ri]; var slug = ''; for (var si = 0; si < link.slugs.length; si++) { - if (!link.slugs[si].is_custom) { slug = link.slugs[si].slug; break; } + if (link.slugs[si].is_primary) { slug = link.slugs[si].slug; break; } } if (!slug && link.slugs.length > 0) slug = link.slugs[0].slug; var a = document.createElement('a'); @@ -1163,7 +1163,7 @@ function pollDashboard() { var tLink = d.top_links[ti]; var tSlug = ''; for (var si = 0; si < tLink.slugs.length; si++) { - if (!tLink.slugs[si].is_custom) { tSlug = tLink.slugs[si].slug; break; } + if (tLink.slugs[si].is_primary) { tSlug = tLink.slugs[si].slug; break; } } if (!tSlug && tLink.slugs.length > 0) tSlug = tLink.slugs[0].slug; var tPct = Math.round((tLink.total_clicks / tlMax) * 100); From acedb4b44cc2eccba9e0eb9e285214414e48199c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 05:44:27 +0000 Subject: [PATCH 2/3] client(admin): centralize primary-slug selection in pickPrimarySlug helper Five call sites picked the link's representative slug, in three different shapes: `find(s => s.is_primary) || slugs[0]`, a manual loop with a separate `!slug && slugs.length > 0` fallback, and an `if (slugs && slugs.length > 0)` wrapper. Collapse all three into one helper so the next slug-semantics drift has a single place to fix. --- src/client.ts | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/client.ts b/src/client.ts index 7aa6ebc..6cbeb3d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -39,6 +39,16 @@ function t(key, params) { return val; } +// The slug to display or copy when "the link" needs one representative chip: +// the link's primary, with slugs[0] as a defensive fallback for malformed data. +function pickPrimarySlug(link) { + if (!link.slugs || link.slugs.length === 0) return null; + for (var i = 0; i < link.slugs.length; i++) { + if (link.slugs[i].is_primary) return link.slugs[i]; + } + return link.slugs[0]; +} + // ---- Toast ---- function toast(msg, type) { var el = document.getElementById('toast'); @@ -150,7 +160,7 @@ function quickShorten() { window.location.href = '/_/admin/links/' + link.id; } } else { - var primary = link.slugs.find(function(s) { return s.is_primary; }) || link.slugs[0]; + var primary = pickPrimarySlug(link); if (primary) copyUrl(primary.slug); toast(t('client.linkCreatedCopied')); window.location.href = '/_/admin/links/' + link.id; @@ -247,7 +257,7 @@ function createDuplicate(url) { api('/links', { method: 'POST', body: JSON.stringify({ url: url, allow_duplicate: true }) }).then(function(res) { if (res.ok) { return res.json().then(function(link) { - var primary = link.slugs.find(function(s) { return s.is_primary; }) || link.slugs[0]; + var primary = pickPrimarySlug(link); if (primary) copyUrl(primary.slug); toast(t('client.linkCreatedCopied')); window.location.href = '/_/admin/links/' + link.id; @@ -1125,11 +1135,8 @@ function pollDashboard() { if (recentNoData) recentNoData.remove(); for (var ri = 0; ri < d.recent_links.length; ri++) { var link = d.recent_links[ri]; - var slug = ''; - for (var si = 0; si < link.slugs.length; si++) { - if (link.slugs[si].is_primary) { slug = link.slugs[si].slug; break; } - } - if (!slug && link.slugs.length > 0) slug = link.slugs[0].slug; + var primarySlug = pickPrimarySlug(link); + var slug = primarySlug ? primarySlug.slug : ''; var a = document.createElement('a'); a.href = '/_/admin/links/' + link.id; a.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;cursor:pointer;overflow:hidden;min-width:0;text-decoration:none;color:inherit'; @@ -1161,11 +1168,8 @@ function pollDashboard() { if (tlNoData) tlNoData.remove(); for (var ti = 0; ti < d.top_links.length; ti++) { var tLink = d.top_links[ti]; - var tSlug = ''; - for (var si = 0; si < tLink.slugs.length; si++) { - if (tLink.slugs[si].is_primary) { tSlug = tLink.slugs[si].slug; break; } - } - if (!tSlug && tLink.slugs.length > 0) tSlug = tLink.slugs[0].slug; + var tPrimary = pickPrimarySlug(tLink); + var tSlug = tPrimary ? tPrimary.slug : ''; var tPct = Math.round((tLink.total_clicks / tlMax) * 100); var a = document.createElement('a'); a.href = '/_/admin/links/' + tLink.id; @@ -1503,11 +1507,8 @@ function showAddLinkToBundlePicker(bundleId, excludeIds) { html += '
' + esc(t('bundles.allLinksAdded')) + '
'; } else { available.forEach(function(link) { - var slug = ''; - if (link.slugs && link.slugs.length > 0) { - var primary = link.slugs.find(function(s) { return s.is_primary; }) || link.slugs[0]; - slug = primary.slug; - } + var primary = pickPrimarySlug(link); + var slug = primary ? primary.slug : ''; var label = link.label || link.url; html += '
'; html += '' + esc(slug) + ''; From f7b9fcd5380e0367f4072f8cb8fc1a646f4d6f91 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 05:55:57 +0000 Subject: [PATCH 3/3] client(admin): pickPrimarySlug returns the slug string, not the slug object Returning the object forced every call site to do `primary.slug` (or `primary ? primary.slug : ''`) and made the helper's contract ambiguous about what shape the result has. Returning the string directly collapses each call site to a single line, makes the empty-slugs case fold cleanly into the falsy check, and removes the misleading "defensive fallback for malformed data" comment. --- src/client.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/client.ts b/src/client.ts index 6cbeb3d..bedb63c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -39,14 +39,14 @@ function t(key, params) { return val; } -// The slug to display or copy when "the link" needs one representative chip: -// the link's primary, with slugs[0] as a defensive fallback for malformed data. +// The slug string to display or copy when "the link" needs one representative +// chip: the link's primary, falling back to the first slug. function pickPrimarySlug(link) { - if (!link.slugs || link.slugs.length === 0) return null; + if (!link.slugs || link.slugs.length === 0) return ''; for (var i = 0; i < link.slugs.length; i++) { - if (link.slugs[i].is_primary) return link.slugs[i]; + if (link.slugs[i].is_primary) return link.slugs[i].slug; } - return link.slugs[0]; + return link.slugs[0].slug; } // ---- Toast ---- @@ -161,7 +161,7 @@ function quickShorten() { } } else { var primary = pickPrimarySlug(link); - if (primary) copyUrl(primary.slug); + if (primary) copyUrl(primary); toast(t('client.linkCreatedCopied')); window.location.href = '/_/admin/links/' + link.id; } @@ -258,7 +258,7 @@ function createDuplicate(url) { if (res.ok) { return res.json().then(function(link) { var primary = pickPrimarySlug(link); - if (primary) copyUrl(primary.slug); + if (primary) copyUrl(primary); toast(t('client.linkCreatedCopied')); window.location.href = '/_/admin/links/' + link.id; }); @@ -1135,8 +1135,7 @@ function pollDashboard() { if (recentNoData) recentNoData.remove(); for (var ri = 0; ri < d.recent_links.length; ri++) { var link = d.recent_links[ri]; - var primarySlug = pickPrimarySlug(link); - var slug = primarySlug ? primarySlug.slug : ''; + var slug = pickPrimarySlug(link); var a = document.createElement('a'); a.href = '/_/admin/links/' + link.id; a.style.cssText = 'display:flex;align-items:center;gap:0.75rem;padding:0.5rem 0;cursor:pointer;overflow:hidden;min-width:0;text-decoration:none;color:inherit'; @@ -1168,8 +1167,7 @@ function pollDashboard() { if (tlNoData) tlNoData.remove(); for (var ti = 0; ti < d.top_links.length; ti++) { var tLink = d.top_links[ti]; - var tPrimary = pickPrimarySlug(tLink); - var tSlug = tPrimary ? tPrimary.slug : ''; + var tSlug = pickPrimarySlug(tLink); var tPct = Math.round((tLink.total_clicks / tlMax) * 100); var a = document.createElement('a'); a.href = '/_/admin/links/' + tLink.id; @@ -1507,8 +1505,7 @@ function showAddLinkToBundlePicker(bundleId, excludeIds) { html += '
' + esc(t('bundles.allLinksAdded')) + '
'; } else { available.forEach(function(link) { - var primary = pickPrimarySlug(link); - var slug = primary ? primary.slug : ''; + var slug = pickPrimarySlug(link); var label = link.label || link.url; html += '
'; html += '' + esc(slug) + '';