fix(frontend): respect disable_hub in both hub-project entry points

An instance with the hub turned off still got the catalogue preloaded on
every empty home and an "Import a hub project" entry in the create menu
— an outbound request the operator has said not to make, and a door to
somewhere unreachable. Both now observe `disableHubStore`, the store the
script and flow hub pickers already read. With the hub off the caption
reads "Create a new one." rather than continuing a sentence whose first
half is gone.

The telemetry disclosure also scoped the create menu and picker counters
to the empty home, when both fire from the toolbar in a populated one,
and said a creation was recorded when what is recorded is the menu
opening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012fRjnaHLwjpHN84gNNxah9
This commit is contained in:
Guilhem Lemouel
2026-09-07 10:08:58 +02:00
co-authored by Claude Opus 5
parent 96c7b3cee2
commit 029dae0488
3 changed files with 54 additions and 44 deletions
@@ -1073,9 +1073,9 @@
are started for, whether AI chat skills are turned on or off and how often one is
loaded, whether SSO logins evaluate an IdP groups claim (SAML or OIDC) and change a
membership, the plan tier and quota shown when the execution meter is opened, how often
an empty workspace home is seen and its create menu or hub-project picker opened, and
which home-page entry point a new item is created from, the name of any public hub
project imported from it and how far that import got, last 30 days)</li
an empty workspace home is seen, how often the home pages create menu and hub-project
picker are opened and from which entry point, and the name of any public hub project
imported from the home page and how far that import got, last 30 days)</li
>
<li
>feature adoption (counts of which flow, script, trigger and worker features your
@@ -1130,9 +1130,9 @@
are started for, whether AI chat skills are turned on or off and how often one is
loaded, whether SSO logins evaluate an IdP groups claim (SAML or OIDC) and change a
membership, the plan tier and quota shown when the execution meter is opened, how often
an empty workspace home is seen and its create menu or hub-project picker opened, and
which home-page entry point a new item is created from, the name of any public hub
project imported from it and how far that import got, last 30 days)</li
an empty workspace home is seen, how often the home pages create menu and hub-project
picker are opened and from which entry point, and the name of any public hub project
imported from the home page and how far that import got, last 30 days)</li
>
<li
>feature adoption (counts of which flow, script, trigger and worker features your
@@ -16,7 +16,7 @@
} from '$lib/gen'
import { resource } from 'runed'
import { getDraftItems } from '$lib/workspaceDrafts.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { disableHubStore, userStore, workspaceStore } from '$lib/stores'
import type uFuzzy from '@leeoniya/ufuzzy'
import {
ArrowDownUp,
@@ -1813,7 +1813,11 @@
whose direct-deploy protection cleared showEditButtons (NoDirectDeployAlert), since
the menu itself does no permission check. -->
{#if !$userStore?.operator && showEditButtons}
<CreateActionsMenu onImportHubProject={() => (hubPickerOpen = true)} />
<!-- No hub entry where the instance has the hub turned off: the same setting the
script and flow hub pickers observe. -->
<CreateActionsMenu
onImportHubProject={$disableHubStore ? undefined : () => (hubPickerOpen = true)}
/>
{/if}
</div>
</div>
@@ -3,7 +3,7 @@
import { logFeatureUsage } from '$lib/utils/featureUsage'
import Popover from '$lib/components/meltComponents/Popover.svelte'
import { preloadHubProjects, type HubProjectPick } from '$lib/hubProject'
import { workspaceStore } from '$lib/stores'
import { disableHubStore, workspaceStore } from '$lib/stores'
import CreateActionsMenu from './CreateActionsMenu.svelte'
import HubTemplatePicker from './HubTemplatePicker.svelte'
@@ -32,9 +32,10 @@
// Warmed as soon as the empty state renders rather than on the first click: the
// catalogue is one request for the whole hub, and paying for it here is what makes
// the picker open on content.
// the picker open on content. Not on an instance with the hub turned off, where the
// request is one the operator has said not to make.
$effect(() => {
if ($workspaceStore) preloadHubProjects($workspaceStore)
if ($workspaceStore && !$disableHubStore) preloadHubProjects($workspaceStore)
})
</script>
@@ -65,38 +66,43 @@
class="border-t border-dashed border-border-light px-4 pb-[22px] pt-[18px] text-center text-[13.5px] leading-relaxed text-hint"
>
Your scripts, flows and apps will show up here.
<!-- Opens downward into the page rather than upward into the hero: the caption sits high
when the AI composer is hidden, so the room is below it. `fitViewport` caps the box on
a short viewport, which is why the height below is definite and the list inside fills
it — a squeezed box with a fixed-height list inside overflows its own frame. -->
<Popover
floatingConfig={{
placement: 'bottom',
strategy: 'absolute',
gutter: 8,
overflowPadding: 16,
flip: { fallbackPlacements: ['top', 'bottom-start', 'top-start'] },
fitViewport: true,
overlap: false
}}
contentClasses="p-0 flex"
contentStyle="height: min(72vh, 520px);"
class="border-b border-transparent text-accent hover:border-accent"
triggerAttrs={{ 'aria-label': 'Start from a template' }}
on:openChange={(e) =>
e.detail && logFeatureUsage('home', 'template_picker_open', { key: 'empty_state' })}
>
{#snippet trigger()}Start from a template{/snippet}
{#snippet content({ close })}
<HubTemplatePicker
onPick={(project) => {
close()
onPick(project)
}}
/>
{/snippet}
</Popover>
or
<!-- The hub half goes when the instance has the hub turned off, and the remaining link
opens the sentence instead of continuing it. -->
{#if !$disableHubStore}
<!-- Opens downward into the page rather than upward into the hero: the caption sits
high when the AI composer is hidden, so the room is below it. `fitViewport` caps
the box on a short viewport, which is why the height below is definite and the
list inside fills it — a squeezed box with a fixed-height list inside overflows
its own frame. -->
<Popover
floatingConfig={{
placement: 'bottom',
strategy: 'absolute',
gutter: 8,
overflowPadding: 16,
flip: { fallbackPlacements: ['top', 'bottom-start', 'top-start'] },
fitViewport: true,
overlap: false
}}
contentClasses="p-0 flex"
contentStyle="height: min(72vh, 520px);"
class="border-b border-transparent text-accent hover:border-accent"
triggerAttrs={{ 'aria-label': 'Start from a template' }}
on:openChange={(e) =>
e.detail && logFeatureUsage('home', 'template_picker_open', { key: 'empty_state' })}
>
{#snippet trigger()}Start from a template{/snippet}
{#snippet content({ close })}
<HubTemplatePicker
onPick={(project) => {
close()
onPick(project)
}}
/>
{/snippet}
</Popover>
or
{/if}
<CreateActionsMenu source="empty_state" triggerElement={newLinkEl}>
{#snippet trigger()}
<!-- A bare <button> for a link inside a sentence, signed off by design: <Button>
@@ -107,7 +113,7 @@
<button
bind:this={newLinkEl}
class="border-b border-transparent text-accent hover:border-accent"
>create a new one</button
>{$disableHubStore ? 'Create a new one' : 'create a new one'}</button
>.
{/snippet}
</CreateActionsMenu>