Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions doc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,42 @@ components:
type: integer
minimum: 0
description: Currently-running `index_runs` rows.
update_available:
type: boolean
description: |
True when the version-check service has found a `server/v*`
release on GitHub strictly newer than the running server.
Field is omitted entirely when version-check is not wired
(set `CIX_VERSION_CHECK_ENABLED=false` to disable polling).
latest_version:
type: string
nullable: true
description: |
Latest released server version (without the `server/v` prefix,
e.g. `0.5.1`). Null until the first successful poll completes.
release_url:
type: string
nullable: true
description: GitHub release page URL for `latest_version`. Null when unknown.
version_check:
$ref: "#/components/schemas/VersionCheckStatus"

VersionCheckStatus:
type: object
required: [enabled]
properties:
enabled:
type: boolean
description: Whether the periodic GitHub poll is running.
checked_at:
type: string
format: date-time
nullable: true
description: Last poll timestamp (UTC, RFC 3339). Null before the first poll.
error:
type: string
nullable: true
description: Last error message, if the most recent poll failed. Null on success.

ProjectSettings:
type: object
Expand Down
10 changes: 10 additions & 0 deletions server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ bundle: fetch-llama build
@# filepath.Dir(os.Executable())/llama default resolves correctly.
mkdir -p $(BUNDLE_DIR)/llama
cp -R $(LLAMA_DIR)/. $(BUNDLE_DIR)/llama/
ifeq ($(OS),darwin)
@# macOS Sequoia (26+) tightened amfid: ad-hoc-signed binaries whose
@# linked dylibs carry stale signatures or a com.apple.provenance
@# xattr from the previous bundle get SIGKILL'd within milliseconds
@# of execve — supervisor sees "signal: killed" with empty stderr.
@# `cp -R` creates new files macOS treats as untrusted, so the strip
@# + deep re-sign must run on every bundle, not just first install.
@xattr -cr $(BUNDLE_DIR)/llama/
@codesign --force --deep --sign - $(BUNDLE_DIR)/llama/llama-server
endif
@echo "Bundle ready: $(BUNDLE_DIR)"
@echo "Optional: tar czf $(DIST_DIR)/$(BUNDLE_NAME).tar.gz -C $(DIST_DIR) $(BUNDLE_NAME)"

Expand Down
16 changes: 16 additions & 0 deletions server/cmd/cix-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/dvcdsys/code-index/server/internal/sessions"
"github.com/dvcdsys/code-index/server/internal/users"
"github.com/dvcdsys/code-index/server/internal/vectorstore"
"github.com/dvcdsys/code-index/server/internal/versioncheck"
)

func runHealthcheck() {
Expand Down Expand Up @@ -190,6 +191,20 @@ func run() error {
}
}

// Background version-check poller. The 60s initial delay keeps GitHub
// off the boot path; the goroutine exits cleanly when bgCtx is canceled
// in the shutdown branch below.
bgCtx, bgCancel := context.WithCancel(context.Background())
defer bgCancel()
vcSvc := versioncheck.New(versioncheck.Config{
Enabled: cfg.VersionCheckEnabled,
Interval: cfg.VersionCheckInterval,
InitialDelay: 60 * time.Second,
Repo: cfg.VersionCheckRepo,
CurrentVersion: version,
}, logger)
go vcSvc.Run(bgCtx)

