mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-18 00:29:57 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2733ae1122 | |||
| 735787b183 | |||
| 2fb3507aa5 | |||
| 262ca8a1d0 | |||
| 66c9bdbfa3 | |||
| 4716a2a647 | |||
| 847f44197b | |||
| 6aacd4d185 | |||
| ab3db3bc68 | |||
| b52baceb50 | |||
| 0d2c8a37ef | |||
| 9298ec7cdb | |||
| 0fdff2feee | |||
| 8e042e6433 | |||
| 883ad14326 | |||
| 3cb41211ee | |||
| ee4a1a6b18 | |||
| a10972990a | |||
| 6d6e105711 |
@@ -0,0 +1,101 @@
|
|||||||
|
import { formatBackupTimestamp } from "./backupFileName";
|
||||||
|
import { assertEquals } from "@test/assert";
|
||||||
|
|
||||||
|
// Local-time constructors are used throughout, matching formatBackupTimestamp,
|
||||||
|
// so these cases do not depend on the machine's timezone.
|
||||||
|
|
||||||
|
function testMonthIsOneIndexed() {
|
||||||
|
console.log("Running month indexing tests...");
|
||||||
|
|
||||||
|
// The case from the report: a backup taken on 12 September 2026 was named
|
||||||
|
// db_2026-8-12_... because Date#getMonth is zero-indexed.
|
||||||
|
{
|
||||||
|
const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 35, 56));
|
||||||
|
assertEquals(
|
||||||
|
result,
|
||||||
|
"2026-09-12_20-35-56",
|
||||||
|
"September must render as 09, not 8"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other reported name, db_2026-0-23_..., was a January backup.
|
||||||
|
{
|
||||||
|
const result = formatBackupTimestamp(new Date(2026, 0, 23, 20, 25, 49));
|
||||||
|
assertEquals(
|
||||||
|
result,
|
||||||
|
"2026-01-23_20-25-49",
|
||||||
|
"January must render as 01, not 0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = formatBackupTimestamp(new Date(2026, 11, 31, 23, 59, 59));
|
||||||
|
assertEquals(
|
||||||
|
result,
|
||||||
|
"2026-12-31_23-59-59",
|
||||||
|
"December must render as 12"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testEveryFieldIsZeroPadded() {
|
||||||
|
console.log("Running zero padding tests...");
|
||||||
|
|
||||||
|
// db_2026-8-12_20-36-2 in the report: a single-digit second was not padded.
|
||||||
|
{
|
||||||
|
const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 36, 2));
|
||||||
|
assertEquals(
|
||||||
|
result,
|
||||||
|
"2026-09-12_20-36-02",
|
||||||
|
"Single-digit seconds must be padded"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const result = formatBackupTimestamp(new Date(2026, 0, 1, 0, 0, 0));
|
||||||
|
assertEquals(
|
||||||
|
result,
|
||||||
|
"2026-01-01_00-00-00",
|
||||||
|
"Midnight on the first of the month must pad every field"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testNamesSortChronologically() {
|
||||||
|
console.log("Running sort order tests...");
|
||||||
|
|
||||||
|
// Zero padding means a plain lexicographic sort of the backups directory
|
||||||
|
// lists the backups in the order they were taken.
|
||||||
|
const taken = [
|
||||||
|
new Date(2026, 8, 12, 20, 36, 2),
|
||||||
|
new Date(2026, 0, 23, 20, 25, 49),
|
||||||
|
new Date(2026, 8, 12, 20, 35, 56),
|
||||||
|
new Date(2026, 11, 31, 23, 59, 59)
|
||||||
|
];
|
||||||
|
|
||||||
|
const sorted = taken
|
||||||
|
.map((date) => formatBackupTimestamp(date))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
sorted.join(","),
|
||||||
|
[
|
||||||
|
"2026-01-23_20-25-49",
|
||||||
|
"2026-09-12_20-35-56",
|
||||||
|
"2026-09-12_20-36-02",
|
||||||
|
"2026-12-31_23-59-59"
|
||||||
|
].join(","),
|
||||||
|
"Backup names must sort into the order the backups were taken"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
try {
|
||||||
|
testMonthIsOneIndexed();
|
||||||
|
testEveryFieldIsZeroPadded();
|
||||||
|
testNamesSortChronologically();
|
||||||
|
console.log("All tests passed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Test failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Builds the timestamp segment of a database backup file name.
|
||||||
|
*
|
||||||
|
* `Date#getMonth` is zero-indexed, so building this inline produced names like
|
||||||
|
* `db_2026-8-12_...` for a backup taken on 12 September 2026. Every field is
|
||||||
|
* also zero-padded, which keeps the names unambiguous and makes them sort
|
||||||
|
* lexicographically in the order they were taken.
|
||||||
|
*
|
||||||
|
* @param date The moment the backup is being taken. Defaults to now.
|
||||||
|
* @returns A timestamp of the form `YYYY-MM-DD_HH-MM-SS`.
|
||||||
|
*/
|
||||||
|
export function formatBackupTimestamp(date: Date = new Date()): string {
|
||||||
|
const pad = (value: number): string => String(value).padStart(2, "0");
|
||||||
|
|
||||||
|
const datePart = [
|
||||||
|
date.getFullYear(),
|
||||||
|
pad(date.getMonth() + 1),
|
||||||
|
pad(date.getDate())
|
||||||
|
].join("-");
|
||||||
|
|
||||||
|
const timePart = [
|
||||||
|
pad(date.getHours()),
|
||||||
|
pad(date.getMinutes()),
|
||||||
|
pad(date.getSeconds())
|
||||||
|
].join("-");
|
||||||
|
|
||||||
|
return `${datePart}_${timePart}`;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import path from "path";
|
|||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { versionMigrations } from "../db/sqlite";
|
import { versionMigrations } from "../db/sqlite";
|
||||||
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
|
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
|
||||||
|
import { formatBackupTimestamp } from "@server/lib/backupFileName";
|
||||||
import { SqliteError } from "better-sqlite3";
|
import { SqliteError } from "better-sqlite3";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
@@ -121,7 +122,7 @@ function backupDb() {
|
|||||||
// copy the db.sqlite file to backups
|
// copy the db.sqlite file to backups
|
||||||
// add the date to the filename
|
// add the date to the filename
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
|
const dateString = formatBackupTimestamp(date);
|
||||||
const dbPath = path.join(dbDir, "db.sqlite");
|
const dbPath = path.join(dbDir, "db.sqlite");
|
||||||
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
||||||
fs.copyFileSync(dbPath, backupPath);
|
fs.copyFileSync(dbPath, backupPath);
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { sql } from "drizzle-orm";
|
|||||||
|
|
||||||
const version = "1.23.0";
|
const version = "1.23.0";
|
||||||
|
|
||||||
await migration();
|
|
||||||
|
|
||||||
export default async function migration() {
|
export default async function migration() {
|
||||||
console.log(`Running setup script ${version}...`);
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
@@ -12,11 +10,11 @@ export default async function migration() {
|
|||||||
await db.execute(sql`BEGIN`);
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
ALTER TABLE "newt" ADD COLUMN "agent" varchar;
|
ALTER TABLE "newt" ADD COLUMN IF NOT EXISTS "agent" varchar;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
ALTER TABLE "newt" ADD COLUMN "agentVersion" varchar;
|
ALTER TABLE "newt" ADD COLUMN IF NOT EXISTS "agentVersion" varchar;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
await db.execute(sql`COMMIT`);
|
await db.execute(sql`COMMIT`);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
users
|
users
|
||||||
} from "../../db/sqlite";
|
} from "../../db/sqlite";
|
||||||
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import { formatBackupTimestamp } from "@server/lib/backupFileName";
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
@@ -34,7 +35,7 @@ export default async function migration() {
|
|||||||
// copy the db.sqlite file to backups
|
// copy the db.sqlite file to backups
|
||||||
// add the date to the filename
|
// add the date to the filename
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
|
const dateString = formatBackupTimestamp(date);
|
||||||
const dbPath = path.join(dbDir, "db.sqlite");
|
const dbPath = path.join(dbDir, "db.sqlite");
|
||||||
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
||||||
fs.copyFileSync(dbPath, backupPath);
|
fs.copyFileSync(dbPath, backupPath);
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ export default async function migration() {
|
|||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
ALTER TABLE 'newt' ADD 'agent' text;
|
ALTER TABLE 'newt' ADD COLUMN 'agent' text;
|
||||||
`
|
`
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`
|
`
|
||||||
ALTER TABLE 'newt' ADD 'agentVersion' text;
|
ALTER TABLE 'newt' ADD COLUMN 'agentVersion' text;
|
||||||
`
|
`
|
||||||
).run();
|
).run();
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -76,11 +76,13 @@ export default function PrivateResourceInferencePage() {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
httpConfigSubdomain: z.string().nullish(),
|
httpConfigSubdomain: z.string().nullish(),
|
||||||
httpConfigDomainId: z.string().nullish(),
|
httpConfigDomainId: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: t("domainRequired") }),
|
||||||
httpConfigFullDomain: z.string().nullish(),
|
httpConfigFullDomain: z.string().nullish(),
|
||||||
ssl: z.boolean().optional()
|
ssl: z.boolean().optional()
|
||||||
}),
|
}),
|
||||||
[]
|
[t]
|
||||||
);
|
);
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ export default function PrivateResourceInferencePage() {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
providers: [],
|
providers: [],
|
||||||
httpConfigSubdomain: siteResource.subdomain ?? null,
|
httpConfigSubdomain: siteResource.subdomain ?? null,
|
||||||
httpConfigDomainId: siteResource.domainId ?? null,
|
httpConfigDomainId: siteResource.domainId ?? "",
|
||||||
httpConfigFullDomain: siteResource.fullDomain ?? null,
|
httpConfigFullDomain: siteResource.fullDomain ?? null,
|
||||||
ssl: siteResource.ssl ?? false
|
ssl: siteResource.ssl ?? false
|
||||||
}
|
}
|
||||||
@@ -289,50 +291,74 @@ export default function PrivateResourceInferencePage() {
|
|||||||
</SettingsSubsectionHeader>
|
</SettingsSubsectionHeader>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
<SettingsFormCell span="full">
|
<SettingsFormCell span="full">
|
||||||
<DomainPicker
|
<FormField
|
||||||
key={`inference-domain-${siteResource.id}`}
|
control={form.control}
|
||||||
orgId={siteResource.orgId}
|
name="httpConfigDomainId"
|
||||||
cols={2}
|
render={() => (
|
||||||
hideFreeDomain
|
<FormItem>
|
||||||
defaultSubdomain={
|
<DomainPicker
|
||||||
httpConfigSubdomain ?? undefined
|
key={`inference-domain-${siteResource.id}`}
|
||||||
}
|
orgId={
|
||||||
defaultDomainId={
|
siteResource.orgId
|
||||||
httpConfigDomainId ?? undefined
|
}
|
||||||
}
|
cols={2}
|
||||||
defaultFullDomain={
|
hideFreeDomain
|
||||||
httpConfigFullDomain ??
|
defaultSubdomain={
|
||||||
undefined
|
httpConfigSubdomain ??
|
||||||
}
|
undefined
|
||||||
onDomainChange={(res) => {
|
}
|
||||||
if (res === null) {
|
defaultDomainId={
|
||||||
form.setValue(
|
httpConfigDomainId ??
|
||||||
"httpConfigSubdomain",
|
undefined
|
||||||
null
|
}
|
||||||
);
|
defaultFullDomain={
|
||||||
form.setValue(
|
httpConfigFullDomain ??
|
||||||
"httpConfigDomainId",
|
undefined
|
||||||
null
|
}
|
||||||
);
|
onDomainChange={(
|
||||||
form.setValue(
|
res
|
||||||
"httpConfigFullDomain",
|
) => {
|
||||||
null
|
if (res === null) {
|
||||||
);
|
form.setValue(
|
||||||
return;
|
"httpConfigSubdomain",
|
||||||
}
|
null
|
||||||
form.setValue(
|
);
|
||||||
"httpConfigSubdomain",
|
form.setValue(
|
||||||
res.subdomain ?? null
|
"httpConfigDomainId",
|
||||||
);
|
"",
|
||||||
form.setValue(
|
{
|
||||||
"httpConfigDomainId",
|
shouldValidate:
|
||||||
res.domainId
|
true
|
||||||
);
|
}
|
||||||
form.setValue(
|
);
|
||||||
"httpConfigFullDomain",
|
form.setValue(
|
||||||
res.fullDomain
|
"httpConfigFullDomain",
|
||||||
);
|
null
|
||||||
}}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.setValue(
|
||||||
|
"httpConfigSubdomain",
|
||||||
|
res.subdomain ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"httpConfigDomainId",
|
||||||
|
res.domainId,
|
||||||
|
{
|
||||||
|
shouldValidate:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"httpConfigFullDomain",
|
||||||
|
res.fullDomain
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
<SettingsFormCell span="half">
|
<SettingsFormCell span="half">
|
||||||
|
|||||||
@@ -139,6 +139,22 @@ export default function GeneralForm() {
|
|||||||
: "Port number should not be set for HTTP resources",
|
: "Port number should not be set for HTTP resources",
|
||||||
path: ["proxyPort"]
|
path: ["proxyPort"]
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (
|
||||||
|
["http", "ssh", "rdp", "vnc", "inference"].includes(
|
||||||
|
resource.mode
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return !!data.domainId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: t("domainRequired"),
|
||||||
|
path: ["domainId"]
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
@@ -434,63 +450,87 @@ export default function GeneralForm() {
|
|||||||
resource.mode
|
resource.mode
|
||||||
) && (
|
) && (
|
||||||
<SettingsFormCell span="full">
|
<SettingsFormCell span="full">
|
||||||
<div id="resource-domain-picker">
|
<FormField
|
||||||
<DomainPicker
|
control={form.control}
|
||||||
allowWildcard={
|
name="domainId"
|
||||||
resource.mode !==
|
render={() => (
|
||||||
"inference"
|
<FormItem>
|
||||||
}
|
<div id="resource-domain-picker">
|
||||||
key={
|
<DomainPicker
|
||||||
resource.resourceId
|
allowWildcard={
|
||||||
}
|
resource.mode !==
|
||||||
orgId={orgId as string}
|
"inference"
|
||||||
cols={2}
|
}
|
||||||
defaultSubdomain={
|
key={
|
||||||
form.watch(
|
resource.resourceId
|
||||||
"subdomain"
|
}
|
||||||
) ?? undefined
|
orgId={
|
||||||
}
|
orgId as string
|
||||||
defaultDomainId={
|
}
|
||||||
form.watch(
|
cols={2}
|
||||||
"domainId"
|
defaultSubdomain={
|
||||||
) ?? undefined
|
form.watch(
|
||||||
}
|
"subdomain"
|
||||||
defaultFullDomain={
|
) ??
|
||||||
resourceFullDomainName ||
|
undefined
|
||||||
undefined
|
}
|
||||||
}
|
defaultDomainId={
|
||||||
onDomainChange={(
|
form.watch(
|
||||||
res
|
"domainId"
|
||||||
) => {
|
) ??
|
||||||
if (res === null) {
|
undefined
|
||||||
form.setValue(
|
}
|
||||||
"domainId",
|
defaultFullDomain={
|
||||||
undefined
|
resourceFullDomainName ||
|
||||||
);
|
undefined
|
||||||
form.setValue(
|
}
|
||||||
"subdomain",
|
onDomainChange={(
|
||||||
undefined
|
res
|
||||||
);
|
) => {
|
||||||
setResourceFullDomain(
|
if (
|
||||||
`${resource.ssl ? "https" : "http"}://`
|
res ===
|
||||||
);
|
null
|
||||||
return;
|
) {
|
||||||
}
|
form.setValue(
|
||||||
form.setValue(
|
"domainId",
|
||||||
"domainId",
|
undefined,
|
||||||
res.domainId
|
{
|
||||||
);
|
shouldValidate:
|
||||||
form.setValue(
|
true
|
||||||
"subdomain",
|
}
|
||||||
res.subdomain ??
|
);
|
||||||
undefined
|
form.setValue(
|
||||||
);
|
"subdomain",
|
||||||
setResourceFullDomain(
|
undefined
|
||||||
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
|
);
|
||||||
);
|
setResourceFullDomain(
|
||||||
}}
|
`${resource.ssl ? "https" : "http"}://`
|
||||||
/>
|
);
|
||||||
</div>
|
return;
|
||||||
|
}
|
||||||
|
form.setValue(
|
||||||
|
"domainId",
|
||||||
|
res.domainId,
|
||||||
|
{
|
||||||
|
shouldValidate:
|
||||||
|
true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"subdomain",
|
||||||
|
res.subdomain ??
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
setResourceFullDomain(
|
||||||
|
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SettingsFormCell>
|
</SettingsFormCell>
|
||||||
)}
|
)}
|
||||||
{!["tcp", "udp", "inference"].includes(
|
{!["tcp", "udp", "inference"].includes(
|
||||||
|
|||||||
@@ -386,25 +386,25 @@ export default function SitesTable({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (originalRow.type === "newt") {
|
if (originalRow.type === "newt") {
|
||||||
if (!originalRow.agent) {
|
if (!originalRow.agent && !originalRow.newtVersion) {
|
||||||
|
// it has not checked in yet
|
||||||
return <span>-</span>;
|
return <span>-</span>;
|
||||||
}
|
}
|
||||||
|
// agent and agentVersion were added after newtVersion, so a
|
||||||
|
// site still running an older Newt reports only newtVersion.
|
||||||
|
// Without these fallbacks the badge renders with no label and
|
||||||
|
// no version at all.
|
||||||
|
const agentLabel =
|
||||||
|
originalRow.agent == "cli" ? "Pangolin CLI" : "Newt";
|
||||||
|
const agentVersion =
|
||||||
|
originalRow.agentVersion ?? originalRow.newtVersion;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
<span>
|
<span>{agentLabel}</span>
|
||||||
{originalRow.agent == "newt"
|
{agentVersion && (
|
||||||
? "Newt"
|
<span>v{agentVersion}</span>
|
||||||
: null}
|
|
||||||
{originalRow.agent == "cli"
|
|
||||||
? "Pangolin CLI"
|
|
||||||
: null}
|
|
||||||
</span>
|
|
||||||
{originalRow.agentVersion && (
|
|
||||||
<span>
|
|
||||||
v{originalRow.agentVersion}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ Type=simple
|
|||||||
User=root
|
User=root
|
||||||
Group=root
|
Group=root
|
||||||
EnvironmentFile=/etc/pangolin/pangolin-site.env
|
EnvironmentFile=/etc/pangolin/pangolin-site.env
|
||||||
ExecStart=/home/owen/fossorial/cli/bin/pangolin up site
|
ExecStart=/usr/local/bin/pangolin up site
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
UMask=0077
|
UMask=0077
|
||||||
|
|||||||
@@ -523,6 +523,13 @@ export function createCreateFormSchema(t: TranslateFn) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (data.mode === "inference" && !data.httpConfigDomainId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t("domainRequired"),
|
||||||
|
path: ["httpConfigDomainId"]
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user