Show the running version on Settings > General
The General page ends with a Version block naming the release this server runs, so an update can be confirmed from the app. The version is COBBLE_VERSION, the release tag the image was built with (Dockerfile ARG, passed by the release workflow, which falls back to package.json's version on a workflow-dispatch build), else package.json's version, which a development server reports too, with (development) after it. runningVersion (src/utils/running-version.ts) drops an unusable version string rather than showing it. Also includes the working tree's pending signup guard in src/pages/__root.server.ts: a user who still holds a session while the install has no users is sent to /auth/logout (and may load it).
This commit is contained in:
@@ -21,6 +21,22 @@ const server: BunextPageServerFn<PagePropsType> = async ({ req, url }) => {
|
||||
const is_signup_page = url.pathname.startsWith("/auth/signup");
|
||||
const has_users = await grabHasUsers();
|
||||
|
||||
if (
|
||||
!has_users &&
|
||||
user?.logged_in_status &&
|
||||
!url.pathname.includes(`auth/logout`)
|
||||
) {
|
||||
return redirect({ destination: "/auth/logout" });
|
||||
}
|
||||
|
||||
if (
|
||||
!has_users &&
|
||||
user?.logged_in_status &&
|
||||
url.pathname.includes(`auth/logout`)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!has_users && !is_signup_page) {
|
||||
return redirect({ destination: "/auth/signup" });
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import grabStorageQuotaOverview from "@/src/functions/backend/db/app-settings/grab-storage-quota-overview";
|
||||
import { grabSystemPromptAppendOverview } from "@/src/functions/backend/db/app-settings/grab-system-prompt-append";
|
||||
import runningVersion from "@/src/utils/running-version";
|
||||
|
||||
type Params = Parameters<BunextPageServerFn<PagePropsType>>[0];
|
||||
|
||||
@@ -44,6 +45,7 @@ export default async function grabSettingsServerProps({ req }: Params) {
|
||||
has_password,
|
||||
storage_quota: await grabStorageQuotaOverview().catch(() => null),
|
||||
system_prompt_append: await grabSystemPromptAppendOverview().catch(() => null),
|
||||
running_version: runningVersion({ env: process.env }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import type { RunningVersion } from "@/src/utils/running-version";
|
||||
|
||||
type Props = {
|
||||
running?: RunningVersion | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* The release this server runs, in words: settings are where someone
|
||||
* checks whether an update landed. A development server says so beside
|
||||
* its version, so the two are never mistaken.
|
||||
*/
|
||||
export default function VersionLine({ running }: Props) {
|
||||
if (!running) return null;
|
||||
|
||||
const version = running.version
|
||||
? `Cobble ${running.version}`
|
||||
: "Cobble";
|
||||
|
||||
return (
|
||||
<Span className="text-sm leading-5 text-ink">
|
||||
{running.mode == "development"
|
||||
? `${version} (development)`
|
||||
: version}
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import SettingsGroup from "../../(partials)/settings-group";
|
||||
import VersionLine from "../(partials)/version-line";
|
||||
|
||||
/**
|
||||
* What this server is running, so an update can be confirmed from the app
|
||||
* instead of the installer's output.
|
||||
*/
|
||||
export default function VersionSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<SettingsGroup
|
||||
title="Version"
|
||||
description="The release this server runs, so an update can be confirmed from the app."
|
||||
id="version"
|
||||
>
|
||||
<VersionLine running={pageProps?.running_version} />
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import AppearanceSection from "./(sections)/appearance-section";
|
||||
import PromptSection from "./(sections)/prompt-section";
|
||||
import StorageSection from "./(sections)/storage-section";
|
||||
import VersionSection from "./(sections)/version-section";
|
||||
import SettingsShell from "../(partials)/settings-shell";
|
||||
import settingsPageMeta from "../(functions)/settings-page-meta";
|
||||
|
||||
@@ -13,6 +14,7 @@ export default function AdminSettingsGeneralPage() {
|
||||
<AppearanceSection />
|
||||
<PromptSection />
|
||||
<StorageSection />
|
||||
<VersionSection />
|
||||
</SettingsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
} from "@/db/types/db";
|
||||
import type { UserTypes } from "../dict/user-types-dict";
|
||||
import type { StorageVolume } from "../utils/storage-volumes";
|
||||
import type { RunningVersion } from "../utils/running-version";
|
||||
import type { WebsocketRooms } from "../dict/websocket-rooms";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite";
|
||||
@@ -1038,6 +1039,10 @@ export type PagePropsType = {
|
||||
setting_key: string;
|
||||
text: string;
|
||||
} | null;
|
||||
/**
|
||||
* The release the server is running, for the General settings page.
|
||||
*/
|
||||
running_version?: RunningVersion | null;
|
||||
push_overview?: PushOverview | null;
|
||||
mcps_overview?: McpServersOverviewPayload | null;
|
||||
tools_overview?: CustomToolsOverviewPayload | null;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import runningVersion from "./running-version";
|
||||
|
||||
describe("runningVersion", () => {
|
||||
test("a release reports the build's version", () => {
|
||||
expect(
|
||||
runningVersion({
|
||||
env: { NODE_ENV: "production", COBBLE_VERSION: "v0.1.1" },
|
||||
}),
|
||||
).toEqual({ version: "v0.1.1", mode: "production" });
|
||||
});
|
||||
|
||||
test("a production build without a version falls back to the source's", () => {
|
||||
expect(
|
||||
runningVersion({
|
||||
env: { NODE_ENV: "production" },
|
||||
package_version: "0.1.1",
|
||||
}),
|
||||
).toEqual({ version: "0.1.1", mode: "production" });
|
||||
});
|
||||
|
||||
test("a development server reports the source's version too", () => {
|
||||
expect(
|
||||
runningVersion({
|
||||
env: { NODE_ENV: "development" },
|
||||
package_version: "0.1.1",
|
||||
}),
|
||||
).toEqual({ version: "0.1.1", mode: "development" });
|
||||
});
|
||||
|
||||
test("a development server with a build version prefers it", () => {
|
||||
expect(
|
||||
runningVersion({
|
||||
env: { NODE_ENV: "development", COBBLE_VERSION: "v0.2.0" },
|
||||
package_version: "0.1.1",
|
||||
}),
|
||||
).toEqual({ version: "v0.2.0", mode: "development" });
|
||||
});
|
||||
|
||||
test("no NODE_ENV at all is a development server", () => {
|
||||
expect(runningVersion({ env: {}, package_version: "0.1.1" })).toEqual({
|
||||
version: "0.1.1",
|
||||
mode: "development",
|
||||
});
|
||||
});
|
||||
|
||||
test("an unusable version is dropped rather than shown", () => {
|
||||
expect(
|
||||
runningVersion({
|
||||
env: { NODE_ENV: "production", COBBLE_VERSION: "bad version!" },
|
||||
package_version: "",
|
||||
}),
|
||||
).toEqual({ version: null, mode: "production" });
|
||||
});
|
||||
|
||||
test("an empty build version falls back to the source's", () => {
|
||||
expect(
|
||||
runningVersion({
|
||||
env: { NODE_ENV: "production", COBBLE_VERSION: " " },
|
||||
package_version: "0.1.1",
|
||||
}),
|
||||
).toEqual({ version: "0.1.1", mode: "production" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import package_json from "@/package.json";
|
||||
|
||||
/**
|
||||
* What the General settings page shows as the running Cobble.
|
||||
*/
|
||||
export type RunningVersion = {
|
||||
/**
|
||||
* The release's version: the image tag the installer pulled
|
||||
* (`COBBLE_VERSION`, set when the image was built), else
|
||||
* `package.json`'s version, which a development server reports too.
|
||||
*/
|
||||
version: string | null;
|
||||
/**
|
||||
* Where the live server runs (`NODE_ENV`): `production`,
|
||||
* `development` or the environment's own name when it set another.
|
||||
*/
|
||||
mode: string;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The process environment (`process.env`).
|
||||
*/
|
||||
env: Record<string, string | undefined>;
|
||||
/**
|
||||
* `package.json`'s version, for a build that wasn't given one.
|
||||
*/
|
||||
package_version?: string;
|
||||
};
|
||||
|
||||
const VersionPattern = /^[A-Za-z0-9._-]{1,64}$/;
|
||||
|
||||
/**
|
||||
* The version a running Cobble reports, development included: the image's
|
||||
* build argument when it has one (the release tag), else the source's own
|
||||
* version, so a dev server names the code it is working on.
|
||||
*/
|
||||
export default function runningVersion({
|
||||
env,
|
||||
package_version = package_json.version,
|
||||
}: Params): RunningVersion {
|
||||
const build_version = env.COBBLE_VERSION?.trim();
|
||||
|
||||
const version =
|
||||
build_version && VersionPattern.test(build_version)
|
||||
? build_version
|
||||
: package_version && VersionPattern.test(package_version)
|
||||
? package_version
|
||||
: null;
|
||||
|
||||
return { version, mode: env.NODE_ENV || "development" };
|
||||
}
|
||||
Reference in New Issue
Block a user