handler := httpapi.NewRouter(httpapi.Deps{
DB: database,
ServerVersion: version,
Expand All @@ -205,6 +220,7 @@ func run() error {
VectorStore: vs,
Indexer: idx,
RuntimeCfg: rcfg,
VersionCheck: vcSvc,
})

srv := &http.Server{
Expand Down
5 changes: 4 additions & 1 deletion server/dashboard/src/app/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import type { ReactNode } from 'react';
import { Sidebar } from './Sidebar';
import { Footer } from './Footer';
import { UpdateBanner } from './UpdateBanner';

// Three-row layout: sidebar + main on top, footer spanning the full
// width on the bottom. min-h-0 on the inner row is required so that
// <main>'s overflow-y-auto honors the footer's height when content
// grows tall.
// grows tall. UpdateBanner sits above the main row so it spans the
// full width when a newer server release is available.
export function Shell({ children }: { children: ReactNode }) {
return (
<div className="flex h-dvh w-full flex-col">
<UpdateBanner />
<div className="flex min-h-0 flex-1">
<Sidebar />
<main className="flex-1 overflow-y-auto">
Expand Down
78 changes: 78 additions & 0 deletions server/dashboard/src/app/UpdateBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useEffect, useState } from 'react';
import { ArrowUpCircle, X } from 'lucide-react';
import { useServerStatus } from '@/lib/useServerStatus';
import { cn } from '@/lib/cn';

const dismissKey = (version: string) => `cix.update-dismissed.${version}`;

// UpdateBanner renders at the top of the dashboard shell when the
// server-side version checker reports a newer `server/v*` release on
// GitHub. Dismissals are namespaced by the latest version, so dismissing
// 0.6.0 silences the banner only until 0.6.1 ships.
export function UpdateBanner() {
const { data } = useServerStatus();
const latest = data?.latest_version ?? null;
const updateAvailable = data?.update_available === true && !!latest;

const [dismissed, setDismissed] = useState(false);

// Re-evaluate the localStorage flag whenever the latest version changes
// — a fresh release should re-show the banner even within an open tab.
useEffect(() => {
if (!latest) {
setDismissed(false);
return;
}
setDismissed(localStorage.getItem(dismissKey(latest)) === '1');
}, [latest]);

if (!updateAvailable || dismissed || !latest) return null;

const onDismiss = () => {
localStorage.setItem(dismissKey(latest), '1');
setDismissed(true);
};

return (
<div
role="status"
className={cn(
'flex items-center gap-3 border-b border-emerald-200 bg-emerald-50 px-5 py-2',
'text-sm text-emerald-900',
'dark:border-emerald-900/50 dark:bg-emerald-950/40 dark:text-emerald-100',
)}
>
<ArrowUpCircle className="h-4 w-4 shrink-0" aria-hidden />
<div className="flex-1">
cix-server <strong>v{latest}</strong> is available
{data?.server_version ? (
<> (you're running v{data.server_version})</>
) : null}
{data?.release_url ? (
<>
{' — '}
<a
href={data.release_url}
target="_blank"
rel="noreferrer noopener"
className="font-medium underline underline-offset-2 hover:no-underline"
>
release notes
</a>
</>
) : null}
</div>
<button
type="button"
onClick={onDismiss}
aria-label="Dismiss update notification"
className={cn(
'rounded-md p-1 transition-colors',
'hover:bg-emerald-100 dark:hover:bg-emerald-900/40',
)}
>
<X className="h-4 w-4" aria-hidden />
</button>
</div>
);
}
10 changes: 10 additions & 0 deletions server/dashboard/src/lib/useServerStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ interface StatusPayload {
server_version: string;
embedding_model: string;
model_loaded: boolean;
// Version-check fields are present only when the server has the
// versioncheck service wired (see CIX_VERSION_CHECK_ENABLED).
update_available?: boolean;
latest_version?: string | null;
release_url?: string | null;
version_check?: {
enabled: boolean;
checked_at?: string | null;
error?: string | null;
};
}

// useServerStatus polls /api/v1/status every 30 seconds. The footer
Expand Down
2 changes: 1 addition & 1 deletion server/dashboard/tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/generated.ts","./src/api/types.ts","./src/app/app.tsx","./src/app/footer.tsx","./src/app/shell.tsx","./src/app/sidebar.tsx","./src/app/themeprovider.tsx","./src/app/providers.tsx","./src/auth/authprovider.tsx","./src/auth/bootstrapneededpage.tsx","./src/auth/changepasswordpage.tsx","./src/auth/loginpage.tsx","./src/auth/useauth.ts","./src/lib/cn.ts","./src/lib/editorpreference.ts","./src/lib/formatdate.ts","./src/lib/theme.ts","./src/lib/useserverstatus.ts","./src/modules/registry.ts","./src/modules/types.ts","./src/modules/api-keys/apikeyspage.tsx","./src/modules/api-keys/hooks.ts","./src/modules/api-keys/index.ts","./src/modules/api-keys/components/apikeytable.tsx","./src/modules/api-keys/components/createapikeydialog.tsx","./src/modules/api-keys/components/revokeapikeydialog.tsx","./src/modules/home/homepage.tsx","./src/modules/home/index.ts","./src/modules/projects/projectdetailpage.tsx","./src/modules/projects/projectslistpage.tsx","./src/modules/projects/projectspage.tsx","./src/modules/projects/hooks.ts","./src/modules/projects/index.ts","./src/modules/projects/components/deleteprojectdialog.tsx","./src/modules/projects/components/projectcard.tsx","./src/modules/projects/components/projectinfocard.tsx","./src/modules/search/searchpage.tsx","./src/modules/search/hooks.ts","./src/modules/search/index.ts","./src/modules/search/components/filters.tsx","./src/modules/search/components/resultfilecard.tsx","./src/modules/search/components/resultsnippet.tsx","./src/modules/search/components/searchinput.tsx","./src/modules/server/serverpage.tsx","./src/modules/server/hooks.ts","./src/modules/server/index.ts","./src/modules/server/components/saveandrestartdialog.tsx","./src/modules/server/components/sidecarstatebadge.tsx","./src/modules/server/components/sourcepill.tsx","./src/modules/server/sections/advancedsection.tsx","./src/modules/server/sections/embeddingmodelsection.tsx","./src/modules/server/sections/runtimeparamssection.tsx","./src/modules/server/sections/sidecarsection.tsx","./src/modules/settings/settingspage.tsx","./src/modules/settings/hooks.ts","./src/modules/settings/index.ts","./src/modules/settings/components/changepasswordform.tsx","./src/modules/settings/components/sessionrow.tsx","./src/modules/settings/sections/editorsection.tsx","./src/modules/settings/sections/profilesection.tsx","./src/modules/settings/sections/sessionssection.tsx","./src/modules/settings/sections/themesection.tsx","./src/modules/users/userspage.tsx","./src/modules/users/hooks.ts","./src/modules/users/index.ts","./src/modules/users/components/deleteuserdialog.tsx","./src/modules/users/components/disableuserbutton.tsx","./src/modules/users/components/inviteuserdialog.tsx","./src/modules/users/components/userroleselect.tsx","./src/modules/users/components/userstable.tsx","./src/ui/alert.tsx","./src/ui/badge.tsx","./src/ui/button.tsx","./src/ui/card.tsx","./src/ui/dialog.tsx","./src/ui/input.tsx","./src/ui/label.tsx","./src/ui/radio-group.tsx","./src/ui/scroll-area.tsx","./src/ui/select.tsx","./src/ui/skeleton.tsx","./src/ui/slider.tsx","./src/ui/sonner.tsx","./src/ui/switch.tsx","./src/ui/table.tsx","./src/ui/tabs.tsx","./src/ui/tooltip.tsx"],"version":"5.9.3"}
{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/generated.ts","./src/api/types.ts","./src/app/app.tsx","./src/app/footer.tsx","./src/app/shell.tsx","./src/app/sidebar.tsx","./src/app/themeprovider.tsx","./src/app/updatebanner.tsx","./src/app/providers.tsx","./src/auth/authprovider.tsx","./src/auth/bootstrapneededpage.tsx","./src/auth/changepasswordpage.tsx","./src/auth/loginpage.tsx","./src/auth/useauth.ts","./src/lib/cn.ts","./src/lib/editorpreference.ts","./src/lib/formatdate.ts","./src/lib/theme.ts","./src/lib/useserverstatus.ts","./src/modules/registry.ts","./src/modules/types.ts","./src/modules/api-keys/apikeyspage.tsx","./src/modules/api-keys/hooks.ts","./src/modules/api-keys/index.ts","./src/modules/api-keys/components/apikeytable.tsx","./src/modules/api-keys/components/createapikeydialog.tsx","./src/modules/api-keys/components/revokeapikeydialog.tsx","./src/modules/home/homepage.tsx","./src/modules/home/index.ts","./src/modules/projects/projectdetailpage.tsx","./src/modules/projects/projectslistpage.tsx","./src/modules/projects/projectspage.tsx","./src/modules/projects/hooks.ts","./src/modules/projects/index.ts","./src/modules/projects/components/deleteprojectdialog.tsx","./src/modules/projects/components/projectcard.tsx","./src/modules/projects/components/projectinfocard.tsx","./src/modules/search/searchpage.tsx","./src/modules/search/hooks.ts","./src/modules/search/index.ts","./src/modules/search/components/filters.tsx","./src/modules/search/components/resultfilecard.tsx","./src/modules/search/components/resultsnippet.tsx","./src/modules/search/components/searchinput.tsx","./src/modules/server/serverpage.tsx","./src/modules/server/hooks.ts","./src/modules/server/index.ts","./src/modules/server/components/saveandrestartdialog.tsx","./src/modules/server/components/sidecarstatebadge.tsx","./src/modules/server/components/sourcepill.tsx","./src/modules/server/sections/advancedsection.tsx","./src/modules/server/sections/embeddingmodelsection.tsx","./src/modules/server/sections/runtimeparamssection.tsx","./src/modules/server/sections/sidecarsection.tsx","./src/modules/settings/settingspage.tsx","./src/modules/settings/hooks.ts","./src/modules/settings/index.ts","./src/modules/settings/components/changepasswordform.tsx","./src/modules/settings/components/sessionrow.tsx","./src/modules/settings/sections/editorsection.tsx","./src/modules/settings/sections/profilesection.tsx","./src/modules/settings/sections/sessionssection.tsx","./src/modules/settings/sections/themesection.tsx","./src/modules/users/userspage.tsx","./src/modules/users/hooks.ts","./src/modules/users/index.ts","./src/modules/users/components/deleteuserdialog.tsx","./src/modules/users/components/disableuserbutton.tsx","./src/modules/users/components/inviteuserdialog.tsx","./src/modules/users/components/userroleselect.tsx","./src/modules/users/components/userstable.tsx","./src/ui/alert.tsx","./src/ui/badge.tsx","./src/ui/button.tsx","./src/ui/card.tsx","./src/ui/dialog.tsx","./src/ui/input.tsx","./src/ui/label.tsx","./src/ui/radio-group.tsx","./src/ui/scroll-area.tsx","./src/ui/select.tsx","./src/ui/skeleton.tsx","./src/ui/slider.tsx","./src/ui/sonner.tsx","./src/ui/switch.tsx","./src/ui/table.tsx","./src/ui/tabs.tsx","./src/ui/tooltip.tsx"],"version":"5.9.3"}
39 changes: 39 additions & 0 deletions server/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"runtime"
"strconv"
"strings"
"time"
)

// Config holds all runtime settings. Port defaults to 21847 — the same
Expand Down Expand Up @@ -80,6 +81,18 @@ type Config struct {
// CIX_BOOTSTRAP_ADMIN_PASSWORD.
BootstrapAdminEmail string
BootstrapAdminPassword string

// Version-check feature — periodic GitHub poll that surfaces a
// "newer release available" banner in the dashboard. Off entirely when
// CIX_VERSION_CHECK_ENABLED=false (no outbound HTTP at all). Repo is
// configurable so forks can point at their own GitHub project; the tag
// filter is hardcoded to `server/v` since this is the server binary.
// Sources: CIX_VERSION_CHECK_ENABLED (default true),
// CIX_VERSION_CHECK_INTERVAL (default 6h, Go duration string),
// CIX_VERSION_CHECK_REPO (default "dvcdsys/code-index").
VersionCheckEnabled bool
VersionCheckInterval time.Duration
VersionCheckRepo string
}

// ModelSafeName returns the embedding model name normalised for use inside
Expand Down Expand Up @@ -238,6 +251,20 @@ func Load() (*Config, error) {
c.BootstrapAdminEmail = getenv("CIX_BOOTSTRAP_ADMIN_EMAIL", "")
c.BootstrapAdminPassword = getenv("CIX_BOOTSTRAP_ADMIN_PASSWORD", "")

vcEnabled, err := getenvBool("CIX_VERSION_CHECK_ENABLED", true)
if err != nil {
return nil, err
}
c.VersionCheckEnabled = vcEnabled

vcInterval, err := getenvDuration("CIX_VERSION_CHECK_INTERVAL", 6*time.Hour)
if err != nil {
return nil, err
}
c.VersionCheckInterval = vcInterval

c.VersionCheckRepo = getenv("CIX_VERSION_CHECK_REPO", "dvcdsys/code-index")

return c, nil
}

Expand Down Expand Up @@ -384,3 +411,15 @@ func getenvBool(key string, def bool) (bool, error) {
}
return b, nil
}

func getenvDuration(key string, def time.Duration) (time.Duration, error) {
v, ok := os.LookupEnv(key)
if !ok {
return def, nil
}
d, err := time.ParseDuration(v)
if err != nil {
return 0, fmt.Errorf("env %s: %w", key, err)
}
return d, nil
}
Loading
Loading