mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
release-2.9.1 (#1557)
* fix(sso): send the PKCE verifier for GitHub login (#1534) * fix(remote-desktop): clip cursor overflow without disabling zoom (#1535) * test(remote-desktop): preserve sessions beyond one hour (#1536) * fix(hosts): expose controls for overflowing editor tabs (#1537) * fix(hosts): expose controls for overflowing editor tabs * test(hosts): mock resize observation in admin panel tests * fix(status): skip TCP probes while host sessions are active (#1538) * fix(database): batch session activity persistence (#1539) * docs(api): describe cookie and API key authentication (#1540) * fix(snippets): repair missing note column on SQLite upgrades (#1541) * fix(docker): retain stored SSH credential association (#1543) * fix(file-manager): render transfer toasts without plugin hooks (#1546) * feat(hosts): keep compact row actions inline (#1547) * fix(auth): allow retrying unavailable second-factor interfaces (#1549) * fix(auth): reject unresolved legacy identity provisioning (#1550) * fix(desktop): ship the plugin SDK inside Mac App Store builds (#1551) * fix(tmux): pass full session info to the terminal's session picker (#1552) The tmux.sessions service reduced detected sessions to bare names, but the picker reads session.name, so every entry rendered blank and selecting one sent an empty name, which created a new session instead of attaching. * fix(ssh): read force keyboard-interactive as a boolean when resolving hosts (#1553) The column holds the text "true"/"false", and the resolver passed it through as-is. The string "false" is truthy, so the password provider treated every saved host as forced keyboard-interactive and left the password out. Jump hops are built straight from the resolved host, so a password hop whose server doesn't offer keyboard-interactive failed with "All configured authentication methods failed". * fix(database): batch database saves for bulk host writes (#1554) * fix(database): yield to the event loop between database saves Each SQLite save serializes and encrypts the whole database into new buffers, which V8 only releases once the event loop turns. Boot migrations and bulk host import write plugin settings per host and per key in an awaited loop, so hundreds of full copies piled up and the container ran out of memory. * fix(database): save once per bulk host write instead of once per row On SQLite every repository write force-saves the whole database. Boot plugin data migrations, host defaults materialization and the bulk host routes write one row per host per setting, so they serialized and encrypted the full database hundreds of times in a row. DatabaseSaveTrigger.batched wraps a function so force saves made while it runs collapse into a single save when it finishes. Nested scopes fold into the outer one, and work that outlives its scope saves normally. * feat(hosts): add instance-wide predefined tag suggestions (#1548) * feat(hosts): add shared predefined tag suggestions * style(hosts): format tag catalog routes * test(database): advance fake timers past the save yield (#1555) * fix(i18n): complete Simplified Chinese core and plugin translations (#1542) * fix(i18n): complete Chinese onboarding and navigation labels * fix(i18n): translate remaining Chinese core and plugin interfaces * fix(i18n): translate host editor tab overflow controls * fix(i18n): use consistent Chinese fleet terminology * fix(i18n): localize hardcoded controls and plugin views * fix(i18n): translate built-in homepage widget catalog * test(homepage): follow translated timezone placeholder * fix(i18n): translate plugin-provided homepage widgets * fix(i18n): localize feature settings section titles * fix: 2.8 oidc accounts unable to sign in after upgrading (#1381) * fix: plugins losing the saved ssh login when copying a host (#1391) * fix: fleets, proxmox and automations not getting the host sudo password * fix: host imports running a defaults pass and metrics restart per host (#1384) * fix: high cpu from status probes and full database saves on every sample (#1300) * fix: user data export freezing the server (#1393) * fix: op:// secret references rejected as ssh keys on credentials (#1394) * fix: slow file deletes from the trash lookups on every delete (#1390) * fix: add openapi docs and error handling to host tag routes * test: compare download stream buffers directly so it stops timing out * chore: drop em dash from host row comment * chore: update release notes for 2.9.1 * fix: restore space to add host tags and redesign predefined tags editor * fix: host key silently accepted when the host is missing from the database (#1397) * fix: clearer host login failed status label and fix its translations (#1396) * chore: add host key and status label fixes to release notes * fix: sso and ldap provider dialogs overflowing the screen and using mismatched toggles * chore: increment ver * fix(audit): store plugin entries with no acting user as a null user_id (#1556) Plugin audit entries written outside a request used the literal "system" as user_id. That column references users.id, so the insert was refused (Postgres logs it as an FK violation) and the entry was silently dropped. Write null instead and keep "system" / plugin:<id> in username. * chore: add sso/ldap dialog and audit log fixes to release notes * fix: tunnels and host settings missing from shared hosts on desktop * fix: remote desktop logins missing from shared hosts on desktop * fix: simplify host status to online/offline and keep it live without a refresh * chore: sync Crowdin translations for 2.9.1 --------- Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
1 parent
5786eaccbb
commit
6b708106e1
912 files changed
+14220
-2200
No files matched your search
@@ -61,6 +61,7 @@ function isTransientStatusError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Throws when the statuses could not be read, so callers keep what they had. */
|
||||
export async function getAllServerStatuses(): Promise<
|
||||
Record<number, ServerStatus>
|
||||
> {
|
||||
@@ -71,31 +72,24 @@ export async function getAllServerStatuses(): Promise<
|
||||
let remoteHostsBySyncId: Map<string, number> | null = null;
|
||||
|
||||
if (isElectron()) {
|
||||
try {
|
||||
const response = await sshHostApi.get<SSHHost[]>("/db/host");
|
||||
const defaultOrigin = await resolveConnectionOrigin({
|
||||
connectionOrigin: null,
|
||||
});
|
||||
const hosts = response.data || [];
|
||||
localHostIds = hosts
|
||||
// A host-list failure throws here: sending no ids would stop every check.
|
||||
const response = await sshHostApi.get<SSHHost[]>("/db/host");
|
||||
const defaultOrigin = await resolveConnectionOrigin({
|
||||
connectionOrigin: null,
|
||||
});
|
||||
const hosts = response.data || [];
|
||||
localHostIds = hosts
|
||||
.filter((host) => (host.connectionOrigin ?? defaultOrigin) === "local")
|
||||
.map((host) => host.id);
|
||||
remoteHostsBySyncId = new Map(
|
||||
hosts
|
||||
.filter(
|
||||
(host) => (host.connectionOrigin ?? defaultOrigin) === "local",
|
||||
(host) =>
|
||||
(host.connectionOrigin ?? defaultOrigin) === "remote" &&
|
||||
!!host.syncId,
|
||||
)
|
||||
.map((host) => host.id);
|
||||
remoteHostsBySyncId = new Map(
|
||||
hosts
|
||||
.filter(
|
||||
(host) =>
|
||||
(host.connectionOrigin ?? defaultOrigin) === "remote" &&
|
||||
!!host.syncId,
|
||||
)
|
||||
.map((host) => [host.syncId as string, host.id]),
|
||||
);
|
||||
} catch {
|
||||
// A host-list failure must not start local probes for hosts whose
|
||||
// configured origin may be remote.
|
||||
localHostIds = [];
|
||||
}
|
||||
.map((host) => [host.syncId as string, host.id]),
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) {
|
||||
@@ -128,10 +122,7 @@ export async function getAllServerStatuses(): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
handleApiError(lastError, "fetch server statuses");
|
||||
return {};
|
||||
}
|
||||
if (lastError) handleApiError(lastError, "fetch server statuses");
|
||||
|
||||
if (remoteHostsBySyncId?.size && (await getLinkedSession())) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { hostApi } from "@/main-axios";
|
||||
|
||||
export async function getHostTags(): Promise<string[]> {
|
||||
return (await hostApi.get<{ tags: string[] }>("/tags")).data.tags;
|
||||
}
|
||||
|
||||
export async function saveHostTags(tags: string[]): Promise<string[]> {
|
||||
return (await hostApi.put<{ tags: string[] }>("/tags", { tags })).data.tags;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { getAllServerStatuses, handleApiError, sshHostApi } from "@/main-axios";
|
||||
import { handleApiError, sshHostApi } from "@/main-axios";
|
||||
import type { SSHHost, SSHHostData, ProxyNode } from "@/types/index";
|
||||
import type { ServerStatus, SSHHostWithStatus } from "@/main-axios";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
import {
|
||||
getCachedSSHHosts,
|
||||
invalidateHostsAndStatusCaches,
|
||||
@@ -10,42 +10,16 @@ import {
|
||||
// SSH HOST MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
export type GetSSHHostsOptions = {
|
||||
/** When false, skip the status service call (host config only). Default true. */
|
||||
includeStatus?: boolean;
|
||||
};
|
||||
|
||||
async function loadSSHHostsFromApi(): Promise<SSHHost[]> {
|
||||
const hostsResponse = await sshHostApi.get("/db/host");
|
||||
return Array.isArray(hostsResponse.data) ? hostsResponse.data : [];
|
||||
}
|
||||
|
||||
export async function getSSHHosts(
|
||||
options: GetSSHHostsOptions = {},
|
||||
): Promise<SSHHostWithStatus[]> {
|
||||
const includeStatus = options.includeStatus !== false;
|
||||
|
||||
/** Host config only. Live status comes from ServerStatusContext. */
|
||||
export async function getSSHHosts(): Promise<SSHHostWithStatus[]> {
|
||||
try {
|
||||
const hosts = await getCachedSSHHosts(loadSSHHostsFromApi);
|
||||
|
||||
if (!includeStatus) {
|
||||
return hosts.map((host) => ({
|
||||
...host,
|
||||
status: "unknown",
|
||||
}));
|
||||
}
|
||||
|
||||
let statuses: Record<number, ServerStatus> = {};
|
||||
try {
|
||||
statuses = (await getAllServerStatuses()) || {};
|
||||
} catch {
|
||||
// Status fetch failure should not prevent host list from loading
|
||||
}
|
||||
|
||||
return hosts.map((host) => ({
|
||||
...host,
|
||||
status: statuses[host.id]?.status || "unknown",
|
||||
}));
|
||||
return hosts.map((host) => ({ ...host, status: "unknown" }));
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "fetch SSH hosts");
|
||||
}
|
||||
|
||||
+30
-4
@@ -296,6 +296,15 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
const [pendingToken, setPendingToken] = useState("");
|
||||
const [secondFactors, setSecondFactors] = useState<SecondFactorRef[]>([]);
|
||||
const [activeFactorId, setActiveFactorId] = useState("");
|
||||
const [authPluginsLoading, setAuthPluginsLoading] = useState(true);
|
||||
const retryAuthPlugins = async () => {
|
||||
setAuthPluginsLoading(true);
|
||||
try {
|
||||
await startPreLoginPlugins({ retryFailed: true });
|
||||
} finally {
|
||||
setAuthPluginsLoading(false);
|
||||
}
|
||||
};
|
||||
const loginMethodUIs = useLoginMethods();
|
||||
const secondFactorUIs = useSecondFactors();
|
||||
// A server that did not list its factors gets every factor with a UI here.
|
||||
@@ -434,7 +443,9 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
.then((allowed) => setPasswordResetAllowed(allowed))
|
||||
.catch(() => setPasswordResetAllowed(false));
|
||||
// Plugins that draw login or second-factor UI load before sign-in.
|
||||
void startPreLoginPlugins().catch(() => {});
|
||||
void startPreLoginPlugins()
|
||||
.catch(() => {})
|
||||
.finally(() => setAuthPluginsLoading(false));
|
||||
getLoginMethods()
|
||||
.then((methods) => setAuthMethods(methods))
|
||||
.catch(() => setAuthMethods([]))
|
||||
@@ -1364,9 +1375,24 @@ export function Auth({ onLogin }: AuthProps) {
|
||||
);
|
||||
if (!ui) {
|
||||
return (
|
||||
<p className="text-xs text-destructive">
|
||||
{t("auth.secondFactorNoUI")}
|
||||
</p>
|
||||
<div className="space-y-2" aria-live="polite">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
authPluginsLoading
|
||||
? "common.loading"
|
||||
: "auth.secondFactorNoUI",
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={authPluginsLoading}
|
||||
onClick={() =>
|
||||
void retryAuthPlugins().catch(() => {})
|
||||
}
|
||||
>
|
||||
{t("auth.retrySecondFactorUI")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const Component = ui.component;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as React from "react";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
|
||||
@@ -53,6 +54,8 @@ function DialogContent({
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
@@ -73,7 +76,7 @@ function DialogContent({
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{t("common.close")}</span>
|
||||
</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
@@ -100,6 +103,8 @@ function DialogFooter({
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
@@ -112,7 +117,7 @@ function DialogFooter({
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t("common.close")}</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -188,7 +188,7 @@ function Select2({
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search..."
|
||||
placeholder={t("common.search")}
|
||||
className="h-9 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as React from "react";
|
||||
import { Dialog as SheetPrimitive } from "radix-ui";
|
||||
|
||||
@@ -53,6 +54,8 @@ function SheetContent({
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
@@ -74,7 +77,7 @@ function SheetContent({
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only">{t("common.close")}</span>
|
||||
</Button>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot } from "radix-ui";
|
||||
@@ -159,6 +160,7 @@ function Sidebar({
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
@@ -192,8 +194,10 @@ function Sidebar({
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
<SheetTitle>{t("common.sidebar")}</SheetTitle>
|
||||
<SheetDescription>
|
||||
{t("common.mobileSidebarDescription")}
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
@@ -253,6 +257,7 @@ function SidebarTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -268,22 +273,23 @@ function SidebarTrigger({
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
<span className="sr-only">{t("common.toggleSidebar")}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
aria-label={t("common.toggleSidebar")}
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
title={t("common.toggleSidebar")}
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
|
||||
@@ -57,7 +57,11 @@ import {
|
||||
useStatusColorScheme,
|
||||
getStatusClasses,
|
||||
} from "@/hooks/use-status-color-scheme";
|
||||
import { useServerStatusMeta } from "@/lib/ServerStatusContext";
|
||||
import {
|
||||
useServerStatus,
|
||||
useServerStatusMeta,
|
||||
} from "@/lib/ServerStatusContext";
|
||||
import { withLiveHostStatus } from "@/sidebar/live-host-status";
|
||||
import { sshHostToHost } from "@/sidebar/HostManagerData";
|
||||
import { getDefaultConnectionTab } from "@/lib/host-connection-tabs";
|
||||
|
||||
@@ -429,7 +433,7 @@ export function HostStatusCard({
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{online}/{hosts.length} {t("hosts.status.available")}
|
||||
{online}/{hosts.length} {t("hosts.status.online")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col overflow-auto flex-1">
|
||||
@@ -439,12 +443,7 @@ export function HostStatusCard({
|
||||
</div>
|
||||
)}
|
||||
{hosts.map((host, i) => {
|
||||
const availability =
|
||||
host.status && host.status !== "unknown"
|
||||
? host.status
|
||||
: host.online
|
||||
? "online"
|
||||
: "offline";
|
||||
const availability = host.status ?? "offline";
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@@ -492,13 +491,11 @@ export function HostStatusCard({
|
||||
<span
|
||||
className={`text-[10px] px-2 py-0.5 font-semibold border ${getStatusClasses(availability, statusScheme, "badge", statusLoading)}`}
|
||||
>
|
||||
{statusLoading
|
||||
{statusLoading || availability === "unknown"
|
||||
? t("dashboardTab.checking")
|
||||
: availability === "online"
|
||||
? t("hosts.status.available")
|
||||
: availability === "reachable"
|
||||
? t("hosts.status.reachable")
|
||||
: t("hosts.status.offline")}
|
||||
? t("hosts.status.online")
|
||||
: t("hosts.status.offline")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -582,7 +579,7 @@ function RecentActivityCard({
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<span
|
||||
className={`size-1.5 rounded-full shrink-0 ${getStatusClasses(host?.online ?? false, statusScheme, "dot", statusLoading)}`}
|
||||
className={`size-1.5 rounded-full shrink-0 ${getStatusClasses(host?.status ?? false, statusScheme, "dot", statusLoading)}`}
|
||||
/>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-xs font-semibold truncate">
|
||||
@@ -1102,7 +1099,14 @@ export function DashboardTab({
|
||||
}
|
||||
}, [mainWidthPct]);
|
||||
|
||||
const [hosts, setHosts] = useState<Host[]>([]);
|
||||
const [configHosts, setHosts] = useState<Host[]>([]);
|
||||
const { statuses, getStatus } = useServerStatus();
|
||||
const hosts = useMemo(
|
||||
() => withLiveHostStatus(configHosts, getStatus),
|
||||
// statuses changes identity whenever a host's status changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[configHosts, statuses, getStatus],
|
||||
);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [uptimeFormatted, setUptimeFormatted] = useState("");
|
||||
const [versionText, setVersionText] = useState("");
|
||||
|
||||
@@ -48,14 +48,13 @@ export function useStatusColorScheme(): StatusColorScheme {
|
||||
|
||||
/** Returns Tailwind class names for a status dot/stripe. */
|
||||
export function getStatusClasses(
|
||||
status: boolean | "online" | "reachable" | "offline" | "degraded",
|
||||
status: boolean | "online" | "offline" | "unknown",
|
||||
scheme: StatusColorScheme,
|
||||
variant: "dot" | "stripe" | "badge",
|
||||
loading = false,
|
||||
): string {
|
||||
const online = status === true || status === "online";
|
||||
const reachable = status === "reachable";
|
||||
if (loading) {
|
||||
if (loading || status === "unknown") {
|
||||
if (scheme === "status") {
|
||||
if (variant === "dot") return "bg-yellow-400 animate-pulse";
|
||||
if (variant === "stripe") return "bg-yellow-400/40 animate-pulse";
|
||||
@@ -65,11 +64,6 @@ export function getStatusClasses(
|
||||
if (variant === "stripe") return "bg-muted-foreground/20 animate-pulse";
|
||||
return "border-border/50 text-muted-foreground/50 bg-muted/20 animate-pulse";
|
||||
}
|
||||
if (reachable) {
|
||||
if (variant === "dot") return "bg-amber-400";
|
||||
if (variant === "stripe") return "bg-amber-400/50";
|
||||
return "border-amber-400/40 text-amber-400 bg-amber-400/10";
|
||||
}
|
||||
if (scheme === "status") {
|
||||
if (variant === "dot") return online ? "bg-emerald-500" : "bg-red-500";
|
||||
if (variant === "stripe")
|
||||
|
||||
@@ -9,7 +9,8 @@ import React, {
|
||||
useSyncExternalStore,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getAllServerStatuses, getSSHHosts } from "@/main-axios";
|
||||
import { getAllServerStatuses } from "@/main-axios";
|
||||
import { invalidateServerStatusCache } from "./hosts-request-cache";
|
||||
import {
|
||||
ServerStatusStore,
|
||||
type ServerStatusEntry,
|
||||
@@ -19,17 +20,26 @@ import { runAdaptivePolling } from "./adaptive-polling";
|
||||
|
||||
interface ServerStatusContextType {
|
||||
statuses: Map<number, ServerStatusEntry>;
|
||||
isLoading: boolean;
|
||||
initialLoadComplete: boolean;
|
||||
refreshStatuses: () => Promise<void>;
|
||||
/** "unknown" until the host has been checked. */
|
||||
getStatus: (hostId: number) => StatusValue;
|
||||
}
|
||||
|
||||
/** Stable for the provider lifetime — fine-grained hooks only need this. */
|
||||
/** Stable for the provider lifetime. Fine-grained hooks only need this. */
|
||||
const StatusStoreContext = createContext<ServerStatusStore | null>(null);
|
||||
const ServerStatusContext = createContext<ServerStatusContextType | null>(null);
|
||||
|
||||
const POLL_INTERVAL = 30000;
|
||||
/** The mounted provider's store, for code outside React (plugin host lists). */
|
||||
let activeStore: ServerStatusStore | null = null;
|
||||
|
||||
/** A host's live status outside React. "unknown" with no provider mounted. */
|
||||
export function getLiveHostStatus(hostId: number): StatusValue {
|
||||
return activeStore?.getStatus(hostId) ?? "unknown";
|
||||
}
|
||||
|
||||
/** The backend keeps statuses in memory, so reading them often is cheap. */
|
||||
const POLL_INTERVAL = 15_000;
|
||||
|
||||
export function ServerStatusProvider({
|
||||
children,
|
||||
@@ -47,121 +57,48 @@ export function ServerStatusProvider({
|
||||
// Bumps only full-context consumers (dashboard, folder counts, etc.).
|
||||
const [version, setVersion] = useState(0);
|
||||
const mountedRef = useRef(true);
|
||||
const refreshInFlightRef = useRef<Promise<boolean | void> | null>(null);
|
||||
const refreshInFlightRef = useRef<Promise<void> | null>(null);
|
||||
|
||||
useEffect(() => store.subscribeAll(() => setVersion((v) => v + 1)), [store]);
|
||||
|
||||
useEffect(() => {
|
||||
return store.subscribeAll(() => {
|
||||
setVersion((v) => v + 1);
|
||||
});
|
||||
activeStore = store;
|
||||
return () => {
|
||||
if (activeStore === store) activeStore = null;
|
||||
};
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
return store.subscribeMeta(() => {
|
||||
setVersion((v) => v + 1);
|
||||
});
|
||||
}, [store]);
|
||||
|
||||
const fetchEnabledHosts = useCallback(async () => {
|
||||
if (!isAuthenticated) {
|
||||
store.setEnabledHostIds(new Set());
|
||||
return new Set<number>();
|
||||
}
|
||||
|
||||
try {
|
||||
const hosts = await getSSHHosts({ includeStatus: false });
|
||||
const enabled = new Set<number>();
|
||||
|
||||
hosts.forEach((host) => {
|
||||
if (host.statusCheckEnabled !== false) enabled.add(host.id);
|
||||
});
|
||||
|
||||
store.setEnabledHostIds(enabled);
|
||||
return enabled;
|
||||
} catch {
|
||||
return store.getEnabledHostIds();
|
||||
}
|
||||
}, [isAuthenticated, store]);
|
||||
|
||||
const refreshStatusesImpl = useCallback(
|
||||
async (rethrow = false) => {
|
||||
if (!mountedRef.current || !isAuthenticated) return;
|
||||
if (
|
||||
typeof document !== "undefined" &&
|
||||
document.visibilityState === "hidden"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (refreshInFlightRef.current) {
|
||||
return refreshInFlightRef.current;
|
||||
}
|
||||
|
||||
const showLoading = !store.getInitialLoadComplete();
|
||||
if (showLoading) store.setLoading(true);
|
||||
|
||||
const run = (async () => {
|
||||
try {
|
||||
const data = await getAllServerStatuses();
|
||||
if (!mountedRef.current) return;
|
||||
|
||||
const newStatuses = new Map<number, ServerStatusEntry>();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (data && typeof data === "object") {
|
||||
Object.entries(data).forEach(([idStr, statusData]) => {
|
||||
const id = parseInt(idStr, 10);
|
||||
if (!isNaN(id)) {
|
||||
const status =
|
||||
statusData?.status === "online" ||
|
||||
statusData?.status === "reachable"
|
||||
? statusData.status
|
||||
: "offline";
|
||||
newStatuses.set(id, {
|
||||
status,
|
||||
lastChecked: statusData?.lastChecked || now,
|
||||
...(statusData?.reason === "host_key_changed"
|
||||
? { reason: statusData.reason }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const previousStatuses = store.getStatuses();
|
||||
const changed =
|
||||
previousStatuses.size !== newStatuses.size ||
|
||||
[...newStatuses].some(
|
||||
([id, entry]) =>
|
||||
previousStatuses.get(id)?.status !== entry.status ||
|
||||
previousStatuses.get(id)?.reason !== entry.reason,
|
||||
);
|
||||
store.applyStatuses(newStatuses);
|
||||
return changed;
|
||||
} catch (error) {
|
||||
if (mountedRef.current) {
|
||||
store.markDegraded(store.getEnabledHostIds());
|
||||
}
|
||||
if (rethrow) throw error;
|
||||
return false;
|
||||
} finally {
|
||||
if (mountedRef.current) {
|
||||
if (showLoading) store.setLoading(false);
|
||||
store.setInitialLoadComplete(true);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
refreshInFlightRef.current = run.finally(() => {
|
||||
refreshInFlightRef.current = null;
|
||||
});
|
||||
return refreshInFlightRef.current;
|
||||
},
|
||||
[isAuthenticated, store],
|
||||
);
|
||||
|
||||
const refreshStatuses = useCallback(async () => {
|
||||
await refreshStatusesImpl();
|
||||
}, [refreshStatusesImpl]);
|
||||
if (!mountedRef.current || !isAuthenticated) return;
|
||||
if (refreshInFlightRef.current) return refreshInFlightRef.current;
|
||||
|
||||
const run = (async () => {
|
||||
try {
|
||||
const data = await getAllServerStatuses();
|
||||
if (!mountedRef.current || !data || typeof data !== "object") return;
|
||||
const next = new Map<number, ServerStatusEntry>();
|
||||
const now = new Date().toISOString();
|
||||
for (const [idStr, entry] of Object.entries(data)) {
|
||||
const id = parseInt(idStr, 10);
|
||||
if (isNaN(id)) continue;
|
||||
next.set(id, {
|
||||
status: entry?.status === "online" ? "online" : "offline",
|
||||
lastChecked: entry?.lastChecked || now,
|
||||
});
|
||||
}
|
||||
store.applyStatuses(next);
|
||||
} catch {
|
||||
// Keep the last known statuses rather than flipping every host.
|
||||
} finally {
|
||||
if (mountedRef.current) store.setInitialLoadComplete(true);
|
||||
}
|
||||
})();
|
||||
|
||||
refreshInFlightRef.current = run.finally(() => {
|
||||
refreshInFlightRef.current = null;
|
||||
});
|
||||
return refreshInFlightRef.current;
|
||||
}, [isAuthenticated, store]);
|
||||
|
||||
const getStatus = useCallback(
|
||||
(hostId: number): StatusValue => store.getStatus(hostId),
|
||||
@@ -170,35 +107,31 @@ export function ServerStatusProvider({
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
if (!isAuthenticated) {
|
||||
store.clear();
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}
|
||||
|
||||
let stopPolling: (() => void) | null = null;
|
||||
|
||||
const init = async () => {
|
||||
await fetchEnabledHosts();
|
||||
if (!mountedRef.current) return;
|
||||
stopPolling = runAdaptivePolling(
|
||||
() => refreshStatusesImpl(true),
|
||||
{
|
||||
minIntervalMs: POLL_INTERVAL,
|
||||
maxIntervalMs: 120_000,
|
||||
stablePollsPerStep: 3,
|
||||
},
|
||||
{ enabled: () => isAuthenticated },
|
||||
);
|
||||
};
|
||||
|
||||
void init();
|
||||
const stopPolling = runAdaptivePolling(refreshStatuses, {
|
||||
minIntervalMs: POLL_INTERVAL,
|
||||
maxIntervalMs: POLL_INTERVAL,
|
||||
});
|
||||
const onFocus = () => void refreshStatuses();
|
||||
window.addEventListener("focus", onFocus);
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
stopPolling?.();
|
||||
stopPolling();
|
||||
window.removeEventListener("focus", onFocus);
|
||||
};
|
||||
}, [fetchEnabledHosts, isAuthenticated, refreshStatusesImpl]);
|
||||
}, [isAuthenticated, refreshStatuses, store]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleHostsChanged = async () => {
|
||||
await fetchEnabledHosts();
|
||||
await refreshStatuses();
|
||||
const handleHostsChanged = () => {
|
||||
invalidateServerStatusCache();
|
||||
void refreshStatuses();
|
||||
};
|
||||
|
||||
window.addEventListener("ssh-hosts:changed", handleHostsChanged);
|
||||
@@ -208,17 +141,16 @@ export function ServerStatusProvider({
|
||||
window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
|
||||
window.removeEventListener("hosts:refresh", handleHostsChanged);
|
||||
};
|
||||
}, [fetchEnabledHosts, refreshStatuses]);
|
||||
}, [refreshStatuses]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
statuses: store.getStatuses(),
|
||||
isLoading: store.getIsLoading(),
|
||||
initialLoadComplete: store.getInitialLoadComplete(),
|
||||
refreshStatuses,
|
||||
getStatus,
|
||||
}),
|
||||
// version refreshes statuses/isLoading/initialLoadComplete snapshots
|
||||
// version refreshes the statuses/initialLoadComplete snapshots
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[version, refreshStatuses, getStatus],
|
||||
);
|
||||
@@ -252,85 +184,49 @@ export function useServerStatus() {
|
||||
|
||||
/**
|
||||
* Subscribe to a single host's status. Only re-renders when that host's
|
||||
* status value changes (or its enabled flag flips). Does not re-render when
|
||||
* other hosts update.
|
||||
* status changes. Null when status checks are off for the host.
|
||||
*/
|
||||
export function useHostStatus(
|
||||
hostId: number,
|
||||
statusCheckEnabled: boolean = true,
|
||||
): StatusValue | null {
|
||||
const store = useStatusStore();
|
||||
|
||||
const snapshot = useSyncExternalStore(
|
||||
const status = useSyncExternalStore(
|
||||
(onChange) => store.subscribeHost(hostId, onChange),
|
||||
() => store.getHostSnapshot(hostId),
|
||||
() => store.getHostSnapshot(hostId),
|
||||
() => store.getStatus(hostId),
|
||||
() => store.getStatus(hostId),
|
||||
);
|
||||
|
||||
if (!statusCheckEnabled) {
|
||||
return null;
|
||||
}
|
||||
return snapshot.split(":", 1)[0] as StatusValue;
|
||||
}
|
||||
|
||||
export function useHostStatusReason(
|
||||
hostId: number,
|
||||
statusCheckEnabled: boolean = true,
|
||||
): ServerStatusEntry["reason"] | null {
|
||||
const store = useStatusStore();
|
||||
const snapshot = useSyncExternalStore(
|
||||
(onChange) => store.subscribeHost(hostId, onChange),
|
||||
() => store.getHostSnapshot(hostId),
|
||||
() => store.getHostSnapshot(hostId),
|
||||
);
|
||||
if (!statusCheckEnabled) return null;
|
||||
return snapshot.endsWith(":host_key_changed")
|
||||
? "host_key_changed"
|
||||
: undefined;
|
||||
return statusCheckEnabled ? status : null;
|
||||
}
|
||||
|
||||
/** Meta flags without depending on the full status map. */
|
||||
export function useServerStatusMeta(): {
|
||||
initialLoadComplete: boolean;
|
||||
isLoading: boolean;
|
||||
} {
|
||||
export function useServerStatusMeta(): { initialLoadComplete: boolean } {
|
||||
const store = useStatusStore();
|
||||
|
||||
useSyncExternalStore(
|
||||
(onChange) => store.subscribeMeta(onChange),
|
||||
() => store.getMetaSnapshot(),
|
||||
() => store.getMetaSnapshot(),
|
||||
);
|
||||
|
||||
return {
|
||||
initialLoadComplete: store.getInitialLoadComplete(),
|
||||
isLoading: store.getIsLoading(),
|
||||
};
|
||||
return { initialLoadComplete: store.getInitialLoadComplete() };
|
||||
}
|
||||
|
||||
const noStatus = () => "";
|
||||
const noStatus = (): StatusValue => "unknown";
|
||||
|
||||
/**
|
||||
* One host's status for plugin views, which can render outside the app shell
|
||||
* (a standalone window). Null there, and while the host has not been checked.
|
||||
*/
|
||||
export function useOptionalHostStatusEntry(hostId: number | undefined): {
|
||||
status: StatusValue;
|
||||
reason?: "host_key_changed";
|
||||
} | null {
|
||||
export function useOptionalHostStatusEntry(
|
||||
hostId: number | undefined,
|
||||
): { status: "online" | "offline" } | null {
|
||||
const store = useContext(StatusStoreContext);
|
||||
const snapshot = useSyncExternalStore(
|
||||
const status = useSyncExternalStore(
|
||||
(onChange) =>
|
||||
store && hostId !== undefined
|
||||
? store.subscribeHost(hostId, onChange)
|
||||
: () => {},
|
||||
() => (store && hostId !== undefined ? store.getHostSnapshot(hostId) : ""),
|
||||
() => (store && hostId !== undefined ? store.getStatus(hostId) : "unknown"),
|
||||
noStatus,
|
||||
);
|
||||
if (!snapshot) return null;
|
||||
const [status] = snapshot.split(":", 1);
|
||||
if (!status) return null;
|
||||
return snapshot.endsWith(":host_key_changed")
|
||||
? { status: status as StatusValue, reason: "host_key_changed" }
|
||||
: { status: status as StatusValue };
|
||||
return status === "unknown" ? null : { status };
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
export type StatusValue = "online" | "reachable" | "offline" | "degraded";
|
||||
export type StatusValue = "online" | "offline" | "unknown";
|
||||
|
||||
export interface ServerStatusEntry {
|
||||
status: StatusValue;
|
||||
status: "online" | "offline";
|
||||
lastChecked: string;
|
||||
reason?: "host_key_changed";
|
||||
}
|
||||
|
||||
type Listener = () => void;
|
||||
@@ -14,44 +13,26 @@ type Listener = () => void;
|
||||
*/
|
||||
export class ServerStatusStore {
|
||||
private statuses = new Map<number, ServerStatusEntry>();
|
||||
private enabledHostIds = new Set<number>();
|
||||
private initialLoadComplete = false;
|
||||
private isLoading = false;
|
||||
private readonly hostListeners = new Map<number, Set<Listener>>();
|
||||
private readonly allListeners = new Set<Listener>();
|
||||
private readonly metaListeners = new Set<Listener>();
|
||||
|
||||
/** "unknown" until the host has been checked. */
|
||||
getStatus(hostId: number): StatusValue {
|
||||
if (!this.enabledHostIds.has(hostId)) {
|
||||
return "offline";
|
||||
}
|
||||
return this.statuses.get(hostId)?.status || "degraded";
|
||||
return this.statuses.get(hostId)?.status ?? "unknown";
|
||||
}
|
||||
|
||||
getStatuses(): Map<number, ServerStatusEntry> {
|
||||
return this.statuses;
|
||||
}
|
||||
|
||||
getEnabledHostIds(): Set<number> {
|
||||
return this.enabledHostIds;
|
||||
}
|
||||
|
||||
getInitialLoadComplete(): boolean {
|
||||
return this.initialLoadComplete;
|
||||
}
|
||||
|
||||
getIsLoading(): boolean {
|
||||
return this.isLoading;
|
||||
}
|
||||
|
||||
/** Snapshot string for a host — used as useSyncExternalStore getSnapshot. */
|
||||
getHostSnapshot(hostId: number): string {
|
||||
const status = this.getStatus(hostId);
|
||||
return `${status}:${this.statuses.get(hostId)?.reason ?? ""}`;
|
||||
}
|
||||
|
||||
getMetaSnapshot(): string {
|
||||
return `${this.initialLoadComplete ? 1 : 0}:${this.isLoading ? 1 : 0}:${this.enabledHostIds.size}`;
|
||||
return this.initialLoadComplete ? "1" : "0";
|
||||
}
|
||||
|
||||
subscribeHost(hostId: number, listener: Listener): () => void {
|
||||
@@ -83,28 +64,6 @@ export class ServerStatusStore {
|
||||
};
|
||||
}
|
||||
|
||||
setEnabledHostIds(next: Set<number>): void {
|
||||
if (setsEqual(this.enabledHostIds, next)) return;
|
||||
const prev = this.enabledHostIds;
|
||||
this.enabledHostIds = next;
|
||||
this.emitMeta();
|
||||
// Hosts that entered/left the enabled set need a status re-read.
|
||||
for (const id of next) {
|
||||
if (!prev.has(id)) this.emitHost(id);
|
||||
}
|
||||
for (const id of prev) {
|
||||
if (!next.has(id)) this.emitHost(id);
|
||||
}
|
||||
this.emitAll();
|
||||
}
|
||||
|
||||
setLoading(loading: boolean): void {
|
||||
if (this.isLoading === loading) return;
|
||||
this.isLoading = loading;
|
||||
this.emitMeta();
|
||||
this.emitAll();
|
||||
}
|
||||
|
||||
setInitialLoadComplete(complete: boolean): void {
|
||||
if (this.initialLoadComplete === complete) return;
|
||||
this.initialLoadComplete = complete;
|
||||
@@ -120,50 +79,24 @@ export class ServerStatusStore {
|
||||
const changedIds: number[] = [];
|
||||
|
||||
for (const [id, entry] of next) {
|
||||
const prev = this.statuses.get(id);
|
||||
if (
|
||||
!prev ||
|
||||
prev.status !== entry.status ||
|
||||
prev.reason !== entry.reason
|
||||
) {
|
||||
changedIds.push(id);
|
||||
}
|
||||
if (this.statuses.get(id)?.status !== entry.status) changedIds.push(id);
|
||||
}
|
||||
for (const id of this.statuses.keys()) {
|
||||
if (!next.has(id)) {
|
||||
changedIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedIds.length === 0) {
|
||||
// Still refresh lastChecked silently without notifying.
|
||||
this.statuses = next;
|
||||
return;
|
||||
if (!next.has(id)) changedIds.push(id);
|
||||
}
|
||||
|
||||
this.statuses = next;
|
||||
if (changedIds.length === 0) return;
|
||||
for (const id of changedIds) {
|
||||
this.emitHost(id);
|
||||
}
|
||||
this.emitAll();
|
||||
}
|
||||
|
||||
markDegraded(enabledIds: Iterable<number>): void {
|
||||
const next = new Map(this.statuses);
|
||||
let changed = false;
|
||||
const now = new Date().toISOString();
|
||||
for (const id of enabledIds) {
|
||||
const existing = next.get(id);
|
||||
if (existing?.status === "degraded") continue;
|
||||
changed = true;
|
||||
next.set(id, {
|
||||
status: "degraded",
|
||||
lastChecked: existing?.lastChecked || now,
|
||||
});
|
||||
}
|
||||
if (changed) {
|
||||
this.applyStatuses(next);
|
||||
}
|
||||
/** Forgets everything, on sign out. */
|
||||
clear(): void {
|
||||
this.applyStatuses(new Map());
|
||||
this.setInitialLoadComplete(false);
|
||||
}
|
||||
|
||||
private emitHost(hostId: number): void {
|
||||
@@ -180,11 +113,3 @@ export class ServerStatusStore {
|
||||
for (const listener of this.metaListeners) listener();
|
||||
}
|
||||
}
|
||||
|
||||
function setsEqual(a: Set<number>, b: Set<number>): boolean {
|
||||
if (a.size !== b.size) return false;
|
||||
for (const id of a) {
|
||||
if (!b.has(id)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+24
-9
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Update available. Open the release notes on GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Close",
|
||||
"minimize": "Minimize",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Sign in failed",
|
||||
"unknown": "Unknown",
|
||||
"continue": "Continue",
|
||||
"maintenance": "Maintenance",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Remove",
|
||||
"add": "Add",
|
||||
"update": "Update",
|
||||
"noResults": "No results found"
|
||||
"noResults": "No results found",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Step {{current}} of {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Host Metrics",
|
||||
"status": {
|
||||
"available": "Available",
|
||||
"reachable": "Up, but the last sign in failed",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitoring disabled",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "This host (direct tunnel)",
|
||||
"statusChecksLabel": "Status Checks",
|
||||
"enableStatusChecks": "Enable Status Checks",
|
||||
"enableStatusChecksDesc": "Periodically ping this host to verify availability",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Use Global Interval",
|
||||
"useGlobalIntervalDesc": "Override with the server-wide status check interval",
|
||||
"checkIntervalS": "Check Interval (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Your account uses a second factor that is not available right now. Contact an admin to reset it.",
|
||||
"secondFactorNoUI": "This second factor cannot be shown on this screen. Contact an admin.",
|
||||
"serverAuthentication": "Server sign-in",
|
||||
"minChars": "min. {{min}} characters"
|
||||
"minChars": "min. {{min}} characters",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verify SSH Host Key",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Language",
|
||||
"keyPassword": "key password",
|
||||
"pastePrivateKey": "Paste your private key here...",
|
||||
"enterPassword": "Enter your password"
|
||||
"enterPassword": "Enter your password",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Issued by {{issuer}}",
|
||||
"sslNames": "Covers {{names}}",
|
||||
"sslManualTitle": "Upload a certificate",
|
||||
"updateExternalAutoProvisionFailed": "Failed to update the auto-create setting"
|
||||
"updateExternalAutoProvisionFailed": "Failed to update the auto-create setting",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Opdatering beskikbaar. Maak die vrystellingsnotas op GitHub oop."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Maak toe",
|
||||
"minimize": "Minimaliseer",
|
||||
"online": "Aanlyn",
|
||||
"offline": "Vanlyn",
|
||||
"signInFailed": "Aanmelding het misluk",
|
||||
"unknown": "Onbekend",
|
||||
"continue": "Gaan voort",
|
||||
"maintenance": "Onderhoud",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Verwyder",
|
||||
"add": "Voeg by",
|
||||
"update": "Opdatering",
|
||||
"noResults": "Geen resultate gevind nie"
|
||||
"noResults": "Geen resultate gevind nie",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Stap {{current}} van {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tonnel",
|
||||
"serverStats": "Gasheermetrieke",
|
||||
"status": {
|
||||
"available": "Beskikbaar",
|
||||
"reachable": "Aan, maar die laaste aanmelding het misluk",
|
||||
"online": "Online",
|
||||
"offline": "Vanlyn",
|
||||
"monitoringDisabled": "Monitering gedeaktiveer",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Lêer \"{{oldName}}\" is suksesvol hernoem na \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Hierdie gasheer (direkte tonnel)",
|
||||
"statusChecksLabel": "Statuskontroles",
|
||||
"enableStatusChecks": "Aktiveer statuskontroles",
|
||||
"enableStatusChecksDesc": "Ping hierdie gasheer gereeld om beskikbaarheid te verifieer",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Gebruik globale interval",
|
||||
"useGlobalIntervalDesc": "Oorskryf met die bedienerwye statuskontrole-interval",
|
||||
"checkIntervalS": "Kontrole-interval (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Jou rekening gebruik 'n tweede faktor wat tans nie beskikbaar is nie. Kontak 'n administrateur om dit terug te stel.",
|
||||
"secondFactorNoUI": "Hierdie tweede faktor kan nie op hierdie skerm gewys word nie. Kontak 'n administrateur.",
|
||||
"serverAuthentication": "Bediener-aanmelding",
|
||||
"minChars": "min. {{min}} karakters"
|
||||
"minChars": "min. {{min}} karakters",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verifieer SSH-gasheersleutel",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Taal",
|
||||
"keyPassword": "sleutelwagwoord",
|
||||
"pastePrivateKey": "Plak jou privaat sleutel hier...",
|
||||
"enterPassword": "Voer jou wagwoord in"
|
||||
"enterPassword": "Voer jou wagwoord in",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Uitgereik deur {{issuer}}",
|
||||
"sslNames": "Omslae {{names}}",
|
||||
"sslManualTitle": "Laai 'n sertifikaat op",
|
||||
"updateExternalAutoProvisionFailed": "Kon nie die outomatiese skep-instelling opdateer nie"
|
||||
"updateExternalAutoProvisionFailed": "Kon nie die outomatiese skep-instelling opdateer nie",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "يتوفر تحديث. اطلع على ملاحظات الإصدار على GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "إغلاق",
|
||||
"minimize": "تصغير",
|
||||
"online": "متصل",
|
||||
"offline": "غير متصل",
|
||||
"signInFailed": "فشل تسجيل الدخول",
|
||||
"unknown": "غير معروف",
|
||||
"continue": "متابعة",
|
||||
"maintenance": "صيانة",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "إزالة",
|
||||
"add": "إضافة",
|
||||
"update": "تحديث",
|
||||
"noResults": "لم يتم العثور على نتائج"
|
||||
"noResults": "لم يتم العثور على نتائج",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "الخطوة {{current}} من {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "النفق",
|
||||
"serverStats": "مقاييس المضيف",
|
||||
"status": {
|
||||
"available": "متاح",
|
||||
"reachable": "تم تسجيل الدخول، لكن آخر تسجيل دخول فشل",
|
||||
"online": "Online",
|
||||
"offline": "غير متصل بالإنترنت",
|
||||
"monitoringDisabled": "تم تعطيل المراقبة",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "حالة"
|
||||
},
|
||||
"folderRenamed": "تمت إعادة تسمية المجلد \"{{oldName}}\" إلى \"{{newName}}\" بنجاح",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "هذا المضيف (نفق مباشر)",
|
||||
"statusChecksLabel": "فحوصات الحالة",
|
||||
"enableStatusChecks": "تمكين فحوصات الحالة",
|
||||
"enableStatusChecksDesc": "إرسال ping دوريًا لهذا المضيف للتحقق من توفره",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "استخدام الفاصل الزمني العام",
|
||||
"useGlobalIntervalDesc": "تجاوز باستخدام فترة فحص الحالة على مستوى الخادم",
|
||||
"checkIntervalS": "فترة الفحص (ثانية)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "يستخدم حسابك عامل مصادقة ثانٍ غير متوفر حاليًا. تواصل مع أحد المسؤولين لإعادة تعيينه.",
|
||||
"secondFactorNoUI": "لا يمكن عرض هذا العامل الثاني على هذه الشاشة. تواصل مع أحد المسؤولين.",
|
||||
"serverAuthentication": "تسجيل الدخول إلى الخادم",
|
||||
"minChars": "الحد الأدنى {{min}} حرفًا"
|
||||
"minChars": "الحد الأدنى {{min}} حرفًا",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "تحقق من مفتاح مضيف SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "اللغة",
|
||||
"keyPassword": "كلمة مرور المفتاح",
|
||||
"pastePrivateKey": "الصق مفتاحك الخاص هنا...",
|
||||
"enterPassword": "أدخل كلمة المرور"
|
||||
"enterPassword": "أدخل كلمة المرور",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "لوحة التحكم",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "صادر عن {{issuer}}",
|
||||
"sslNames": "يغطي {{names}}",
|
||||
"sslManualTitle": "قم بتحميل شهادة",
|
||||
"updateExternalAutoProvisionFailed": "فشل تحديث إعداد الإنشاء التلقائي"
|
||||
"updateExternalAutoProvisionFailed": "فشل تحديث إعداد الإنشاء التلقائي",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Налична е актуализация. Отворете бележките за изданието в GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Затвори",
|
||||
"minimize": "Минимизиране",
|
||||
"online": "Онлайн",
|
||||
"offline": "Офлайн",
|
||||
"signInFailed": "Влизането не бе успешно",
|
||||
"unknown": "Неизвестен",
|
||||
"continue": "Продължи",
|
||||
"maintenance": "Поддръжка",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Премахване",
|
||||
"add": "Добавяне",
|
||||
"update": "Актуализация",
|
||||
"noResults": "Няма намерени резултати"
|
||||
"noResults": "Няма намерени резултати",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Стъпка {{current}} от {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Тунел",
|
||||
"serverStats": "Метрики на хоста",
|
||||
"status": {
|
||||
"available": "Налично",
|
||||
"reachable": "Включено, но последното влизане не беше успешно",
|
||||
"online": "Online",
|
||||
"offline": "Офлайн",
|
||||
"monitoringDisabled": "Мониторингът е деактивиран",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Статус"
|
||||
},
|
||||
"folderRenamed": "Папка „{{oldName}}“ е преименувана успешно на „{{newName}}“",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Този хост (директен тунел)",
|
||||
"statusChecksLabel": "Проверки на състоянието",
|
||||
"enableStatusChecks": "Активиране на проверки на състоянието",
|
||||
"enableStatusChecksDesc": "Периодично проверявайте наличността на този хост чрез ping.",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Използване на глобален интервал",
|
||||
"useGlobalIntervalDesc": "Замяна с интервала за проверка на състоянието на целия сървър",
|
||||
"checkIntervalS": "Интервал(и) на проверка",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Вашият акаунт използва втори фактор, който не е наличен в момента. Свържете се с администратор, за да го нулира.",
|
||||
"secondFactorNoUI": "Този втори фактор не може да бъде показан на този екран. Свържете се с администратор.",
|
||||
"serverAuthentication": "Влизане в сървъра",
|
||||
"minChars": "мин. {{min}} символа"
|
||||
"minChars": "мин. {{min}} символа",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Проверете SSH хост ключа",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Език",
|
||||
"keyPassword": "ключова парола",
|
||||
"pastePrivateKey": "Поставете личния си ключ тук...",
|
||||
"enterPassword": "Въведете паролата си"
|
||||
"enterPassword": "Въведете паролата си",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Табло за управление",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Издадено от {{issuer}}",
|
||||
"sslNames": "Обхваща {{names}}",
|
||||
"sslManualTitle": "Качване на сертификат",
|
||||
"updateExternalAutoProvisionFailed": "Актуализирането на настройката за автоматично създаване не бе успешно"
|
||||
"updateExternalAutoProvisionFailed": "Актуализирането на настройката за автоматично създаване не бе успешно",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "আপডেট উপলব্ধ। গিটহাবে রিলিজ নোট খুলুন।"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "বন্ধ করুন",
|
||||
"minimize": "ন্যূনতম করুন",
|
||||
"online": "অনলাইন",
|
||||
"offline": "অফলাইন",
|
||||
"signInFailed": "সাইন ইন ব্যর্থ হয়েছে",
|
||||
"unknown": "অজানা",
|
||||
"continue": "চালিয়ে যান",
|
||||
"maintenance": "রক্ষণাবেক্ষণ",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "অপসারণ করুন",
|
||||
"add": "যোগ করুন",
|
||||
"update": "আপডেট",
|
||||
"noResults": "কোনো ফলাফল পাওয়া যায়নি"
|
||||
"noResults": "কোনো ফলাফল পাওয়া যায়নি",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "{{total}} এর {{current}} ধাপ",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "টানেল",
|
||||
"serverStats": "হোস্ট মেট্রিক্স",
|
||||
"status": {
|
||||
"available": "উপলব্ধ",
|
||||
"reachable": "চালু হয়েছে, কিন্তু শেষ সাইন-ইনটি ব্যর্থ হয়েছে।",
|
||||
"online": "Online",
|
||||
"offline": "অফলাইন",
|
||||
"monitoringDisabled": "পর্যবেক্ষণ অক্ষম করা হয়েছে",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "অবস্থা"
|
||||
},
|
||||
"folderRenamed": "\"{{oldName}}\" ফোল্ডারটির নাম সফলভাবে \"{{newName}}\" রাখা হয়েছে।",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "এই হোস্ট (সরাসরি টানেল)",
|
||||
"statusChecksLabel": "অবস্থা যাচাই",
|
||||
"enableStatusChecks": "স্ট্যাটাস চেক সক্ষম করুন",
|
||||
"enableStatusChecksDesc": "প্রাপ্যতা যাচাই করতে পর্যায়ক্রমে এই হোস্টটিকে পিং করুন।",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "গ্লোবাল ইন্টারভাল ব্যবহার করুন",
|
||||
"useGlobalIntervalDesc": "সার্ভার-ব্যাপী স্ট্যাটাস চেক ব্যবধান দিয়ে ওভাররাইড করুন",
|
||||
"checkIntervalS": "চেক ব্যবধান (গুলি)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "আপনার অ্যাকাউন্টে একটি দ্বিতীয় ফ্যাক্টর ব্যবহৃত হয় যা এই মুহূর্তে উপলব্ধ নেই। এটি রিসেট করতে একজন অ্যাডমিনের সাথে যোগাযোগ করুন।",
|
||||
"secondFactorNoUI": "এই দ্বিতীয় ফ্যাক্টরটি এই স্ক্রিনে দেখানো যাবে না। একজন অ্যাডমিনের সাথে যোগাযোগ করুন।",
|
||||
"serverAuthentication": "সার্ভার সাইন-ইন",
|
||||
"minChars": "সর্বনিম্ন {{min}} অক্ষর"
|
||||
"minChars": "সর্বনিম্ন {{min}} অক্ষর",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "SSH হোস্ট কী যাচাই করুন",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "ভাষা",
|
||||
"keyPassword": "কী পাসওয়ার্ড",
|
||||
"pastePrivateKey": "আপনার প্রাইভেট কী এখানে পেস্ট করুন...",
|
||||
"enterPassword": "আপনার পাসওয়ার্ড লিখুন"
|
||||
"enterPassword": "আপনার পাসওয়ার্ড লিখুন",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "ড্যাশবোর্ড",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "{{issuer}} দ্বারা জারি করা হয়েছে",
|
||||
"sslNames": "{{names}} কভার করে",
|
||||
"sslManualTitle": "একটি সার্টিফিকেট আপলোড করুন",
|
||||
"updateExternalAutoProvisionFailed": "স্বয়ংক্রিয়-সৃষ্টি সেটিং আপডেট করতে ব্যর্থ হয়েছে"
|
||||
"updateExternalAutoProvisionFailed": "স্বয়ংক্রিয়-সৃষ্টি সেটিং আপডেট করতে ব্যর্থ হয়েছে",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Actualització disponible. Obriu les notes de la versió a GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Tanca",
|
||||
"minimize": "Minimitzar",
|
||||
"online": "En línia",
|
||||
"offline": "Fora de línia",
|
||||
"signInFailed": "No s'ha pogut iniciar la sessió",
|
||||
"unknown": "Desconegut",
|
||||
"continue": "Continua",
|
||||
"maintenance": "Manteniment",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Elimina",
|
||||
"add": "Afegeix",
|
||||
"update": "Actualització",
|
||||
"noResults": "No s'han trobat resultats"
|
||||
"noResults": "No s'han trobat resultats",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Pas {{current}} de {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Túnel",
|
||||
"serverStats": "Mètriques de l'amfitrió",
|
||||
"status": {
|
||||
"available": "Disponible",
|
||||
"reachable": "Activat, però l'últim inici de sessió ha fallat",
|
||||
"online": "Online",
|
||||
"offline": "Fora de línia",
|
||||
"monitoringDisabled": "Monitorització desactivada",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Estat"
|
||||
},
|
||||
"folderRenamed": "La carpeta \"{{oldName}}\" s'ha canviat de nom a \"{{newName}}\" correctament",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Aquest host (túnel directe)",
|
||||
"statusChecksLabel": "Comprovacions d'estat",
|
||||
"enableStatusChecks": "Activa les comprovacions d'estat",
|
||||
"enableStatusChecksDesc": "Fes ping periòdicament a aquest host per verificar la disponibilitat",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Utilitza l'interval global",
|
||||
"useGlobalIntervalDesc": "Substitueix amb l'interval de comprovació d'estat de tot el servidor",
|
||||
"checkIntervalS": "Interval (s) de comprovació",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "El teu compte utilitza un segon factor que no està disponible ara mateix. Contacta amb un administrador per restablir-lo.",
|
||||
"secondFactorNoUI": "Aquest segon factor no es pot mostrar en aquesta pantalla. Contacta amb un administrador.",
|
||||
"serverAuthentication": "Inici de sessió al servidor",
|
||||
"minChars": "mínim {{min}} caràcters"
|
||||
"minChars": "mínim {{min}} caràcters",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verifica la clau d'amfitrió SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Llengua",
|
||||
"keyPassword": "contrasenya clau",
|
||||
"pastePrivateKey": "Enganxa la teva clau privada aquí...",
|
||||
"enterPassword": "Introdueix la teva contrasenya"
|
||||
"enterPassword": "Introdueix la teva contrasenya",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tauler de control",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Publicat per {{issuer}}",
|
||||
"sslNames": "Cobertes {{names}}",
|
||||
"sslManualTitle": "Puja un certificat",
|
||||
"updateExternalAutoProvisionFailed": "No s'ha pogut actualitzar la configuració de creació automàtica"
|
||||
"updateExternalAutoProvisionFailed": "No s'ha pogut actualitzar la configuració de creació automàtica",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Aktualizace je k dispozici. Otevřít poznámky k verzi na GitHubu."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Blízko",
|
||||
"minimize": "Minimalizovat",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Přihlášení se nezdařilo",
|
||||
"unknown": "Neznámý",
|
||||
"continue": "Pokračovat",
|
||||
"maintenance": "Údržba",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Odstranit",
|
||||
"add": "Přidat",
|
||||
"update": "Aktualizovat",
|
||||
"noResults": "Nenalezeny žádné výsledky"
|
||||
"noResults": "Nenalezeny žádné výsledky",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Krok {{current}} z {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunel",
|
||||
"serverStats": "Metriky hostitele",
|
||||
"status": {
|
||||
"available": "K dispozici",
|
||||
"reachable": "Nahoře, ale poslední přihlášení se nezdařilo",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitorování zakázáno",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Postavení"
|
||||
},
|
||||
"folderRenamed": "Složka „{{oldName}}“ byla úspěšně přejmenována na „{{newName}}“",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Tento hostitel (přímý tunel)",
|
||||
"statusChecksLabel": "Kontroly stavu",
|
||||
"enableStatusChecks": "Povolit kontroly stavu",
|
||||
"enableStatusChecksDesc": "Pravidelně pingovat tento hostitel pro ověření dostupnosti",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Použít globální interval",
|
||||
"useGlobalIntervalDesc": "Přepsat intervalem kontroly stavu na úrovni celého serveru",
|
||||
"checkIntervalS": "Kontrolní interval(y)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Váš účet používá druhý faktor, který momentálně není k dispozici. Pro jeho resetování kontaktujte administrátora.",
|
||||
"secondFactorNoUI": "Tento druhý faktor nelze na této obrazovce zobrazit. Kontaktujte administrátora.",
|
||||
"serverAuthentication": "Přihlášení k serveru",
|
||||
"minChars": "min. {{min}} znaků"
|
||||
"minChars": "min. {{min}} znaků",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Ověření klíče hostitele SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Jazyk",
|
||||
"keyPassword": "klíčové heslo",
|
||||
"pastePrivateKey": "Vložte sem svůj soukromý klíč...",
|
||||
"enterPassword": "Zadejte své heslo"
|
||||
"enterPassword": "Zadejte své heslo",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Vydáno uživatelem {{issuer}}",
|
||||
"sslNames": "Kryty {{names}}",
|
||||
"sslManualTitle": "Nahrát certifikát",
|
||||
"updateExternalAutoProvisionFailed": "Nepodařilo se aktualizovat nastavení automatického vytváření"
|
||||
"updateExternalAutoProvisionFailed": "Nepodařilo se aktualizovat nastavení automatického vytváření",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Opdatering tilgængelig. Åbn udgivelsesnoterne på GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Tæt",
|
||||
"minimize": "Minimer",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Log ind mislykkedes",
|
||||
"unknown": "Ukendt",
|
||||
"continue": "Fortsætte",
|
||||
"maintenance": "Opretholdelse",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Fjerne",
|
||||
"add": "Tilføje",
|
||||
"update": "Opdatering",
|
||||
"noResults": "Ingen resultater fundet"
|
||||
"noResults": "Ingen resultater fundet",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Trin {{current}} af {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Værtsmålinger",
|
||||
"status": {
|
||||
"available": "Tilgængelig",
|
||||
"reachable": "Oppe, men den sidste login mislykkedes",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Overvågning deaktiveret",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Mappen \"{{oldName}}\" er omdøbt til \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Denne vært (direkte tunnel)",
|
||||
"statusChecksLabel": "Statuskontroller",
|
||||
"enableStatusChecks": "Aktivér statuskontroller",
|
||||
"enableStatusChecksDesc": "Ping denne vært regelmæssigt for at bekræfte tilgængelighed",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Brug globalt interval",
|
||||
"useGlobalIntervalDesc": "Tilsidesæt med det serveromfattende statuskontrolinterval",
|
||||
"checkIntervalS": "Kontrolinterval (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Din konto bruger en anden faktor, der ikke er tilgængelig lige nu. Kontakt en administrator for at nulstille den.",
|
||||
"secondFactorNoUI": "Denne anden faktor kan ikke vises på denne skærm. Kontakt en administrator.",
|
||||
"serverAuthentication": "Serverlogin",
|
||||
"minChars": "min. {{min}} tegn"
|
||||
"minChars": "min. {{min}} tegn",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Bekræft SSH-værtsnøgle",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Sprog",
|
||||
"keyPassword": "nøgleadgangskode",
|
||||
"pastePrivateKey": "Indsæt din private nøgle her...",
|
||||
"enterPassword": "Indtast din adgangskode"
|
||||
"enterPassword": "Indtast din adgangskode",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Udstedt af {{issuer}}",
|
||||
"sslNames": "Dækker {{names}}",
|
||||
"sslManualTitle": "Upload et certifikat",
|
||||
"updateExternalAutoProvisionFailed": "Kunne ikke opdatere indstillingen for automatisk oprettelse"
|
||||
"updateExternalAutoProvisionFailed": "Kunne ikke opdatere indstillingen for automatisk oprettelse",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Update verfügbar. Die Versionshinweise finden Sie auf GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Schließen",
|
||||
"minimize": "Minimieren",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Anmeldung fehlgeschlagen",
|
||||
"unknown": "Unbekannt",
|
||||
"continue": "Weiter",
|
||||
"maintenance": "Wartung",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Entfernen",
|
||||
"add": "Hinzufügen",
|
||||
"update": "Aktualisieren",
|
||||
"noResults": "Keine Ergebnisse gefunden"
|
||||
"noResults": "Keine Ergebnisse gefunden",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Schritt {{current}} von {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Host-Metriken",
|
||||
"status": {
|
||||
"available": "Verfügbar",
|
||||
"reachable": "Oben angekommen, aber die letzte Anmeldung schlug fehl.",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Überwachung deaktiviert",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Ordner \"{{oldName}}\" erfolgreich in \"{{newName}}\" umbenannt",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Dieser Host (direkter Tunnel)",
|
||||
"statusChecksLabel": "Statusprüfungen",
|
||||
"enableStatusChecks": "Statusprüfungen aktivieren",
|
||||
"enableStatusChecksDesc": "Diesen Host regelmäßig anpingen, um die Verfügbarkeit zu überprüfen",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Globales Intervall verwenden",
|
||||
"useGlobalIntervalDesc": "Mit dem serverweiten Statusprüfungsintervall überschreiben",
|
||||
"checkIntervalS": "Prüfintervall (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Ihr Konto verwendet einen zweiten Faktor, der momentan nicht verfügbar ist. Wenden Sie sich an einen Administrator, um ihn zurückzusetzen.",
|
||||
"secondFactorNoUI": "Dieser zweite Faktor kann auf diesem Bildschirm nicht angezeigt werden. Wenden Sie sich an einen Administrator.",
|
||||
"serverAuthentication": "Serveranmeldung",
|
||||
"minChars": "min. {{min}} Zeichen"
|
||||
"minChars": "min. {{min}} Zeichen",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "SSH-Host-Key überprüfen",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Sprache",
|
||||
"keyPassword": "Schlüsselpasswort",
|
||||
"pastePrivateKey": "Privaten Schlüssel hier einfügen...",
|
||||
"enterPassword": "Passwort eingeben"
|
||||
"enterPassword": "Passwort eingeben",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Herausgegeben von {{issuer}}",
|
||||
"sslNames": "Cover {{names}}",
|
||||
"sslManualTitle": "Laden Sie ein Zertifikat hoch",
|
||||
"updateExternalAutoProvisionFailed": "Die Aktualisierung der Einstellung für die automatische Erstellung ist fehlgeschlagen."
|
||||
"updateExternalAutoProvisionFailed": "Die Aktualisierung der Einstellung für die automatische Erstellung ist fehlgeschlagen.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Διαθέσιμη ενημέρωση. Ανοίξτε τις σημειώσεις έκδοσης στο GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Κοντά",
|
||||
"minimize": "Ελαττώνω",
|
||||
"online": "Διαδικτυακά",
|
||||
"offline": "Εκτός σύνδεσης",
|
||||
"signInFailed": "Η σύνδεση απέτυχε",
|
||||
"unknown": "Αγνωστος",
|
||||
"continue": "Συνεχίζω",
|
||||
"maintenance": "Συντήρηση",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Αφαιρώ",
|
||||
"add": "Προσθέτω",
|
||||
"update": "Εκσυγχρονίζω",
|
||||
"noResults": "Δεν βρέθηκαν αποτελέσματα"
|
||||
"noResults": "Δεν βρέθηκαν αποτελέσματα",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Βήμα {{current}} από {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Σήραγγα",
|
||||
"serverStats": "Μετρήσεις κεντρικού υπολογιστή",
|
||||
"status": {
|
||||
"available": "Διαθέσιμος",
|
||||
"reachable": "Ενεργοποιημένο, αλλά η τελευταία σύνδεση απέτυχε",
|
||||
"online": "Online",
|
||||
"offline": "Εκτός σύνδεσης",
|
||||
"monitoringDisabled": "Η παρακολούθηση απενεργοποιήθηκε",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Κατάσταση"
|
||||
},
|
||||
"folderRenamed": "Ο φάκελος \"{{oldName}}\" μετονομάστηκε με επιτυχία σε \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Αυτός ο κεντρικός υπολογιστής (άμεση σήραγγα)",
|
||||
"statusChecksLabel": "Έλεγχοι κατάστασης",
|
||||
"enableStatusChecks": "Ενεργοποίηση ελέγχων κατάστασης",
|
||||
"enableStatusChecksDesc": "Περιοδική ping σε αυτόν τον κεντρικό υπολογιστή για επαλήθευση διαθεσιμότητας",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Χρήση καθολικού διαστήματος",
|
||||
"useGlobalIntervalDesc": "Παράκαμψη με το διάστημα ελέγχου κατάστασης σε ολόκληρο τον διακομιστή",
|
||||
"checkIntervalS": "Διάστημα ελέγχου (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Ο λογαριασμός σας χρησιμοποιεί έναν δεύτερο παράγοντα που δεν είναι διαθέσιμος αυτήν τη στιγμή. Επικοινωνήστε με έναν διαχειριστή για να τον επαναφέρετε.",
|
||||
"secondFactorNoUI": "Αυτός ο δεύτερος παράγοντας δεν μπορεί να εμφανιστεί σε αυτήν την οθόνη. Επικοινωνήστε με έναν διαχειριστή.",
|
||||
"serverAuthentication": "Σύνδεση διακομιστή",
|
||||
"minChars": "ελάχ. {{min}} χαρακτήρες"
|
||||
"minChars": "ελάχ. {{min}} χαρακτήρες",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Επαλήθευση κλειδιού κεντρικού υπολογιστή SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Γλώσσα",
|
||||
"keyPassword": "κωδικός πρόσβασης κλειδιού",
|
||||
"pastePrivateKey": "Επικολλήστε το ιδιωτικό σας κλειδί εδώ...",
|
||||
"enterPassword": "Εισαγάγετε τον κωδικό πρόσβασής σας"
|
||||
"enterPassword": "Εισαγάγετε τον κωδικό πρόσβασής σας",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Ταμπλό",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Εκδόθηκε από {{issuer}}",
|
||||
"sslNames": "Καλύμματα {{names}}",
|
||||
"sslManualTitle": "Μεταφόρτωση πιστοποιητικού",
|
||||
"updateExternalAutoProvisionFailed": "Η ενημέρωση της ρύθμισης αυτόματης δημιουργίας απέτυχε"
|
||||
"updateExternalAutoProvisionFailed": "Η ενημέρωση της ρύθμισης αυτόματης δημιουργίας απέτυχε",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Actualización disponible. Consulta las notas de la versión en GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Cerrar",
|
||||
"minimize": "Minimizar",
|
||||
"online": "En línea",
|
||||
"offline": "Sin conexión",
|
||||
"signInFailed": "Error al iniciar sesión",
|
||||
"unknown": "Desconocido",
|
||||
"continue": "Continuar",
|
||||
"maintenance": "Mantenimiento",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Quitar",
|
||||
"add": "Añadir",
|
||||
"update": "Actualizar",
|
||||
"noResults": "No se encontraron resultados"
|
||||
"noResults": "No se encontraron resultados",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Paso {{current}} de {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Túnel",
|
||||
"serverStats": "Métricas del host",
|
||||
"status": {
|
||||
"available": "Disponible",
|
||||
"reachable": "Arriba, pero el último cartel falló.",
|
||||
"online": "Online",
|
||||
"offline": "Desconectado",
|
||||
"monitoringDisabled": "Monitoreo desactivado",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Estado"
|
||||
},
|
||||
"folderRenamed": "Carpeta \"{{oldName}}\" renombrada a \"{{newName}}\" correctamente",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Este host (túnel directo)",
|
||||
"statusChecksLabel": "Comprobaciones de estado",
|
||||
"enableStatusChecks": "Habilitar comprobaciones de estado",
|
||||
"enableStatusChecksDesc": "Hacer ping periódicamente a este host para verificar su disponibilidad.",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Usar intervalo global",
|
||||
"useGlobalIntervalDesc": "Sobrescribir con el intervalo de comprobación de estado global del servidor.",
|
||||
"checkIntervalS": "Intervalo de comprobación (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Tu cuenta utiliza un segundo factor de autenticación que no está disponible en este momento. Ponte en contacto con un administrador para restablecerlo.",
|
||||
"secondFactorNoUI": "Este segundo factor no se puede mostrar en esta pantalla. Contacta con un administrador.",
|
||||
"serverAuthentication": "Inicio de sesión en el servidor",
|
||||
"minChars": "min. {{min}} caracteres"
|
||||
"minChars": "min. {{min}} caracteres",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verificar clave de host SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Idioma",
|
||||
"keyPassword": "contraseña de la clave",
|
||||
"pastePrivateKey": "Pegue su clave privada aquí...",
|
||||
"enterPassword": "Introduce tu contraseña"
|
||||
"enterPassword": "Introduce tu contraseña",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panel de control",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Emitido por {{issuer}}",
|
||||
"sslNames": "Portadas {{names}}",
|
||||
"sslManualTitle": "Subir un certificado",
|
||||
"updateExternalAutoProvisionFailed": "No se pudo actualizar la configuración de creación automática."
|
||||
"updateExternalAutoProvisionFailed": "No se pudo actualizar la configuración de creación automática.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Päivitys saatavilla. Avaa julkaisutiedot GitHubissa."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Lähellä",
|
||||
"minimize": "Minimoida",
|
||||
"online": "Verkossa",
|
||||
"offline": "Offline-tilassa",
|
||||
"signInFailed": "Kirjautuminen epäonnistui",
|
||||
"unknown": "Tuntematon",
|
||||
"continue": "Jatkaa",
|
||||
"maintenance": "Huolto",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Poistaa",
|
||||
"add": "Lisätä",
|
||||
"update": "Päivittää",
|
||||
"noResults": "Ei tuloksia"
|
||||
"noResults": "Ei tuloksia",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Vaihe {{current}} / {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunneli",
|
||||
"serverStats": "Isännän mittarit",
|
||||
"status": {
|
||||
"available": "Saatavilla",
|
||||
"reachable": "Ylös, mutta viimeisin kirjautuminen epäonnistui",
|
||||
"online": "Online",
|
||||
"offline": "Offline-tilassa",
|
||||
"monitoringDisabled": "Valvonta poistettu käytöstä",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Kansio \"{{oldName}}\" nimettiin uudelleen onnistuneesti muotoon \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Tämä isäntä (suora tunneli)",
|
||||
"statusChecksLabel": "Tilatarkastukset",
|
||||
"enableStatusChecks": "Ota käyttöön tilan tarkistukset",
|
||||
"enableStatusChecksDesc": "Lähetä tälle isännälle säännöllisesti ping-viesti saatavuuden varmistamiseksi.",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Käytä globaalia väliä",
|
||||
"useGlobalIntervalDesc": "Ohita palvelimen laajuisella tilan tarkistusvälillä",
|
||||
"checkIntervalS": "Tarkistusväli (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Tilisi käyttää toista vahvistustekijää, joka ei ole tällä hetkellä käytettävissä. Ota yhteyttä ylläpitäjään sen palauttamiseksi.",
|
||||
"secondFactorNoUI": "Tätä toista tekijää ei voida näyttää tällä näytöllä. Ota yhteyttä ylläpitäjään.",
|
||||
"serverAuthentication": "Palvelimelle kirjautuminen",
|
||||
"minChars": "vähintään {{min}} merkkiä"
|
||||
"minChars": "vähintään {{min}} merkkiä",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Vahvista SSH-isäntäavain",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Kieli",
|
||||
"keyPassword": "avainsalasana",
|
||||
"pastePrivateKey": "Liitä yksityinen avaimesi tähän...",
|
||||
"enterPassword": "Anna salasanasi"
|
||||
"enterPassword": "Anna salasanasi",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Kojelauta",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Myöntäjä {{issuer}}",
|
||||
"sslNames": "Kannet {{names}}",
|
||||
"sslManualTitle": "Lähetä sertifikaatti",
|
||||
"updateExternalAutoProvisionFailed": "Automaattisen luomisen asetuksen päivittäminen epäonnistui"
|
||||
"updateExternalAutoProvisionFailed": "Automaattisen luomisen asetuksen päivittäminen epäonnistui",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Mise à jour disponible. Consultez les notes de version sur GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Fermer",
|
||||
"minimize": "Réduire",
|
||||
"online": "En ligne",
|
||||
"offline": "Hors ligne",
|
||||
"signInFailed": "Échec de la connexion",
|
||||
"unknown": "Inconnu",
|
||||
"continue": "Continuer",
|
||||
"maintenance": "Maintenance",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Retirer",
|
||||
"add": "Ajouter",
|
||||
"update": "Mettre à jour",
|
||||
"noResults": "Aucun résultat trouvé"
|
||||
"noResults": "Aucun résultat trouvé",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Étape {{current}} de {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Métriques de l'hôte",
|
||||
"status": {
|
||||
"available": "Disponible",
|
||||
"reachable": "J'ai réussi, mais la dernière connexion a échoué.",
|
||||
"online": "Online",
|
||||
"offline": "Hors ligne",
|
||||
"monitoringDisabled": "Surveillance désactivée",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Statut"
|
||||
},
|
||||
"folderRenamed": "Dossier « {{oldName}} » renommé en « {{newName}} » avec succès",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Cet hôte (tunnel direct)",
|
||||
"statusChecksLabel": "Vérifications d'état",
|
||||
"enableStatusChecks": "Activer les vérifications d'état",
|
||||
"enableStatusChecksDesc": "Envoyer périodiquement un ping à cet hôte pour vérifier sa disponibilité",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Utiliser l'intervalle global",
|
||||
"useGlobalIntervalDesc": "Remplacer par l'intervalle de vérification d'état défini à l'échelle du serveur",
|
||||
"checkIntervalS": "Intervalle de vérification (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Votre compte utilise une authentification à deux facteurs actuellement indisponible. Veuillez contacter un administrateur pour la réinitialiser.",
|
||||
"secondFactorNoUI": "Ce deuxième facteur ne peut pas être affiché sur cet écran. Veuillez contacter un administrateur.",
|
||||
"serverAuthentication": "Connexion au serveur",
|
||||
"minChars": "min. {{min}} caractères"
|
||||
"minChars": "min. {{min}} caractères",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Vérifier la clé d'hôte SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Langue",
|
||||
"keyPassword": "mot de passe de la clé",
|
||||
"pastePrivateKey": "Collez votre clé privée ici...",
|
||||
"enterPassword": "Saisissez votre mot de passe"
|
||||
"enterPassword": "Saisissez votre mot de passe",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tableau de bord",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Émis par {{issuer}}",
|
||||
"sslNames": "Couvertures {{names}}",
|
||||
"sslManualTitle": "Téléverser un certificat",
|
||||
"updateExternalAutoProvisionFailed": "Échec de la mise à jour du paramètre de création automatique"
|
||||
"updateExternalAutoProvisionFailed": "Échec de la mise à jour du paramètre de création automatique",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "עדכון זמין. פתח את הערות הגרסה ב-GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "לִסְגוֹר",
|
||||
"minimize": "לְצַמְצֵם",
|
||||
"online": "באינטרנט",
|
||||
"offline": "לא מקוון",
|
||||
"signInFailed": "הכניסה נכשלה",
|
||||
"unknown": "לֹא יְדוּעַ",
|
||||
"continue": "לְהַמשִׁיך",
|
||||
"maintenance": "תַחזוּקָה",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "לְהַסִיר",
|
||||
"add": "לְהוֹסִיף",
|
||||
"update": "לְעַדְכֵּן",
|
||||
"noResults": "לא נמצאו תוצאות"
|
||||
"noResults": "לא נמצאו תוצאות",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "שלב {{current}} של {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "מִנהָרָה",
|
||||
"serverStats": "מדדי מארח",
|
||||
"status": {
|
||||
"available": "זָמִין",
|
||||
"reachable": "למעלה, אבל הכניסה האחרונה נכשלה",
|
||||
"online": "Online",
|
||||
"offline": "לא מקוון",
|
||||
"monitoringDisabled": "ניטור מושבת",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "סטָטוּס"
|
||||
},
|
||||
"folderRenamed": "תיקיית \"{{oldName}}\" שמה שונה בהצלחה ל- \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "מארח זה (מנהרה ישירה)",
|
||||
"statusChecksLabel": "בדיקות סטטוס",
|
||||
"enableStatusChecks": "הפעל בדיקות סטטוס",
|
||||
"enableStatusChecksDesc": "שלח פינג למארח הזה מעת לעת כדי לוודא זמינות",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "השתמש במרווח גלובלי",
|
||||
"useGlobalIntervalDesc": "עקיפה עם מרווח בדיקת המצב הכולל של השרת",
|
||||
"checkIntervalS": "מרווח זמן בדיקה (ים)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "החשבון שלך משתמש בגורם שני שאינו זמין כרגע. צור קשר עם מנהל כדי לאפס אותו.",
|
||||
"secondFactorNoUI": "לא ניתן להציג את הגורם השני במסך זה. צור קשר עם מנהל.",
|
||||
"serverAuthentication": "כניסה לשרת",
|
||||
"minChars": "מינימום {{min}} תווים"
|
||||
"minChars": "מינימום {{min}} תווים",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "אימות מפתח מארח SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "שָׂפָה",
|
||||
"keyPassword": "סיסמת מפתח",
|
||||
"pastePrivateKey": "הדבק את המפתח הפרטי שלך כאן...",
|
||||
"enterPassword": "הזן את הסיסמה שלך"
|
||||
"enterPassword": "הזן את הסיסמה שלך",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "לוּחַ מַחווָנִים",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "הונפק על ידי {{issuer}}",
|
||||
"sslNames": "כריכות {{names}}",
|
||||
"sslManualTitle": "העלה תעודה",
|
||||
"updateExternalAutoProvisionFailed": "נכשל עדכון הגדרת היצירה האוטומטית"
|
||||
"updateExternalAutoProvisionFailed": "נכשל עדכון הגדרת היצירה האוטומטית",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "अपडेट उपलब्ध है। GitHub पर रिलीज़ नोट्स देखें।"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "बंद करें",
|
||||
"minimize": "न्यूनतम करें",
|
||||
"online": "ऑनलाइन",
|
||||
"offline": "ऑफ़लाइन",
|
||||
"signInFailed": "भाग लेना विफल हुआ",
|
||||
"unknown": "अज्ञात",
|
||||
"continue": "जारी रखें",
|
||||
"maintenance": "रखरखाव",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "हटाएं",
|
||||
"add": "जोड़ें",
|
||||
"update": "अपडेट करें",
|
||||
"noResults": "कोई परिणाम नहीं मिला"
|
||||
"noResults": "कोई परिणाम नहीं मिला",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "चरण {{current}} का {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "टनल",
|
||||
"serverStats": "होस्ट मेट्रिक्स",
|
||||
"status": {
|
||||
"available": "उपलब्ध",
|
||||
"reachable": "ऊपर तो आ गया, लेकिन आखिरी बार साइन इन करने में विफल रहा।",
|
||||
"online": "Online",
|
||||
"offline": "ऑफलाइन",
|
||||
"monitoringDisabled": "निगरानी अक्षम है",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "स्थिति"
|
||||
},
|
||||
"folderRenamed": "फ़ोल्डर \"{{oldName}}\" का नाम बदलकर \"{{newName}}\" सफलतापूर्वक किया गया",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "यह होस्ट (डायरेक्ट टनल)",
|
||||
"statusChecksLabel": "स्थिति जाँच",
|
||||
"enableStatusChecks": "स्थिति जाँच सक्षम करें",
|
||||
"enableStatusChecksDesc": "उपलब्धता सत्यापित करने के लिए समय-समय पर इस होस्ट को पिंग करें",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "वैश्विक अंतराल का उपयोग करें",
|
||||
"useGlobalIntervalDesc": "सर्वर-व्यापी स्थिति जाँच अंतराल से ओवरराइड करें",
|
||||
"checkIntervalS": "जाँच अंतराल (सेकंड)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "आपके खाते में एक दूसरा फैक्टर फैक्टर इस्तेमाल होता है जो अभी उपलब्ध नहीं है। इसे रीसेट करने के लिए एडमिन से संपर्क करें।",
|
||||
"secondFactorNoUI": "यह दूसरा कारक इस स्क्रीन पर नहीं दिखाया जा सकता। कृपया व्यवस्थापक से संपर्क करें।",
|
||||
"serverAuthentication": "सर्वर साइन-इन",
|
||||
"minChars": "न्यूनतम {{min}} अक्षर"
|
||||
"minChars": "न्यूनतम {{min}} अक्षर",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "SSH होस्ट कुंजी सत्यापित करें",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "भाषा",
|
||||
"keyPassword": "कुंजी पासवर्ड",
|
||||
"pastePrivateKey": "अपनी निजी कुंजी यहाँ चिपकाएँ...",
|
||||
"enterPassword": "अपना पासवर्ड दर्ज करें"
|
||||
"enterPassword": "अपना पासवर्ड दर्ज करें",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "डैशबोर्ड",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "{{issuer}} द्वारा जारी किया गया",
|
||||
"sslNames": "कवर {{names}}",
|
||||
"sslManualTitle": "प्रमाणपत्र अपलोड करें",
|
||||
"updateExternalAutoProvisionFailed": "ऑटो-क्रिएट सेटिंग को अपडेट करने में विफलता"
|
||||
"updateExternalAutoProvisionFailed": "ऑटो-क्रिएट सेटिंग को अपडेट करने में विफलता",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Frissítés elérhető. Nyissa meg a kiadási megjegyzéseket a GitHubon."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Közeli",
|
||||
"minimize": "Minimalizálás",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Bejelentkezés sikertelen",
|
||||
"unknown": "Ismeretlen",
|
||||
"continue": "Folytatás",
|
||||
"maintenance": "Karbantartás",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Eltávolítás",
|
||||
"add": "Hozzáadás",
|
||||
"update": "Frissítés",
|
||||
"noResults": "Nincs találat"
|
||||
"noResults": "Nincs találat",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "{{total}} {{current}} lépése",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Alagút",
|
||||
"serverStats": "Gazdagép-metrikák",
|
||||
"status": {
|
||||
"available": "Elérhető",
|
||||
"reachable": "Felment, de az utolsó bejelentkezés sikertelen",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitorozás letiltva",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Állapot"
|
||||
},
|
||||
"folderRenamed": "A(z) „{{oldName}}” mappa sikeresen átnevezve erre: „{{newName}}”",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Ez a gazdagép (közvetlen alagút)",
|
||||
"statusChecksLabel": "Állapotellenőrzések",
|
||||
"enableStatusChecks": "Állapotellenőrzések engedélyezése",
|
||||
"enableStatusChecksDesc": "Rendszeresen pingelje ezt a gazdagépet az elérhetőség ellenőrzéséhez",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Globális intervallum használata",
|
||||
"useGlobalIntervalDesc": "Felülbírálás a szerver szintű állapotellenőrzési intervallummal",
|
||||
"checkIntervalS": "Ellenőrzési intervallum (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "A fiókod egy második faktort használ, ami jelenleg nem érhető el. Lépj kapcsolatba egy adminisztrátorral a visszaállításához.",
|
||||
"secondFactorNoUI": "Ez a második tényező nem jeleníthető meg ezen a képernyőn. Lépjen kapcsolatba egy adminisztrátorral.",
|
||||
"serverAuthentication": "Szerver bejelentkezés",
|
||||
"minChars": "min. {{min}} karakter"
|
||||
"minChars": "min. {{min}} karakter",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "SSH host kulcs ellenőrzése",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Nyelv",
|
||||
"keyPassword": "kulcs jelszó",
|
||||
"pastePrivateKey": "Illeszd be ide a privát kulcsodat...",
|
||||
"enterPassword": "Írja be a jelszavát"
|
||||
"enterPassword": "Írja be a jelszavát",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Műszerfal",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Kibocsátó: {{issuer}}",
|
||||
"sslNames": "Borítók {{names}}",
|
||||
"sslManualTitle": "Tanúsítvány feltöltése",
|
||||
"updateExternalAutoProvisionFailed": "Nem sikerült frissíteni az automatikus létrehozási beállítást"
|
||||
"updateExternalAutoProvisionFailed": "Nem sikerült frissíteni az automatikus létrehozási beállítást",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Pembaruan tersedia. Buka catatan rilis di GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Menutup",
|
||||
"minimize": "Memperkecil",
|
||||
"online": "On line",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Login gagal.",
|
||||
"unknown": "Tidak dikenal",
|
||||
"continue": "Melanjutkan",
|
||||
"maintenance": "Pemeliharaan",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Menghapus",
|
||||
"add": "Menambahkan",
|
||||
"update": "Memperbarui",
|
||||
"noResults": "Tidak ada hasil yang ditemukan"
|
||||
"noResults": "Tidak ada hasil yang ditemukan",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Langkah {{current}} dari {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Terowongan",
|
||||
"serverStats": "Metrik Host",
|
||||
"status": {
|
||||
"available": "Tersedia",
|
||||
"reachable": "Berhasil, tetapi tanda terakhir gagal.",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Pemantauan dinonaktifkan",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Folder \"{{oldName}}\" berhasil diganti namanya menjadi \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Host ini (terowongan langsung)",
|
||||
"statusChecksLabel": "Pemeriksaan Status",
|
||||
"enableStatusChecks": "Aktifkan Pemeriksaan Status",
|
||||
"enableStatusChecksDesc": "Lakukan ping secara berkala ke host ini untuk memverifikasi ketersediaan.",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Gunakan Interval Global",
|
||||
"useGlobalIntervalDesc": "Ganti dengan interval pemeriksaan status di seluruh server.",
|
||||
"checkIntervalS": "Interval Pemeriksaan (detik)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Akun Anda menggunakan faktor kedua yang saat ini tidak tersedia. Hubungi admin untuk mengatur ulang faktor tersebut.",
|
||||
"secondFactorNoUI": "Faktor kedua ini tidak dapat ditampilkan di layar ini. Hubungi administrator.",
|
||||
"serverAuthentication": "Masuk ke server",
|
||||
"minChars": "min. {{min}} karakter"
|
||||
"minChars": "min. {{min}} karakter",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verifikasi Kunci Host SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Bahasa",
|
||||
"keyPassword": "kata sandi kunci",
|
||||
"pastePrivateKey": "Tempelkan kunci pribadi Anda di sini...",
|
||||
"enterPassword": "Masukkan kata sandi Anda"
|
||||
"enterPassword": "Masukkan kata sandi Anda",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dasbor",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Dikeluarkan oleh {{issuer}}",
|
||||
"sslNames": "Sampul {{names}}",
|
||||
"sslManualTitle": "Unggah sertifikat",
|
||||
"updateExternalAutoProvisionFailed": "Gagal memperbarui pengaturan pembuatan otomatis."
|
||||
"updateExternalAutoProvisionFailed": "Gagal memperbarui pengaturan pembuatan otomatis.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Aggiornamento disponibile. Consulta le note di rilascio su GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Chiudi",
|
||||
"minimize": "Riduci a icona",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Accesso non riuscito",
|
||||
"unknown": "Sconosciuto",
|
||||
"continue": "Continua",
|
||||
"maintenance": "Manutenzione",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Rimuovi",
|
||||
"add": "Aggiungi",
|
||||
"update": "Aggiorna",
|
||||
"noResults": "Nessun risultato trovato"
|
||||
"noResults": "Nessun risultato trovato",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Passo {{current}} di {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Metriche host",
|
||||
"status": {
|
||||
"available": "Disponibile",
|
||||
"reachable": "Su, ma l'ultimo accesso non è riuscito",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitoraggio disabilitato",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Stato"
|
||||
},
|
||||
"folderRenamed": "Cartella \"{{oldName}}\" rinominata in \"{{newName}}\" con successo",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Questo host (tunnel diretto)",
|
||||
"statusChecksLabel": "Controlli di stato",
|
||||
"enableStatusChecks": "Abilita controlli di stato",
|
||||
"enableStatusChecksDesc": "Esegui periodicamente il ping di questo host per verificarne la disponibilità",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Usa intervallo globale",
|
||||
"useGlobalIntervalDesc": "Sovrascrivi con l'intervallo di controllo dello stato del server",
|
||||
"checkIntervalS": "Intervallo di controllo (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Il tuo account utilizza un secondo fattore di autenticazione al momento non disponibile. Contatta un amministratore per reimpostarlo.",
|
||||
"secondFactorNoUI": "Questo secondo fattore non può essere visualizzato in questa schermata. Contatta un amministratore.",
|
||||
"serverAuthentication": "Accesso al server",
|
||||
"minChars": "min. {{min}} caratteri"
|
||||
"minChars": "min. {{min}} caratteri",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verifica chiave host SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Lingua",
|
||||
"keyPassword": "password chiave",
|
||||
"pastePrivateKey": "Incolla qui la tua chiave privata...",
|
||||
"enterPassword": "Inserisci la password"
|
||||
"enterPassword": "Inserisci la password",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Emesso da {{issuer}}",
|
||||
"sslNames": "Copertine {{names}}",
|
||||
"sslManualTitle": "Carica un certificato",
|
||||
"updateExternalAutoProvisionFailed": "Impossibile aggiornare l'impostazione di creazione automatica"
|
||||
"updateExternalAutoProvisionFailed": "Impossibile aggiornare l'impostazione di creazione automatica",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "アップデートが利用可能です。GitHubのリリースノートをご覧ください。"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "閉じる",
|
||||
"minimize": "最小化",
|
||||
"online": "オンライン",
|
||||
"offline": "オフライン",
|
||||
"signInFailed": "サインインに失敗しました",
|
||||
"unknown": "不明",
|
||||
"continue": "続行",
|
||||
"maintenance": "メンテナンス",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "削除",
|
||||
"add": "追加",
|
||||
"update": "更新",
|
||||
"noResults": "検索結果が見つかりませんでした"
|
||||
"noResults": "検索結果が見つかりませんでした",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "ステップ {{current}} / {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "トンネル",
|
||||
"serverStats": "ホストメトリクス",
|
||||
"status": {
|
||||
"available": "利用可能",
|
||||
"reachable": "上昇したが、最後のサインインが失敗した",
|
||||
"online": "Online",
|
||||
"offline": "オフライン",
|
||||
"monitoringDisabled": "監視が無効になっています",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "状態"
|
||||
},
|
||||
"folderRenamed": "フォルダ「{{oldName}}」を「{{newName}}」に正常にリネームしました",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "このホスト(直接トンネル)",
|
||||
"statusChecksLabel": "ステータスチェック",
|
||||
"enableStatusChecks": "ステータスチェックを有効にする",
|
||||
"enableStatusChecksDesc": "定期的にこのホストに ping を送信して可用性を確認します",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "グローバル間隔を使用する",
|
||||
"useGlobalIntervalDesc": "サーバー全体のステータスチェック間隔で上書きします",
|
||||
"checkIntervalS": "チェック間隔 (秒)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "お客様のアカウントには、現在ご利用いただけない二段階認証が設定されています。リセットするには、管理者にお問い合わせください。",
|
||||
"secondFactorNoUI": "この2つ目の要素は、この画面には表示できません。管理者にお問い合わせください。",
|
||||
"serverAuthentication": "サーバーへのサインイン",
|
||||
"minChars": "最小 {{min}} 文字"
|
||||
"minChars": "最小 {{min}} 文字",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "SSHホストキーを確認",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "言語",
|
||||
"keyPassword": "キーパスワード",
|
||||
"pastePrivateKey": "秘密鍵をここに貼り付け...",
|
||||
"enterPassword": "パスワードを入力"
|
||||
"enterPassword": "パスワードを入力",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "ダッシュボード",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "{{issuer}} によって発行されました",
|
||||
"sslNames": "カバー {{names}}",
|
||||
"sslManualTitle": "証明書をアップロードしてください",
|
||||
"updateExternalAutoProvisionFailed": "自動作成設定の更新に失敗しました"
|
||||
"updateExternalAutoProvisionFailed": "自動作成設定の更新に失敗しました",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "업데이트가 있습니다. GitHub에서 릴리스 노트를 확인하세요."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "닫기",
|
||||
"minimize": "최소화",
|
||||
"online": "온라인",
|
||||
"offline": "오프라인",
|
||||
"signInFailed": "로그인 실패",
|
||||
"unknown": "알 수 없음",
|
||||
"continue": "계속",
|
||||
"maintenance": "유지 관리",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "제거",
|
||||
"add": "추가",
|
||||
"update": "업데이트",
|
||||
"noResults": "검색 결과가 없습니다."
|
||||
"noResults": "검색 결과가 없습니다.",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "{{current}} 단계 {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "터널",
|
||||
"serverStats": "호스트 메트릭",
|
||||
"status": {
|
||||
"available": "사용 가능",
|
||||
"reachable": "올라갔지만 마지막 로그인 시도가 실패했습니다.",
|
||||
"online": "Online",
|
||||
"offline": "오프라인",
|
||||
"monitoringDisabled": "모니터링이 비활성화되었습니다.",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "상태"
|
||||
},
|
||||
"folderRenamed": "폴더 \"{{oldName}}\"이(가) \"{{newName}}\"(으)로 성공적으로 이름이 변경되었습니다.",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "이 호스트 (직접 터널)",
|
||||
"statusChecksLabel": "상태 확인",
|
||||
"enableStatusChecks": "상태 확인 활성화",
|
||||
"enableStatusChecksDesc": "주기적으로 이 호스트에 ping을 보내 가용성을 확인합니다.",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "전역 간격 사용",
|
||||
"useGlobalIntervalDesc": "서버 전체 상태 확인 간격으로 재정의합니다.",
|
||||
"checkIntervalS": "확인 간격(초)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "현재 사용 중인 2단계 인증 방식이 비활성화되어 있습니다. 관리자에게 문의하여 재설정하세요.",
|
||||
"secondFactorNoUI": "두 번째 요소는 이 화면에 표시할 수 없습니다. 관리자에게 문의하십시오.",
|
||||
"serverAuthentication": "서버 로그인",
|
||||
"minChars": "최소 {{min}} 문자"
|
||||
"minChars": "최소 {{min}} 문자",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "SSH 호스트 키 확인",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "언어",
|
||||
"keyPassword": "키 비밀번호",
|
||||
"pastePrivateKey": "여기에 개인 키를 붙여넣으세요...",
|
||||
"enterPassword": "비밀번호를 입력하세요"
|
||||
"enterPassword": "비밀번호를 입력하세요",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "대시보드",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "{{issuer}}에서 발행함",
|
||||
"sslNames": "{{names}}을 덮습니다.",
|
||||
"sslManualTitle": "인증서를 업로드하세요",
|
||||
"updateExternalAutoProvisionFailed": "자동 생성 설정을 업데이트하는 데 실패했습니다."
|
||||
"updateExternalAutoProvisionFailed": "자동 생성 설정을 업데이트하는 데 실패했습니다.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Update beschikbaar. Bekijk de release-opmerkingen op GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Dichtbij",
|
||||
"minimize": "Minimaliseren",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Aanmelden mislukt",
|
||||
"unknown": "Onbekend",
|
||||
"continue": "Doorgaan",
|
||||
"maintenance": "Onderhoud",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Verwijderen",
|
||||
"add": "Toevoegen",
|
||||
"update": "Update",
|
||||
"noResults": "Geen resultaten gevonden"
|
||||
"noResults": "Geen resultaten gevonden",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Stap {{current}} van {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Hoststatistieken",
|
||||
"status": {
|
||||
"available": "Beschikbaar",
|
||||
"reachable": "Omhoog, maar de laatste aanmelding mislukte.",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitoring uitgeschakeld",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Map \"{{oldName}}\" is succesvol hernoemd naar \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Deze host (directe tunnel)",
|
||||
"statusChecksLabel": "Statuscontroles",
|
||||
"enableStatusChecks": "Statuscontroles inschakelen",
|
||||
"enableStatusChecksDesc": "Controleer periodiek de beschikbaarheid van deze host.",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Gebruik globaal interval",
|
||||
"useGlobalIntervalDesc": "Overschrijf dit met het serverbrede statuscontrole-interval.",
|
||||
"checkIntervalS": "Controle-interval (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Uw account maakt gebruik van een tweede authenticatiemethode die momenteel niet beschikbaar is. Neem contact op met een beheerder om deze opnieuw in te stellen.",
|
||||
"secondFactorNoUI": "Deze tweede factor kan niet op dit scherm worden weergegeven. Neem contact op met een beheerder.",
|
||||
"serverAuthentication": "Server aanmelden",
|
||||
"minChars": "min. {{min}} tekens"
|
||||
"minChars": "min. {{min}} tekens",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verifieer de SSH-hostsleutel",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Taal",
|
||||
"keyPassword": "sleutel wachtwoord",
|
||||
"pastePrivateKey": "Plak hier uw privésleutel...",
|
||||
"enterPassword": "Voer uw wachtwoord in"
|
||||
"enterPassword": "Voer uw wachtwoord in",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Uitgegeven door {{issuer}}",
|
||||
"sslNames": "Omslagen {{names}}",
|
||||
"sslManualTitle": "Een certificaat uploaden",
|
||||
"updateExternalAutoProvisionFailed": "Het bijwerken van de instelling voor automatisch aanmaken is mislukt."
|
||||
"updateExternalAutoProvisionFailed": "Het bijwerken van de instelling voor automatisch aanmaken is mislukt.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Oppdatering tilgjengelig. Åpne utgivelsesnotatene på GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Lukke",
|
||||
"minimize": "Minimer",
|
||||
"online": "På nett",
|
||||
"offline": "Frakoblet",
|
||||
"signInFailed": "Pålogging mislyktes",
|
||||
"unknown": "Ukjent",
|
||||
"continue": "Fortsette",
|
||||
"maintenance": "Vedlikehold",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Fjerne",
|
||||
"add": "Legge til",
|
||||
"update": "Oppdatering",
|
||||
"noResults": "Ingen resultater funnet"
|
||||
"noResults": "Ingen resultater funnet",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Trinn {{current}} av {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Vertsmålinger",
|
||||
"status": {
|
||||
"available": "Tilgjengelig",
|
||||
"reachable": "Oppe, men siste pålogging mislyktes",
|
||||
"online": "Online",
|
||||
"offline": "Frakoblet",
|
||||
"monitoringDisabled": "Overvåking deaktivert",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Mappen «{{oldName}}» har fått nytt navn til «{{newName}}»",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Denne verten (direkte tunnel)",
|
||||
"statusChecksLabel": "Statuskontroller",
|
||||
"enableStatusChecks": "Aktiver statuskontroller",
|
||||
"enableStatusChecksDesc": "Ping denne verten med jevne mellomrom for å bekrefte tilgjengelighet",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Bruk globalt intervall",
|
||||
"useGlobalIntervalDesc": "Overstyr med det serveromfattende statuskontrollintervallet",
|
||||
"checkIntervalS": "Kontrollintervall (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Kontoen din bruker en annen faktor som ikke er tilgjengelig akkurat nå. Kontakt en administrator for å tilbakestille den.",
|
||||
"secondFactorNoUI": "Denne andre faktoren kan ikke vises på denne skjermen. Kontakt en administrator.",
|
||||
"serverAuthentication": "Serverpålogging",
|
||||
"minChars": "min. {{min}} tegn"
|
||||
"minChars": "min. {{min}} tegn",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Bekreft SSH-vertsnøkkel",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Språk",
|
||||
"keyPassword": "nøkkelpassord",
|
||||
"pastePrivateKey": "Lim inn din private nøkkel her...",
|
||||
"enterPassword": "Skriv inn passordet ditt"
|
||||
"enterPassword": "Skriv inn passordet ditt",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashbord",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Utstedt av {{issuer}}",
|
||||
"sslNames": "Omslag {{names}}",
|
||||
"sslManualTitle": "Last opp et sertifikat",
|
||||
"updateExternalAutoProvisionFailed": "Kunne ikke oppdatere innstillingen for automatisk oppretting"
|
||||
"updateExternalAutoProvisionFailed": "Kunne ikke oppdatere innstillingen for automatisk oppretting",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Aktualizacja dostępna. Otwórz notatki do wydania na GitHubie."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Zamknij",
|
||||
"minimize": "Minimalizuj",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Logowanie nie powiodło się",
|
||||
"unknown": "Nieznany",
|
||||
"continue": "Kontynuuj",
|
||||
"maintenance": "Konserwacja",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Usuń",
|
||||
"add": "Dodaj",
|
||||
"update": "Aktualizuj",
|
||||
"noResults": "Nie znaleziono wyników"
|
||||
"noResults": "Nie znaleziono wyników",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Krok {{current}} z {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunel",
|
||||
"serverStats": "Metryki hosta",
|
||||
"status": {
|
||||
"available": "Dostępny",
|
||||
"reachable": "W górę, ale ostatnie logowanie nie powiodło się",
|
||||
"online": "Online",
|
||||
"offline": "Niedostępny",
|
||||
"monitoringDisabled": "Monitorowanie wyłączone",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Zmieniono nazwę folderu „{{oldName}}” na „{{newName}}”",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Ten host (tunel bezpośredni)",
|
||||
"statusChecksLabel": "Sprawdzanie statusu",
|
||||
"enableStatusChecks": "Włącz sprawdzanie statusu",
|
||||
"enableStatusChecksDesc": "Okresowo wysyłaj polecenie ping do tego hosta w celu sprawdzenia dostępności",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Użyj interwału globalnego",
|
||||
"useGlobalIntervalDesc": "Użyj globalnego interwału sprawdzania statusu",
|
||||
"checkIntervalS": "Interwał sprawdzania (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Twoje konto korzysta z drugiego czynnika uwierzytelniania, który obecnie nie jest dostępny. Skontaktuj się z administratorem, aby go zresetować.",
|
||||
"secondFactorNoUI": "Drugiego czynnika nie można wyświetlić na tym ekranie. Skontaktuj się z administratorem.",
|
||||
"serverAuthentication": "Logowanie do serwera",
|
||||
"minChars": "min. {{min}} znaków"
|
||||
"minChars": "min. {{min}} znaków",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Zweryfikuj klucz hosta SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Język",
|
||||
"keyPassword": "hasło klucza",
|
||||
"pastePrivateKey": "Wklej tutaj klucz prywatny...",
|
||||
"enterPassword": "Wprowadź hasło"
|
||||
"enterPassword": "Wprowadź hasło",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Pulpit",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Wydane przez {{issuer}}",
|
||||
"sslNames": "Obejmuje {{names}}",
|
||||
"sslManualTitle": "Prześlij certyfikat",
|
||||
"updateExternalAutoProvisionFailed": "Nie udało się zaktualizować ustawienia automatycznego tworzenia"
|
||||
"updateExternalAutoProvisionFailed": "Nie udało się zaktualizować ustawienia automatycznego tworzenia",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Atualização disponível. Consulte as notas de lançamento no GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Fechar",
|
||||
"minimize": "Minimizar",
|
||||
"online": "On-line",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Falha no login",
|
||||
"unknown": "Desconhecido",
|
||||
"continue": "Continuar",
|
||||
"maintenance": "Manutenção",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Remover",
|
||||
"add": "Adicionar",
|
||||
"update": "Atualizar",
|
||||
"noResults": "Nenhum resultado encontrado"
|
||||
"noResults": "Nenhum resultado encontrado",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Etapa {{current}} de {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Túnel",
|
||||
"serverStats": "Métricas do host",
|
||||
"status": {
|
||||
"available": "Disponível",
|
||||
"reachable": "Subiu, mas o último sinal falhou.",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitoramento desativado",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Pasta \"{{oldName}}\" renomeada para \"{{newName}}\" com sucesso",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Este host (túnel direto)",
|
||||
"statusChecksLabel": "Verificações de status",
|
||||
"enableStatusChecks": "Ativar verificações de status",
|
||||
"enableStatusChecksDesc": "Periodicamente, faça ping neste host para verificar a disponibilidade.",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Usar intervalo global",
|
||||
"useGlobalIntervalDesc": "Substitua pelo intervalo de verificação de status de todo o servidor.",
|
||||
"checkIntervalS": "Intervalo de verificação (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Sua conta utiliza um segundo fator de autenticação que não está disponível no momento. Entre em contato com um administrador para redefini-lo.",
|
||||
"secondFactorNoUI": "Este segundo fator não pode ser exibido nesta tela. Entre em contato com um administrador.",
|
||||
"serverAuthentication": "Login no servidor",
|
||||
"minChars": "mín. {{min}} caracteres"
|
||||
"minChars": "mín. {{min}} caracteres",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verificar chave de host SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Linguagem",
|
||||
"keyPassword": "senha chave",
|
||||
"pastePrivateKey": "Cole sua chave privada aqui...",
|
||||
"enterPassword": "Digite sua senha"
|
||||
"enterPassword": "Digite sua senha",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Painel",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Emitido por {{issuer}}",
|
||||
"sslNames": "Capas {{names}}",
|
||||
"sslManualTitle": "Faça o upload de um certificado",
|
||||
"updateExternalAutoProvisionFailed": "Falha ao atualizar a configuração de criação automática."
|
||||
"updateExternalAutoProvisionFailed": "Falha ao atualizar a configuração de criação automática.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Atualização disponível. Consulte as notas de lançamento no GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Fechar",
|
||||
"minimize": "Minimizar",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Falha no login",
|
||||
"unknown": "Desconhecido",
|
||||
"continue": "Continuar",
|
||||
"maintenance": "Manutenção",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Remover",
|
||||
"add": "Adicionar",
|
||||
"update": "Atualizar",
|
||||
"noResults": "Nenhum resultado encontrado"
|
||||
"noResults": "Nenhum resultado encontrado",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Passo {{current}} de {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Túnel",
|
||||
"serverStats": "Métricas do Host",
|
||||
"status": {
|
||||
"available": "Disponível",
|
||||
"reachable": "Subiu, mas o último sinal falhou.",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitorização desativado",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Estatuto"
|
||||
},
|
||||
"folderRenamed": "Pasta \"{{oldName}}\" renomeada para \"{{newName}}\" com sucesso",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Este host (túnel direto)",
|
||||
"statusChecksLabel": "Verificações de Estado",
|
||||
"enableStatusChecks": "Ativar Verificações de Estado",
|
||||
"enableStatusChecksDesc": "Fazer ping periodicamente a este host para verificar a disponibilidade",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Usar Intervalo Global",
|
||||
"useGlobalIntervalDesc": "Substituir pelo intervalo de verificação de estado global do servidor",
|
||||
"checkIntervalS": "Intervalo de Verificação (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "A sua conta utiliza um segundo fator de autenticação que não está disponível de momento. Contacte um administrador para redefini-lo.",
|
||||
"secondFactorNoUI": "Este segundo fator não pode ser apresentado neste ecrã. Entre em contacto com um administrador.",
|
||||
"serverAuthentication": "Iniciar sessão no servidor",
|
||||
"minChars": "mín. {{min}} caracteres"
|
||||
"minChars": "mín. {{min}} caracteres",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verificar chave de host SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Idioma",
|
||||
"keyPassword": "palavra-passe da chave",
|
||||
"pastePrivateKey": "Cole aqui a sua chave privada...",
|
||||
"enterPassword": "Introduza a sua palavra-passe"
|
||||
"enterPassword": "Introduza a sua palavra-passe",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Painel de Controlo",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Emitido por {{issuer}}",
|
||||
"sslNames": "Capas {{names}}",
|
||||
"sslManualTitle": "Faça o upload de um certificado",
|
||||
"updateExternalAutoProvisionFailed": "Falha ao atualizar a configuração de criação automática."
|
||||
"updateExternalAutoProvisionFailed": "Falha ao atualizar a configuração de criação automática.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Actualizare disponibilă. Deschideți notele de lansare pe GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Aproape",
|
||||
"minimize": "Minimizare",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"signInFailed": "Conectarea a eșuat",
|
||||
"unknown": "Necunoscut",
|
||||
"continue": "Continua",
|
||||
"maintenance": "Întreţinere",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Elimina",
|
||||
"add": "Adăuga",
|
||||
"update": "Actualizare",
|
||||
"noResults": "Nu s-au găsit rezultate"
|
||||
"noResults": "Nu s-au găsit rezultate",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Pasul {{current}} din {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunel",
|
||||
"serverStats": "Metrici ale gazdei",
|
||||
"status": {
|
||||
"available": "Disponibil",
|
||||
"reachable": "Activ, dar ultima conectare a eșuat",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"monitoringDisabled": "Monitorizarea este dezactivată",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Stare"
|
||||
},
|
||||
"folderRenamed": "Dosarul „{{oldName}}” a fost redenumit în „{{newName}}” a fost reușit.",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Această gazdă (tunel direct)",
|
||||
"statusChecksLabel": "Verificări de stare",
|
||||
"enableStatusChecks": "Activați verificările de stare",
|
||||
"enableStatusChecksDesc": "Ping periodic această gazdă pentru a verifica disponibilitatea",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Utilizați intervalul global",
|
||||
"useGlobalIntervalDesc": "Suprascriere cu intervalul de verificare a stării la nivel de server",
|
||||
"checkIntervalS": "Interval (e) de verificare",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Contul tău folosește un al doilea factor care nu este disponibil momentan. Contactează un administrator pentru a-l reseta.",
|
||||
"secondFactorNoUI": "Acest al doilea factor nu poate fi afișat pe acest ecran. Contactați un administrator.",
|
||||
"serverAuthentication": "Conectare la server",
|
||||
"minChars": "min. {{min}} caractere"
|
||||
"minChars": "min. {{min}} caractere",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verificați cheia gazdă SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Limbă",
|
||||
"keyPassword": "parolă cheie",
|
||||
"pastePrivateKey": "Lipește cheia privată aici...",
|
||||
"enterPassword": "Introduceți parola"
|
||||
"enterPassword": "Introduceți parola",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Bord",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Emis de {{issuer}}",
|
||||
"sslNames": "Acoperă {{names}}",
|
||||
"sslManualTitle": "Încărcați un certificat",
|
||||
"updateExternalAutoProvisionFailed": "Nu s-a putut actualiza setarea de creare automată"
|
||||
"updateExternalAutoProvisionFailed": "Nu s-a putut actualiza setarea de creare automată",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Доступно обновление. Откройте примечания к выпуску на GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Закрыть",
|
||||
"minimize": "Свернуть",
|
||||
"online": "В сети",
|
||||
"offline": "Не в сети",
|
||||
"signInFailed": "Не удалось войти в систему.",
|
||||
"unknown": "Неизвестно",
|
||||
"continue": "Продолжить",
|
||||
"maintenance": "Обслуживание",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Удалить",
|
||||
"add": "Добавить",
|
||||
"update": "Обновить",
|
||||
"noResults": "Результаты не найдены"
|
||||
"noResults": "Результаты не найдены",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Шаг {{current}} из {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Туннель",
|
||||
"serverStats": "Метрики хоста",
|
||||
"status": {
|
||||
"available": "Доступный",
|
||||
"reachable": "Вход выполнен, но последняя попытка входа не удалась.",
|
||||
"online": "Online",
|
||||
"offline": "Офлайн",
|
||||
"monitoringDisabled": "Мониторинг отключен",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Статус"
|
||||
},
|
||||
"folderRenamed": "Папка \"{{oldName}}\" успешно переименована в \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Этот хост (прямой туннель)",
|
||||
"statusChecksLabel": "Проверки состояния",
|
||||
"enableStatusChecks": "Включить проверки состояния",
|
||||
"enableStatusChecksDesc": "Периодически пинговать этот хост для проверки доступности",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Использовать глобальный интервал",
|
||||
"useGlobalIntervalDesc": "Переопределить глобальным интервалом проверки состояния сервера",
|
||||
"checkIntervalS": "Интервал проверки (с)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Для сброса второго фактора аутентификации в вашем аккаунте в данный момент недоступен. Обратитесь к администратору.",
|
||||
"secondFactorNoUI": "Этот второй фактор не отображается на данном экране. Свяжитесь с администратором.",
|
||||
"serverAuthentication": "Вход на сервер",
|
||||
"minChars": "мин. {{min}} символов"
|
||||
"minChars": "мин. {{min}} символов",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Проверить SSH-ключ хоста",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Язык",
|
||||
"keyPassword": "пароль ключа",
|
||||
"pastePrivateKey": "Вставьте свой закрытый ключ сюда...",
|
||||
"enterPassword": "Введите пароль"
|
||||
"enterPassword": "Введите пароль",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Панель управления",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Выпущено {{issuer}}",
|
||||
"sslNames": "Обложки {{names}}",
|
||||
"sslManualTitle": "Загрузите сертификат",
|
||||
"updateExternalAutoProvisionFailed": "Не удалось обновить настройку автоматического создания."
|
||||
"updateExternalAutoProvisionFailed": "Не удалось обновить настройку автоматического создания.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Ажурирање је доступно. Отворите белешке о издању на GitHub-у."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Затвори",
|
||||
"minimize": "Минимизирај",
|
||||
"online": "Онлајн",
|
||||
"offline": "Офлајн",
|
||||
"signInFailed": "Пријављивање није успело",
|
||||
"unknown": "Непознато",
|
||||
"continue": "Настави",
|
||||
"maintenance": "Одржавање",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Уклони",
|
||||
"add": "Додај",
|
||||
"update": "Ажурирање",
|
||||
"noResults": "Нису пронађени резултати"
|
||||
"noResults": "Нису пронађени резултати",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Корак {{current}} од {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Тунел",
|
||||
"serverStats": "Метрике хоста",
|
||||
"status": {
|
||||
"available": "Доступно",
|
||||
"reachable": "Укључено, али последња пријава није успела",
|
||||
"online": "Online",
|
||||
"offline": "Офлајн",
|
||||
"monitoringDisabled": "Праћење је онемогућено",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Статус"
|
||||
},
|
||||
"folderRenamed": "Фасцикла „{{oldName}}“ је успешно преименована у „{{newName}}“",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Овај хост (директни тунел)",
|
||||
"statusChecksLabel": "Провере статуса",
|
||||
"enableStatusChecks": "Омогући провере статуса",
|
||||
"enableStatusChecksDesc": "Периодично пингујте овај хост да бисте проверили доступност",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Користи глобални интервал",
|
||||
"useGlobalIntervalDesc": "Замени интервал провере статуса на нивоу сервера",
|
||||
"checkIntervalS": "Интервал провере(е)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Ваш налог користи други фактор који тренутно није доступан. Контактирајте администратора да га ресетује.",
|
||||
"secondFactorNoUI": "Овај други фактор не може бити приказан на овом екрану. Контактирајте администратора.",
|
||||
"serverAuthentication": "Пријава на сервер",
|
||||
"minChars": "мин. {{min}} знакова"
|
||||
"minChars": "мин. {{min}} знакова",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Проверите SSH кључ хоста",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Језик",
|
||||
"keyPassword": "кључна лозинка",
|
||||
"pastePrivateKey": "Налепите свој приватни кључ овде...",
|
||||
"enterPassword": "Унесите лозинку"
|
||||
"enterPassword": "Унесите лозинку",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Контролна табла",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Издао/ла {{issuer}}",
|
||||
"sslNames": "Навлаке {{names}}",
|
||||
"sslManualTitle": "Отпреми сертификат",
|
||||
"updateExternalAutoProvisionFailed": "Ажурирање подешавања аутоматског креирања није успело"
|
||||
"updateExternalAutoProvisionFailed": "Ажурирање подешавања аутоматског креирања није успело",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Uppdatering tillgänglig. Öppna versionsinformationen på GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Nära",
|
||||
"minimize": "Minimera",
|
||||
"online": "Online",
|
||||
"offline": "Off-line",
|
||||
"signInFailed": "Inloggningen misslyckades",
|
||||
"unknown": "Okänd",
|
||||
"continue": "Fortsätta",
|
||||
"maintenance": "Underhåll",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Ta bort",
|
||||
"add": "Tillägga",
|
||||
"update": "Uppdatera",
|
||||
"noResults": "Inga resultat hittades"
|
||||
"noResults": "Inga resultat hittades",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Steg {{current}} av {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tunnel",
|
||||
"serverStats": "Värdstatistik",
|
||||
"status": {
|
||||
"available": "Tillgänglig",
|
||||
"reachable": "Uppe, men den senaste inloggningen misslyckades",
|
||||
"online": "Online",
|
||||
"offline": "Off-line",
|
||||
"monitoringDisabled": "Övervakning inaktiverad",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Status"
|
||||
},
|
||||
"folderRenamed": "Mappen \"{{oldName}}\" har bytt namn till \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Denna värd (direkt tunnel)",
|
||||
"statusChecksLabel": "Statuskontroller",
|
||||
"enableStatusChecks": "Aktivera statuskontroller",
|
||||
"enableStatusChecksDesc": "Pinga den här värden regelbundet för att verifiera tillgänglighet",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Använd globalt intervall",
|
||||
"useGlobalIntervalDesc": "Åsidosätt med det serveromfattande statuskontrollintervallet",
|
||||
"checkIntervalS": "Kontrollintervall (s)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Ditt konto använder en andra faktor som inte är tillgänglig just nu. Kontakta en administratör för att återställa den.",
|
||||
"secondFactorNoUI": "Den här andra faktorn kan inte visas på den här skärmen. Kontakta en administratör.",
|
||||
"serverAuthentication": "Serverinloggning",
|
||||
"minChars": "min. {{min}} tecken"
|
||||
"minChars": "min. {{min}} tecken",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Verifiera SSH-värdnyckel",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Språk",
|
||||
"keyPassword": "nyckellösenord",
|
||||
"pastePrivateKey": "Klistra in din privata nyckel här...",
|
||||
"enterPassword": "Ange ditt lösenord"
|
||||
"enterPassword": "Ange ditt lösenord",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Instrumentpanel",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Utfärdad av {{issuer}}",
|
||||
"sslNames": "Omslag {{names}}",
|
||||
"sslManualTitle": "Ladda upp ett certifikat",
|
||||
"updateExternalAutoProvisionFailed": "Misslyckades med att uppdatera inställningen för automatiskt skapande"
|
||||
"updateExternalAutoProvisionFailed": "Misslyckades med att uppdatera inställningen för automatiskt skapande",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "มีการอัปเดตแล้ว โปรดดูรายละเอียดการเปลี่ยนแปลงได้ที่ GitHub"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "ปิด",
|
||||
"minimize": "ลดขนาด",
|
||||
"online": "ออนไลน์",
|
||||
"offline": "ออฟไลน์",
|
||||
"signInFailed": "การเข้าสู่ระบบล้มเหลว",
|
||||
"unknown": "ไม่ทราบ",
|
||||
"continue": "ดำเนินการต่อ",
|
||||
"maintenance": "การซ่อมบำรุง",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "ลบ",
|
||||
"add": "เพิ่ม",
|
||||
"update": "อัปเดต",
|
||||
"noResults": "ไม่พบผลลัพธ์"
|
||||
"noResults": "ไม่พบผลลัพธ์",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "ขั้นตอน {{current}} ของ {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "อุโมงค์",
|
||||
"serverStats": "เมตริกโฮสต์",
|
||||
"status": {
|
||||
"available": "มีอยู่",
|
||||
"reachable": "ใช้งานได้ แต่การเข้าสู่ระบบครั้งสุดท้ายล้มเหลว",
|
||||
"online": "Online",
|
||||
"offline": "ออฟไลน์",
|
||||
"monitoringDisabled": "ปิดใช้งานการตรวจสอบ",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "สถานะ"
|
||||
},
|
||||
"folderRenamed": "เปลี่ยนชื่อโฟลเดอร์ \"{{oldName}}\" เป็น \"{{newName}}\" สำเร็จแล้ว",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "โฮสต์นี้ (อุโมงค์ตรง)",
|
||||
"statusChecksLabel": "การตรวจสอบสถานะ",
|
||||
"enableStatusChecks": "เปิดใช้งานการตรวจสอบสถานะ",
|
||||
"enableStatusChecksDesc": "ตรวจสอบความพร้อมใช้งานของโฮสต์นี้เป็นระยะโดยการ ping ไปยังโฮสต์นี้",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "ใช้ช่วงเวลาทั่วโลก",
|
||||
"useGlobalIntervalDesc": "แทนที่ด้วยช่วงเวลาตรวจสอบสถานะทั่วทั้งเซิร์ฟเวอร์",
|
||||
"checkIntervalS": "ช่วงเวลาตรวจสอบ",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "บัญชีของคุณใช้การยืนยันตัวตนสองขั้นตอน ซึ่งขณะนี้ไม่สามารถใช้งานได้ โปรดติดต่อผู้ดูแลระบบเพื่อรีเซ็ต",
|
||||
"secondFactorNoUI": "ปัจจัยที่สองนี้ไม่สามารถแสดงบนหน้าจอนี้ได้ โปรดติดต่อผู้ดูแลระบบ",
|
||||
"serverAuthentication": "การเข้าสู่ระบบเซิร์ฟเวอร์",
|
||||
"minChars": "อย่างน้อย {{min}} ตัวอักษร"
|
||||
"minChars": "อย่างน้อย {{min}} ตัวอักษร",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "ตรวจสอบคีย์โฮสต์ SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "ภาษา",
|
||||
"keyPassword": "รหัสผ่านสำคัญ",
|
||||
"pastePrivateKey": "วางรหัสส่วนตัวของคุณที่นี่...",
|
||||
"enterPassword": "ป้อนรหัสผ่านของคุณ"
|
||||
"enterPassword": "ป้อนรหัสผ่านของคุณ",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "แดชบอร์ด",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "ออกโดย {{issuer}}",
|
||||
"sslNames": "ปก {{names}}",
|
||||
"sslManualTitle": "อัปโหลดใบรับรอง",
|
||||
"updateExternalAutoProvisionFailed": "ไม่สามารถอัปเดตการตั้งค่าการสร้างอัตโนมัติได้"
|
||||
"updateExternalAutoProvisionFailed": "ไม่สามารถอัปเดตการตั้งค่าการสร้างอัตโนมัติได้",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Güncelleme mevcut. GitHub'daki sürüm notlarını açın."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Kapat",
|
||||
"minimize": "Simge Durumuna Küçült",
|
||||
"online": "Çevrimiçi",
|
||||
"offline": "Çevrimdışı",
|
||||
"signInFailed": "Giriş başarısız",
|
||||
"unknown": "Bilinmeyen",
|
||||
"continue": "Devam",
|
||||
"maintenance": "Bakım",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Kaldır",
|
||||
"add": "Ekle",
|
||||
"update": "Güncelle",
|
||||
"noResults": "Hiçbir sonuç bulunamadı."
|
||||
"noResults": "Hiçbir sonuç bulunamadı.",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Adım {{current}} , {{total}}'in",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Tünel",
|
||||
"serverStats": "Sunucu Metrikleri",
|
||||
"status": {
|
||||
"available": "Mevcut",
|
||||
"reachable": "Yukarı çıktı, ancak son giriş başarısız oldu.",
|
||||
"online": "Online",
|
||||
"offline": "Çevrimdışı",
|
||||
"monitoringDisabled": "İzleme devre dışı bırakıldı",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Durum"
|
||||
},
|
||||
"folderRenamed": "\"{{oldName}}\" klasörü başarıyla \"{{newName}}\" olarak yeniden adlandırıldı.",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Bu ana bilgisayar (doğrudan tünel)",
|
||||
"statusChecksLabel": "Durum Denetimleri",
|
||||
"enableStatusChecks": "Durum Denetimlerini Etkinleştir",
|
||||
"enableStatusChecksDesc": "Kullanılabilirliği doğrulamak için bu sunucuya düzenli olarak ping at",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Genel Aralığı Kullan",
|
||||
"useGlobalIntervalDesc": "Sunucu geneli durum denetim aralığını geçersiz kıl",
|
||||
"checkIntervalS": "Denetim Aralığı (sn)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Hesabınızda şu anda kullanılamayan ikinci bir faktör kimlik doğrulama yöntemi kullanılıyor. Sıfırlamak için bir yöneticiyle iletişime geçin.",
|
||||
"secondFactorNoUI": "Bu ikinci faktör bu ekranda gösterilemiyor. Lütfen bir yöneticiyle iletişime geçin.",
|
||||
"serverAuthentication": "Sunucuya giriş yap",
|
||||
"minChars": "min. {{min}} karakter"
|
||||
"minChars": "min. {{min}} karakter",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "SSH Sunucu Anahtarını Doğrula",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Dil",
|
||||
"keyPassword": "anahtar parolası",
|
||||
"pastePrivateKey": "Özel anahtarınızı buraya yapıştırın...",
|
||||
"enterPassword": "Parolanızı girin"
|
||||
"enterPassword": "Parolanızı girin",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Gösterge Paneli",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "{{issuer}} tarafından yayınlandı",
|
||||
"sslNames": "Kapaklar {{names}}",
|
||||
"sslManualTitle": "Sertifika yükleyin",
|
||||
"updateExternalAutoProvisionFailed": "Otomatik oluşturma ayarı güncellenemedi."
|
||||
"updateExternalAutoProvisionFailed": "Otomatik oluşturma ayarı güncellenemedi.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Доступне оновлення. Відкрийте нотатки до випуску на GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Закрити",
|
||||
"minimize": "Згорнути",
|
||||
"online": "Онлайн",
|
||||
"offline": "Офлайн",
|
||||
"signInFailed": "Не вдалося ввійти",
|
||||
"unknown": "Невідомо",
|
||||
"continue": "Продовжити",
|
||||
"maintenance": "Технічне обслуговування",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Видалити",
|
||||
"add": "Додати",
|
||||
"update": "Оновлення",
|
||||
"noResults": "Результатів не знайдено"
|
||||
"noResults": "Результатів не знайдено",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Крок {{current}} з {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Тунель",
|
||||
"serverStats": "Метрики хоста",
|
||||
"status": {
|
||||
"available": "Доступно",
|
||||
"reachable": "Вгору, але останній вхід не вдався",
|
||||
"online": "Online",
|
||||
"offline": "Офлайн",
|
||||
"monitoringDisabled": "Моніторинг вимкнено",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Статус"
|
||||
},
|
||||
"folderRenamed": "Папку «{{oldName}}» успішно перейменовано на «{{newName}}»",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Цей хост (прямий тунель)",
|
||||
"statusChecksLabel": "Перевірки статусу",
|
||||
"enableStatusChecks": "Увімкнути перевірки стану",
|
||||
"enableStatusChecksDesc": "Періодично перевіряти доступність цього хоста за допомогою ping-повідомлення",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Використовувати глобальний інтервал",
|
||||
"useGlobalIntervalDesc": "Перевизначити інтервал перевірки стану на рівні сервера",
|
||||
"checkIntervalS": "Інтервал перевірки (і)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Ваш обліковий запис використовує другий фактор, який зараз недоступний. Зверніться до адміністратора, щоб скинути його.",
|
||||
"secondFactorNoUI": "Цей другий фактор неможливо відобразити на цьому екрані. Зверніться до адміністратора.",
|
||||
"serverAuthentication": "Вхід на сервер",
|
||||
"minChars": "мін. {{min}} символів"
|
||||
"minChars": "мін. {{min}} символів",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Перевірте ключ хоста SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Мова",
|
||||
"keyPassword": "пароль ключа",
|
||||
"pastePrivateKey": "Вставте сюди свій закритий ключ...",
|
||||
"enterPassword": "Введіть свій пароль"
|
||||
"enterPassword": "Введіть свій пароль",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Інформаційна панель",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Видано {{issuer}}",
|
||||
"sslNames": "Обкладинки {{names}}",
|
||||
"sslManualTitle": "Завантажити сертифікат",
|
||||
"updateExternalAutoProvisionFailed": "Не вдалося оновити налаштування автоматичного створення"
|
||||
"updateExternalAutoProvisionFailed": "Не вдалося оновити налаштування автоматичного створення",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "Đã có bản cập nhật. Xem ghi chú phát hành trên GitHub."
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "Đóng",
|
||||
"minimize": "Thu nhỏ",
|
||||
"online": "Trực tuyến",
|
||||
"offline": "Ngoại tuyến",
|
||||
"signInFailed": "Đăng nhập không thành công",
|
||||
"unknown": "Không xác định",
|
||||
"continue": "Tiếp tục",
|
||||
"maintenance": "Bảo trì",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "Xóa",
|
||||
"add": "Thêm",
|
||||
"update": "Cập nhật",
|
||||
"noResults": "Không tìm thấy kết quả nào."
|
||||
"noResults": "Không tìm thấy kết quả nào.",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "Bước {{current}} của {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "Đường hầm",
|
||||
"serverStats": "Số liệu máy chủ",
|
||||
"status": {
|
||||
"available": "Có sẵn",
|
||||
"reachable": "Lên rồi, nhưng dấu hiệu cuối cùng đã thất bại.",
|
||||
"online": "Online",
|
||||
"offline": "Ngoại tuyến",
|
||||
"monitoringDisabled": "Chức năng giám sát đã bị vô hiệu hóa.",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "Trạng thái"
|
||||
},
|
||||
"folderRenamed": "Đã đổi tên thư mục \"{{oldName}}\" thành \"{{newName}}\" thành công",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "Máy chủ này (đường hầm trực tiếp)",
|
||||
"statusChecksLabel": "Kiểm tra trạng thái",
|
||||
"enableStatusChecks": "Bật Kiểm tra trạng thái",
|
||||
"enableStatusChecksDesc": "Định kỳ ping máy này để xác minh tính khả dụng",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "Dùng khoảng thời gian toàn cục",
|
||||
"useGlobalIntervalDesc": "Ghi đè bằng khoảng thời gian kiểm tra trạng thái toàn máy chủ",
|
||||
"checkIntervalS": "Khoảng kiểm tra (giây)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "Tài khoản của bạn sử dụng xác thực hai yếu tố hiện không khả dụng. Vui lòng liên hệ với quản trị viên để thiết lập lại.",
|
||||
"secondFactorNoUI": "Yếu tố thứ hai không thể hiển thị trên màn hình này. Vui lòng liên hệ với quản trị viên.",
|
||||
"serverAuthentication": "Đăng nhập máy chủ",
|
||||
"minChars": "tối thiểu {{min}} ký tự"
|
||||
"minChars": "tối thiểu {{min}} ký tự",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "Xác minh khóa máy chủ SSH",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "Ngôn ngữ",
|
||||
"keyPassword": "mật khẩu khóa",
|
||||
"pastePrivateKey": "Dán khóa riêng tư của bạn vào đây...",
|
||||
"enterPassword": "Nhập mật khẩu của bạn"
|
||||
"enterPassword": "Nhập mật khẩu của bạn",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Bảng điều khiển",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "Được phát hành bởi {{issuer}}",
|
||||
"sslNames": "Ảnh bìa {{names}}",
|
||||
"sslManualTitle": "Tải lên chứng chỉ",
|
||||
"updateExternalAutoProvisionFailed": "Không thể cập nhật cài đặt tự động tạo."
|
||||
"updateExternalAutoProvisionFailed": "Không thể cập nhật cài đặt tự động tạo.",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "有更新可用。请在 GitHub 上查看发布说明。"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "关闭",
|
||||
"minimize": "最小化",
|
||||
"online": "在线",
|
||||
"offline": "离线",
|
||||
"signInFailed": "登录失败",
|
||||
"unknown": "未知",
|
||||
"continue": "继续",
|
||||
"maintenance": "维护",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "移除",
|
||||
"add": "添加",
|
||||
"update": "更新",
|
||||
"noResults": "未找到结果"
|
||||
"noResults": "未找到结果",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "步骤 {{current}}/{{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "隧道",
|
||||
"serverStats": "主机指标",
|
||||
"status": {
|
||||
"available": "可用的",
|
||||
"reachable": "已登录,但最后一个登录信息失败了。",
|
||||
"online": "Online",
|
||||
"offline": "离线",
|
||||
"monitoringDisabled": "监控已禁用",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "地位"
|
||||
},
|
||||
"folderRenamed": "文件夹 \"{{oldName}}\" 已成功重命名为 \"{{newName}}\"",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "此主机(直接隧道)",
|
||||
"statusChecksLabel": "状态检查",
|
||||
"enableStatusChecks": "启用状态检查",
|
||||
"enableStatusChecksDesc": "定期 ping 此主机以验证可用性",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "使用全局间隔",
|
||||
"useGlobalIntervalDesc": "使用服务器范围的状态检查间隔覆盖",
|
||||
"checkIntervalS": "检查间隔(秒)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "您的账户启用了第二重验证,但该验证目前不可用。请联系管理员重置验证。",
|
||||
"secondFactorNoUI": "第二个因素无法在此屏幕上显示。请联系管理员。",
|
||||
"serverAuthentication": "服务器登录",
|
||||
"minChars": "最小 {{min}} 个字符"
|
||||
"minChars": "最小 {{min}} 个字符",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "验证 SSH 主机密钥",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "语言",
|
||||
"keyPassword": "密钥密码",
|
||||
"pastePrivateKey": "在此粘贴您的私钥...",
|
||||
"enterPassword": "输入您的密码"
|
||||
"enterPassword": "输入您的密码",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "仪表盘",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "由 {{issuer}} 发行",
|
||||
"sslNames": "封面 {{names}}",
|
||||
"sslManualTitle": "上传证书",
|
||||
"updateExternalAutoProvisionFailed": "自动创建设置更新失败"
|
||||
"updateExternalAutoProvisionFailed": "自动创建设置更新失败",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
"updateLinkLabel": "有更新可用。請在 GitHub 上查看發布說明。"
|
||||
},
|
||||
"common": {
|
||||
"scrollTabsLeft": "Scroll tabs left",
|
||||
"scrollTabsRight": "Scroll tabs right",
|
||||
"close": "關閉",
|
||||
"minimize": "最小化",
|
||||
"online": "線上",
|
||||
"offline": "離線",
|
||||
"signInFailed": "登入失敗",
|
||||
"unknown": "未知",
|
||||
"continue": "繼續",
|
||||
"maintenance": "維護",
|
||||
@@ -187,7 +188,11 @@
|
||||
"remove": "移除",
|
||||
"add": "新增",
|
||||
"update": "更新",
|
||||
"noResults": "未找到結果"
|
||||
"noResults": "未找到結果",
|
||||
"sidebar": "Sidebar",
|
||||
"mobileSidebarDescription": "Displays the mobile sidebar.",
|
||||
"toggleSidebar": "Toggle Sidebar",
|
||||
"search": "Search..."
|
||||
},
|
||||
"onboarding": {
|
||||
"stepCounter": "步驟 {{current}} 的 {{total}}",
|
||||
@@ -352,10 +357,10 @@
|
||||
"tunnel": "通道",
|
||||
"serverStats": "主機指標",
|
||||
"status": {
|
||||
"available": "可用的",
|
||||
"reachable": "已登錄,但最後一個登入資訊失敗了。",
|
||||
"online": "Online",
|
||||
"offline": "離線",
|
||||
"monitoringDisabled": "監控已停用",
|
||||
"checking": "Checking",
|
||||
"monitoringDisabled": "Status checks off",
|
||||
"label": "地位"
|
||||
},
|
||||
"folderRenamed": "資料夾「{{oldName}}」已成功重新命名為「{{newName}}」",
|
||||
@@ -477,7 +482,7 @@
|
||||
"sameHost": "此主機(直接通道)",
|
||||
"statusChecksLabel": "狀態檢查",
|
||||
"enableStatusChecks": "啟用狀態檢查",
|
||||
"enableStatusChecksDesc": "定期 ping 此主機以驗證可用性",
|
||||
"enableStatusChecksDesc": "Check host reachability. Active sessions skip extra probes. SSH probes without an active session may trigger aggressive Fail2Ban rules; disable status checks on those hosts.",
|
||||
"useGlobalInterval": "使用全域間隔",
|
||||
"useGlobalIntervalDesc": "以伺服器範圍的狀態檢查間隔覆寫",
|
||||
"checkIntervalS": "檢查間隔(秒)",
|
||||
@@ -1058,7 +1063,8 @@
|
||||
"secondFactorUnavailable": "您的帳戶啟用了第二重驗證,但目前不可用。請聯絡管理員重置驗證。",
|
||||
"secondFactorNoUI": "第二個因素無法在此畫面上顯示。請聯絡管理員。",
|
||||
"serverAuthentication": "伺服器登入",
|
||||
"minChars": "最小字元數 {{min}}"
|
||||
"minChars": "最小字元數 {{min}}",
|
||||
"retrySecondFactorUI": "Reload verification methods"
|
||||
},
|
||||
"hostKey": {
|
||||
"verifyNewHost": "驗證 SSH 主機金鑰",
|
||||
@@ -1119,7 +1125,13 @@
|
||||
"language": "語言",
|
||||
"keyPassword": "金鑰密碼",
|
||||
"pastePrivateKey": "在此貼上您的私密金鑰...",
|
||||
"enterPassword": "輸入您的密碼"
|
||||
"enterPassword": "輸入您的密碼",
|
||||
"apiKeyName": "e.g., CI Pipeline",
|
||||
"credentialName": "e.g. Production SSH Key",
|
||||
"optionalDetails": "Optional details...",
|
||||
"sshUsername": "e.g. root or deploy",
|
||||
"hostAddress": "10.0.0.1 or example.com",
|
||||
"hostName": "e.g. Web Server Production"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "儀表板",
|
||||
@@ -1501,7 +1513,10 @@
|
||||
"sslIssuer": "由 {{issuer}} 發行",
|
||||
"sslNames": "封面 {{names}}",
|
||||
"sslManualTitle": "上傳證書",
|
||||
"updateExternalAutoProvisionFailed": "自動建立設定更新失敗"
|
||||
"updateExternalAutoProvisionFailed": "自動建立設定更新失敗",
|
||||
"hostTags": "Predefined host tags",
|
||||
"hostTagsDesc": "Tags every user sees as suggestions in the host editor. Press Space or Enter to add one. Removing a tag here does not remove it from hosts.",
|
||||
"hostTagsError": "Could not load or save host tag suggestions."
|
||||
},
|
||||
"newUi": {
|
||||
"sidebar": {
|
||||
|
||||
@@ -63,13 +63,12 @@ import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
||||
import { getDeviceId } from "@/lib/device-id";
|
||||
|
||||
export type ServerStatus = {
|
||||
status: "online" | "reachable" | "offline";
|
||||
status: "online" | "offline";
|
||||
lastChecked: string;
|
||||
reason?: "host_key_changed";
|
||||
};
|
||||
|
||||
export type SSHHostWithStatus = SSHHost & {
|
||||
status: "online" | "reachable" | "offline" | "unknown";
|
||||
status: "online" | "offline" | "unknown";
|
||||
};
|
||||
|
||||
export interface AuthResponse {
|
||||
|
||||
@@ -46,7 +46,10 @@ import { invokeAction } from "@/shell/action-registry";
|
||||
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
|
||||
import { useActionSlot } from "@/hooks/use-action-slot";
|
||||
import { usePluginComponent } from "./component-registry";
|
||||
import { useOptionalHostStatusEntry } from "@/lib/ServerStatusContext";
|
||||
import {
|
||||
getLiveHostStatus,
|
||||
useOptionalHostStatusEntry,
|
||||
} from "@/lib/ServerStatusContext";
|
||||
|
||||
/**
|
||||
* Core permission groups, mirroring RESERVED_PERMISSION_PREFIXES in the SDK
|
||||
@@ -370,12 +373,7 @@ export const pluginHostBridge: PluginHostBridge = {
|
||||
usePluginComponent: (id) => usePluginComponent(id),
|
||||
|
||||
useHostStatus: (hostId) => {
|
||||
const entry = useOptionalHostStatusEntry(hostId);
|
||||
if (!entry) return null;
|
||||
return {
|
||||
status: entry.status === "degraded" ? "unknown" : entry.status,
|
||||
...(entry.reason ? { reason: entry.reason } : {}),
|
||||
};
|
||||
return useOptionalHostStatusEntry(hostId);
|
||||
},
|
||||
|
||||
useHostActions: () => useHostActions() as unknown as HostActionContribution[],
|
||||
@@ -429,9 +427,13 @@ export const pluginHostBridge: PluginHostBridge = {
|
||||
void setCookie(name, value, 365);
|
||||
},
|
||||
listHosts: async (pluginId) =>
|
||||
(await getSSHHosts({ includeStatus: false })).map((host) =>
|
||||
toPluginHostRecord(host, pluginId ?? null),
|
||||
),
|
||||
(await getSSHHosts()).map((host) => {
|
||||
const status = getLiveHostStatus(host.id);
|
||||
return toPluginHostRecord(
|
||||
{ ...host, status, online: status === "online" },
|
||||
pluginId ?? null,
|
||||
);
|
||||
}),
|
||||
notifyHostsChanged: () => {
|
||||
window.dispatchEvent(new CustomEvent("termix:hosts-changed"));
|
||||
},
|
||||
|
||||
@@ -449,7 +449,9 @@ async function fetchPreLoginPlugins(): Promise<PluginSummary[]> {
|
||||
* factors) before anyone has signed in. After sign-in the full runtime
|
||||
* reconciles, keeping these active when their bundle has not changed.
|
||||
*/
|
||||
export async function startPreLoginPlugins(): Promise<void> {
|
||||
export async function startPreLoginPlugins(
|
||||
options: { retryFailed?: boolean } = {},
|
||||
): Promise<void> {
|
||||
installPluginHostBridge();
|
||||
let list: PluginSummary[];
|
||||
try {
|
||||
@@ -468,6 +470,9 @@ export async function startPreLoginPlugins(): Promise<void> {
|
||||
// subset would unload everything else. Auth remounts during the
|
||||
// post-login fade, so this runs after sign-in every time.
|
||||
if (started) return;
|
||||
if (options.retryFailed) {
|
||||
for (const plugin of list) failedVersions.delete(plugin.id);
|
||||
}
|
||||
await reconcile(list);
|
||||
markPluginsSettled();
|
||||
});
|
||||
|
||||
@@ -77,9 +77,12 @@ export function FeatureSettingsSection({
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<AccordionSection
|
||||
label={plugin.name}
|
||||
label={t(pluginKey(plugin.id, "plugin.name"), {
|
||||
defaultValue: plugin.name,
|
||||
})}
|
||||
icon={<PluginIcon name={plugin.icon} className="size-3.5" />}
|
||||
open={open}
|
||||
onToggle={onToggle}
|
||||
@@ -206,7 +209,11 @@ function FeatureSettingsForm({
|
||||
<div className="flex flex-col gap-2 pt-2">
|
||||
{!running && (
|
||||
<div className="border border-yellow-500/40 bg-yellow-500/10 px-3 py-2 text-xs text-yellow-500">
|
||||
{t("settings.featureUnavailable", { name: plugin.name })}
|
||||
{t("settings.featureUnavailable", {
|
||||
name: t(pluginKey(plugin.id, "plugin.name"), {
|
||||
defaultValue: plugin.name,
|
||||
}),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from "./palette-registry";
|
||||
import { activityTarget } from "@/lib/activity-types";
|
||||
import { shell } from "@/plugin-host/shell-bridge";
|
||||
import { getLiveHostStatus } from "@/lib/ServerStatusContext";
|
||||
|
||||
interface CommandPaletteProps {
|
||||
isOpen: boolean;
|
||||
@@ -501,7 +502,7 @@ export function CommandPalette({
|
||||
<Server
|
||||
className={cn(
|
||||
"size-4",
|
||||
host.online
|
||||
getLiveHostStatus(Number(host.id)) === "online"
|
||||
? "text-accent-brand"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
@@ -517,7 +518,8 @@ export function CommandPalette({
|
||||
{t("hosts.sharing.sharedBadge")}
|
||||
</span>
|
||||
)}
|
||||
{host.online && (
|
||||
{getLiveHostStatus(Number(host.id)) ===
|
||||
"online" && (
|
||||
<span className="size-1.5 rounded-full bg-accent-brand animate-pulse shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
@@ -646,7 +648,9 @@ export function CommandPalette({
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-none hover:bg-accent-brand/10 cursor-pointer"
|
||||
>
|
||||
<LifeBuoy className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Support</span>
|
||||
<span className="text-sm font-medium">
|
||||
{t("dashboard.support")}
|
||||
</span>
|
||||
</CommandItem>
|
||||
</div>
|
||||
</CommandGroup>
|
||||
|
||||
@@ -150,7 +150,7 @@ export function AdminApiKeysSection({
|
||||
<span className="text-accent-brand">*</span>
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g., CI Pipeline"
|
||||
placeholder={t("placeholders.apiKeyName")}
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
className="text-xs"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { X } from "lucide-react";
|
||||
import { getHostTags, saveHostTags } from "@/api/host-tags-api";
|
||||
|
||||
export function AdminHostTags() {
|
||||
const { t } = useTranslation();
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
getHostTags()
|
||||
.then((list) => {
|
||||
if (cancelled) return;
|
||||
setTags(list);
|
||||
setLoaded(true);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) toast.error(t("admin.hostTagsError"));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
async function save(next: string[]) {
|
||||
const previous = tags;
|
||||
setTags(next);
|
||||
setSaving(true);
|
||||
try {
|
||||
setTags(await saveHostTags(next));
|
||||
window.dispatchEvent(new Event("termix:host-tags-changed"));
|
||||
} catch {
|
||||
setTags(previous);
|
||||
toast.error(t("admin.hostTagsError"));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function add(raw: string) {
|
||||
const tag = raw.trim();
|
||||
setInput("");
|
||||
if (!tag || tags.includes(tag)) return;
|
||||
void save([...tags, tag]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="global-host-tags" className="text-xs font-medium">
|
||||
{t("admin.hostTags")}
|
||||
</label>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.hostTagsDesc")}
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-1 min-h-9 px-2 py-1 border border-border bg-background focus-within:ring-1 focus-within:ring-ring">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] bg-muted border border-border/60 text-foreground"
|
||||
>
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
aria-label={t("common.remove")}
|
||||
onClick={() => void save(tags.filter((tg) => tg !== tag))}
|
||||
className="text-muted-foreground hover:text-destructive ml-0.5"
|
||||
>
|
||||
<X className="size-2.5" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
id="global-host-tags"
|
||||
className="flex-1 min-w-16 text-xs bg-transparent outline-none placeholder:text-muted-foreground/50"
|
||||
placeholder={tags.length === 0 ? t("hosts.addTag") : ""}
|
||||
disabled={!loaded || saving}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onBlur={() => add(input)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.nativeEvent.isComposing) return;
|
||||
if ((e.key === " " || e.key === "Enter") && input.trim()) {
|
||||
e.preventDefault();
|
||||
add(input);
|
||||
} else if (e.key === "Backspace" && !input && tags.length > 0) {
|
||||
void save(tags.slice(0, -1));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export function AdminSettingsPanel({
|
||||
const [passwordLoginForced, setPasswordLoginForced] = useState(false);
|
||||
const [allowPasswordReset, setAllowPasswordReset] = useState(true);
|
||||
const [sessionTimeout, setSessionTimeout] = useState("24");
|
||||
const [statusInterval, setStatusInterval] = useState("60");
|
||||
const [statusInterval, setStatusInterval] = useState("30");
|
||||
const [logLevel, setLogLevel] = useState("info");
|
||||
const [notificationPrivateEndpoints, setNotificationPrivateEndpoints] =
|
||||
useState<string[]>([]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Input } from "@/components/input";
|
||||
import { SettingRow } from "@/components/section-card";
|
||||
import { Database, Lock, RefreshCw, Server, Settings } from "lucide-react";
|
||||
import { AccordionSection, AdminToggle } from "./AdminSettingsShared";
|
||||
import { AdminHostTags } from "./AdminHostTags";
|
||||
import type { TlsStatus } from "@/api/tls-api";
|
||||
|
||||
type GeneralSettingsSectionProps = {
|
||||
@@ -349,6 +350,9 @@ export function AdminHostDefaultsSection({
|
||||
>
|
||||
{t("hostDefaults.editServerDefaults")}
|
||||
</Button>
|
||||
<div className="border-t border-border pt-3">
|
||||
<AdminHostTags />
|
||||
</div>
|
||||
</div>
|
||||
</AccordionSection>
|
||||
);
|
||||
|
||||
@@ -183,7 +183,7 @@ export function CredentialEditorView({
|
||||
{t("hosts.friendlyNameLabel")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. Production SSH Key"
|
||||
placeholder={t("placeholders.credentialName")}
|
||||
value={credForm.name}
|
||||
onChange={(e) => setCredField("name", e.target.value)}
|
||||
/>
|
||||
@@ -203,7 +203,7 @@ export function CredentialEditorView({
|
||||
{t("hosts.descriptionLabel")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="Optional details..."
|
||||
placeholder={t("placeholders.optionalDetails")}
|
||||
value={credForm.description}
|
||||
onChange={(e) => setCredField("description", e.target.value)}
|
||||
/>
|
||||
@@ -278,7 +278,7 @@ export function CredentialEditorView({
|
||||
{t("hosts.username")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. root or deploy"
|
||||
placeholder={t("placeholders.sshUsername")}
|
||||
value={credForm.username}
|
||||
onChange={(e) => setCredField("username", e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -1444,7 +1444,7 @@ export function HostEditor({
|
||||
{t("hosts.friendlyNameLabel")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. Production SSH Key"
|
||||
placeholder={t("placeholders.credentialName")}
|
||||
value={quickCredentialName}
|
||||
onChange={(e) => setQuickCredentialName(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { getHostTags } from "@/api/host-tags-api";
|
||||
import type { ProxyNode } from "@/types/index";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/button";
|
||||
@@ -47,6 +48,39 @@ export function HostEditorGeneralTab({
|
||||
const { t } = useTranslation();
|
||||
const syncLinked = !!useSyncStatus()?.linked;
|
||||
const pluginProtocols = useHostProtocols();
|
||||
const [predefinedTags, setPredefinedTags] = React.useState<string[]>([]);
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
const load = () => {
|
||||
void getHostTags()
|
||||
.then((tags) => {
|
||||
if (!cancelled) setPredefinedTags(tags);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
load();
|
||||
window.addEventListener("termix:host-tags-changed", load);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.removeEventListener("termix:host-tags-changed", load);
|
||||
};
|
||||
}, []);
|
||||
const suggestedTags = [
|
||||
...new Set([
|
||||
...predefinedTags,
|
||||
...hosts.flatMap((item) => item.tags ?? []),
|
||||
]),
|
||||
].filter(
|
||||
(tag) =>
|
||||
!form.tags.includes(tag) &&
|
||||
tag
|
||||
.toLocaleLowerCase()
|
||||
.includes(form.tagInput.trim().toLocaleLowerCase()),
|
||||
);
|
||||
const addTag = (tag: string) => {
|
||||
if (!form.tags.includes(tag)) setField("tags", [...form.tags, tag]);
|
||||
setField("tagInput", "");
|
||||
};
|
||||
|
||||
// Tracks which picker is shown, independent of whether a value is set yet
|
||||
// -- switching to "parent host" mode with nothing picked shouldn't bounce
|
||||
@@ -174,7 +208,7 @@ export function HostEditorGeneralTab({
|
||||
{t("hosts.addressIp")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="10.0.0.1 or example.com"
|
||||
placeholder={t("placeholders.hostAddress")}
|
||||
value={form.ip}
|
||||
onChange={(e) => setField("ip", e.target.value)}
|
||||
/>
|
||||
@@ -186,7 +220,7 @@ export function HostEditorGeneralTab({
|
||||
{t("hosts.friendlyName")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="e.g. Web Server Production"
|
||||
placeholder={t("placeholders.hostName")}
|
||||
value={form.name}
|
||||
onChange={(e) => setField("name", e.target.value)}
|
||||
/>
|
||||
@@ -284,13 +318,11 @@ export function HostEditorGeneralTab({
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
(e.key === " " || e.key === "Enter") &&
|
||||
!e.nativeEvent.isComposing &&
|
||||
form.tagInput.trim()
|
||||
) {
|
||||
e.preventDefault();
|
||||
const tag = form.tagInput.trim();
|
||||
if (!form.tags.includes(tag))
|
||||
setField("tags", [...form.tags, tag]);
|
||||
setField("tagInput", "");
|
||||
addTag(form.tagInput.trim());
|
||||
} else if (
|
||||
e.key === "Backspace" &&
|
||||
!form.tagInput &&
|
||||
@@ -302,6 +334,24 @@ export function HostEditorGeneralTab({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{suggestedTags.length > 0 && (
|
||||
<div
|
||||
className="col-span-2 flex flex-wrap gap-1 max-h-24 overflow-y-auto"
|
||||
aria-label={t("admin.hostTags")}
|
||||
>
|
||||
{suggestedTags.map((tag) => (
|
||||
<button
|
||||
type="button"
|
||||
key={tag}
|
||||
onClick={() => addTag(tag)}
|
||||
className="flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] border border-dashed border-border text-muted-foreground hover:text-foreground hover:border-foreground/40 transition-colors"
|
||||
>
|
||||
<Plus className="size-2.5" />
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5 col-span-2">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.privateNotes")}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { useEffect, useRef, type ComponentType, type ReactNode } from "react";
|
||||
import { KeyRound, Settings, Terminal } from "lucide-react";
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentType,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
KeyRound,
|
||||
Settings,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { byOrderThenId, createRegistry } from "@/lib/registry";
|
||||
|
||||
/** Core host editor tabs. Plugins add theirs through registerHostEditorSection. */
|
||||
@@ -197,6 +210,27 @@ export function TabStrip({
|
||||
variant?: "primary" | "secondary";
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const [scroll, setScroll] = useState({ left: false, right: false });
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const update = () =>
|
||||
setScroll({
|
||||
left: el.scrollLeft > 1,
|
||||
right: el.scrollLeft + el.clientWidth < el.scrollWidth - 1,
|
||||
});
|
||||
const observer = new ResizeObserver(update);
|
||||
observer.observe(el);
|
||||
if (el.firstElementChild) observer.observe(el.firstElementChild);
|
||||
el.addEventListener("scroll", update);
|
||||
update();
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
el.removeEventListener("scroll", update);
|
||||
};
|
||||
}, [tabs]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
@@ -229,14 +263,48 @@ export function TabStrip({
|
||||
);
|
||||
};
|
||||
|
||||
const overflow = scroll.left || scroll.right;
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`overflow-x-auto scrollbar-none ${
|
||||
variant === "secondary" ? "border-t border-border bg-card" : ""
|
||||
}`}
|
||||
className={`flex min-w-0 items-center ${variant === "secondary" ? "border-t border-border bg-card" : ""}`}
|
||||
>
|
||||
<div className="flex min-w-max">{tabs.map(renderTab)}</div>
|
||||
{overflow && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("common.scrollTabsLeft")}
|
||||
title={t("common.scrollTabsLeft")}
|
||||
disabled={!scroll.left}
|
||||
className="shrink-0 p-1 disabled:opacity-30"
|
||||
onClick={() =>
|
||||
ref.current?.scrollBy({
|
||||
left: -ref.current.clientWidth * 0.75,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
<div ref={ref} className="min-w-0 flex-1 overflow-x-auto scrollbar-none">
|
||||
<div className="flex min-w-max">{tabs.map(renderTab)}</div>
|
||||
</div>
|
||||
{overflow && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("common.scrollTabsRight")}
|
||||
title={t("common.scrollTabsRight")}
|
||||
disabled={!scroll.right}
|
||||
className="shrink-0 p-1 disabled:opacity-30"
|
||||
onClick={() =>
|
||||
ref.current?.scrollBy({
|
||||
left: ref.current.clientWidth * 0.75,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getErrorMessage } from "../lib/error-message.js";
|
||||
import { enabledHostProtocols, useHostProtocols } from "./host-protocols";
|
||||
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { HostData } from "@/types/index";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
@@ -53,6 +53,8 @@ import {
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
import type { Host, HostFolder, TabType } from "@/types/ui-types";
|
||||
import { sortHostTree, type SortKey } from "@/sidebar/host-sort";
|
||||
import { withLiveStatus } from "@/sidebar/live-host-status";
|
||||
import { useServerStatus } from "@/lib/ServerStatusContext";
|
||||
import { useHostSidebarPreferences } from "@/sidebar/tree/hooks/useHostSidebarPreferences";
|
||||
import { useArrangeLock } from "@/sidebar/use-arrange-lock";
|
||||
import type {
|
||||
@@ -137,9 +139,7 @@ function hostPassesFilters(
|
||||
if (filters.status.length > 0) {
|
||||
const ok =
|
||||
(filters.status.includes("online") && host.online) ||
|
||||
(filters.status.includes("offline") &&
|
||||
host.status !== "reachable" &&
|
||||
!host.online) ||
|
||||
(filters.status.includes("offline") && host.status === "offline") ||
|
||||
(filters.status.includes("pinned") && !!host.pin);
|
||||
if (!ok) return false;
|
||||
}
|
||||
@@ -219,6 +219,13 @@ export function HostsPanel({
|
||||
active?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { statuses, getStatus } = useServerStatus();
|
||||
const liveHostTree = useMemo(
|
||||
() => (hostTree ? withLiveStatus(hostTree, getStatus) : undefined),
|
||||
// statuses changes identity whenever a host's status changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[hostTree, statuses, getStatus],
|
||||
);
|
||||
const hostSwitchPlugins = usePluginHostSections().filter(
|
||||
(plugin) => !!plugin.contributes?.settings?.host?.enableKey,
|
||||
);
|
||||
@@ -1028,10 +1035,10 @@ export function HostsPanel({
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<SidebarTree
|
||||
children={
|
||||
hostTree
|
||||
liveHostTree
|
||||
? groupHosts(
|
||||
applyFilters(
|
||||
sortHostTree(hostTree, sortKey, pinnedFirst),
|
||||
sortHostTree(liveHostTree, sortKey, pinnedFirst),
|
||||
filterState,
|
||||
hostSwitches,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Host, HostFolder } from "@/types/ui-types";
|
||||
import type { StatusValue } from "@/lib/server-status-store";
|
||||
import { isFolder } from "./tree/visible-rows";
|
||||
|
||||
/**
|
||||
* Copies each host's live status onto the tree so sorting, filtering and
|
||||
* grouping by status read the same value as the row's dot. A host with
|
||||
* status checks off gets no status.
|
||||
*/
|
||||
export function withLiveStatus(
|
||||
folder: HostFolder,
|
||||
getStatus: (hostId: number) => StatusValue,
|
||||
): HostFolder {
|
||||
return {
|
||||
...folder,
|
||||
children: folder.children.map((child) => {
|
||||
if (isFolder(child)) return withLiveStatus(child, getStatus);
|
||||
const host = child as Host;
|
||||
const status =
|
||||
host.statusCheckEnabled === false
|
||||
? undefined
|
||||
: getStatus(Number(host.id));
|
||||
if (host.status === status) return host;
|
||||
return { ...host, status, online: status === "online" };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** The flat-list version of withLiveStatus. */
|
||||
export function withLiveHostStatus(
|
||||
hosts: Host[],
|
||||
getStatus: (hostId: number) => StatusValue,
|
||||
): Host[] {
|
||||
return withLiveStatus({ name: "", children: hosts }, getStatus)
|
||||
.children as Host[];
|
||||
}
|
||||
@@ -15,23 +15,12 @@ import { HostItem, statusCheckEnabled } from "../HostItem/HostItem";
|
||||
import { isFolder, folderHasMatch, collectAllHosts } from "../visible-rows";
|
||||
import { FolderActions } from "./FolderActions";
|
||||
|
||||
export function folderHostCount(folder: HostFolder): {
|
||||
total: number;
|
||||
online: number;
|
||||
} {
|
||||
let total = 0,
|
||||
online = 0;
|
||||
export function folderHostCount(folder: HostFolder): number {
|
||||
let total = 0;
|
||||
for (const child of folder.children) {
|
||||
if (isFolder(child)) {
|
||||
const c = folderHostCount(child);
|
||||
total += c.total;
|
||||
online += c.online;
|
||||
} else {
|
||||
total++;
|
||||
if (child.online) online++;
|
||||
}
|
||||
total += isFolder(child) ? folderHostCount(child) : 1;
|
||||
}
|
||||
return { total, online };
|
||||
return total;
|
||||
}
|
||||
|
||||
export function FolderItem({
|
||||
@@ -140,13 +129,11 @@ export function FolderItem({
|
||||
onReorderHoverChange?: (edge: "before" | "after" | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { getStatus, initialLoadComplete } = useServerStatus();
|
||||
const { total } = folderHostCount(folder);
|
||||
const online = initialLoadComplete
|
||||
? collectAllHosts(folder.children).filter(
|
||||
(h) => statusCheckEnabled(h) && getStatus(Number(h.id)) === "online",
|
||||
).length
|
||||
: folderHostCount(folder).online;
|
||||
const { getStatus } = useServerStatus();
|
||||
const total = folderHostCount(folder);
|
||||
const online = collectAllHosts(folder.children).filter(
|
||||
(h) => statusCheckEnabled(h) && getStatus(Number(h.id)) === "online",
|
||||
).length;
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const reorderEdge = isReorderHovered ? reorderHoverEdge : null;
|
||||
|
||||
|
||||
@@ -59,11 +59,7 @@ import {
|
||||
useStatusColorScheme,
|
||||
getStatusClasses,
|
||||
} from "@/hooks/use-status-color-scheme";
|
||||
import {
|
||||
useHostStatus,
|
||||
useHostStatusReason,
|
||||
useServerStatusMeta,
|
||||
} from "@/lib/ServerStatusContext";
|
||||
import { useHostStatus } from "@/lib/ServerStatusContext";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -101,16 +97,16 @@ export function statusCheckEnabled(host: Host): boolean {
|
||||
|
||||
export function buildStatusTooltip(
|
||||
host: Host,
|
||||
status: "online" | "reachable" | "offline",
|
||||
status: "online" | "offline" | "unknown",
|
||||
t: (key: string) => string = (k) => k,
|
||||
): string {
|
||||
if (!statusCheckEnabled(host)) return t("hosts.status.monitoringDisabled");
|
||||
const statusLabel =
|
||||
status === "online"
|
||||
? t("hosts.status.available")
|
||||
: status === "reachable"
|
||||
? t("hosts.status.reachable")
|
||||
: t("hosts.status.offline");
|
||||
if (!statusCheckEnabled(host)) return t("hosts.status.monitoringDisabled");
|
||||
? t("hosts.status.online")
|
||||
: status === "offline"
|
||||
? t("hosts.status.offline")
|
||||
: t("hosts.status.checking");
|
||||
const protocols: string[] = [];
|
||||
if (host.enableSsh) protocols.push("SSH");
|
||||
for (const protocol of enabledHostProtocols(host)) {
|
||||
@@ -283,22 +279,11 @@ export function HostItem({
|
||||
const pluginMenuItems = hostMenuItemsFor(useHostContextMenuItems(), host);
|
||||
const splitTargets = useSplitTargets();
|
||||
const statusScheme = useStatusColorScheme();
|
||||
const { initialLoadComplete } = useServerStatusMeta();
|
||||
const statusCheckOn = statusCheckEnabled(host);
|
||||
const statusLoading = !initialLoadComplete && statusCheckOn;
|
||||
// Per-host subscription — status polls only re-render rows that flipped.
|
||||
const liveStatus = useHostStatus(Number(host.id), statusCheckOn);
|
||||
const statusReason = useHostStatusReason(Number(host.id), statusCheckOn);
|
||||
// Per-host subscription, so a poll only re-renders rows that flipped.
|
||||
const availability =
|
||||
liveStatus === "online" ||
|
||||
liveStatus === "reachable" ||
|
||||
liveStatus === "offline"
|
||||
? liveStatus
|
||||
: host.status === "reachable"
|
||||
? "reachable"
|
||||
: host.online
|
||||
? "online"
|
||||
: "offline";
|
||||
useHostStatus(Number(host.id), statusCheckOn) ?? "offline";
|
||||
const statusLoading = availability === "unknown";
|
||||
const isOnline = availability === "online";
|
||||
const previousAvailability = useRef(availability);
|
||||
const [statusLocking, setStatusLocking] = useState(false);
|
||||
@@ -1096,7 +1081,10 @@ export function HostItem({
|
||||
className={`flex flex-col flex-1 min-w-0 ${tokens.rowPadding} ${isCompact ? "" : "gap-[3.5px]"}`}
|
||||
>
|
||||
{/* Name row */}
|
||||
<div data-drag-label className="flex items-center gap-1.5 min-w-0">
|
||||
<div
|
||||
data-drag-label
|
||||
className={`flex items-center gap-1.5 min-w-0 ${isCompact ? "min-h-[19px]" : ""}`}
|
||||
>
|
||||
{onToggleExpand && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1133,9 +1121,7 @@ export function HostItem({
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{statusReason === "host_key_changed"
|
||||
? `${t("hostKey.keyChangedWarning")}: ${t("hostKey.keyChangedDescription")}`
|
||||
: buildStatusTooltip(host, availability, t)}
|
||||
{buildStatusTooltip(host, availability, t)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -1190,6 +1176,23 @@ export function HostItem({
|
||||
{host.ip}
|
||||
</span>
|
||||
)}
|
||||
{isCompact && !selectionMode && (
|
||||
<div
|
||||
data-testid="host-inline-actions"
|
||||
className={`flex items-center gap-[2px] min-w-0 max-w-[60%] overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&>*]:shrink-0 ${alwaysShowTray || actionsOnly || trayOpenState || hoverTrayOpen ? "" : "hidden"}`}
|
||||
>
|
||||
{connectionButtons}
|
||||
<div
|
||||
className={
|
||||
alwaysShowTray || trayOpenState || hoverTrayOpen
|
||||
? "flex items-center gap-[2px]"
|
||||
: "hidden"
|
||||
}
|
||||
>
|
||||
{managementButtons}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!selectionMode && (shouldUseClickTray || actionsOnly) && (
|
||||
<button
|
||||
title={
|
||||
@@ -1239,7 +1242,8 @@ export function HostItem({
|
||||
)}
|
||||
|
||||
{/* Connection buttons: permanent in "always"/"actionsOnly" modes, or shown once the chevron opens the tray in click mode */}
|
||||
{!selectionMode &&
|
||||
{!isCompact &&
|
||||
!selectionMode &&
|
||||
(alwaysShowTray ||
|
||||
actionsOnly ||
|
||||
(shouldUseClickTray && isTrayOpen)) && (
|
||||
@@ -1251,61 +1255,63 @@ export function HostItem({
|
||||
)}
|
||||
|
||||
{/* Action tray — slides open on hover (default) or via chevron in click-tray mode */}
|
||||
<div className={trayVisibilityClass}>
|
||||
{tokens.showResourceRow &&
|
||||
isOnline &&
|
||||
((host.cpu != null && host.cpu > 0) ||
|
||||
(host.ram != null && host.ram > 0)) && (
|
||||
<div className="flex items-center gap-[10.5px] pt-[5.25px]">
|
||||
{host.cpu != null && host.cpu > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Cpu className="size-2.5 shrink-0 text-muted-foreground/40" />
|
||||
<div className="w-9 h-1 bg-muted-foreground/15 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`motion-meter h-full rounded-full ${host.cpu > 80 ? "bg-red-400" : host.cpu > 50 ? "bg-yellow-400" : "bg-accent-brand"}`}
|
||||
style={{ width: `${host.cpu}%` }}
|
||||
/>
|
||||
{!isCompact && (
|
||||
<div className={trayVisibilityClass}>
|
||||
{tokens.showResourceRow &&
|
||||
isOnline &&
|
||||
((host.cpu != null && host.cpu > 0) ||
|
||||
(host.ram != null && host.ram > 0)) && (
|
||||
<div className="flex items-center gap-[10.5px] pt-[5.25px]">
|
||||
{host.cpu != null && host.cpu > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Cpu className="size-2.5 shrink-0 text-muted-foreground/40" />
|
||||
<div className="w-9 h-1 bg-muted-foreground/15 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`motion-meter h-full rounded-full ${host.cpu > 80 ? "bg-red-400" : host.cpu > 50 ? "bg-yellow-400" : "bg-accent-brand"}`}
|
||||
style={{ width: `${host.cpu}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[9px] tabular-nums text-muted-foreground/50">
|
||||
{host.cpu}%
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[9px] tabular-nums text-muted-foreground/50">
|
||||
{host.cpu}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{host.ram != null && host.ram > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<MemoryStick className="size-2.5 shrink-0 text-muted-foreground/40" />
|
||||
<div className="w-9 h-1 bg-muted-foreground/15 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`motion-meter h-full rounded-full ${host.ram > 80 ? "bg-red-400" : host.ram > 60 ? "bg-yellow-400" : "bg-accent-brand/60"}`}
|
||||
style={{ width: `${host.ram}%` }}
|
||||
/>
|
||||
)}
|
||||
{host.ram != null && host.ram > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<MemoryStick className="size-2.5 shrink-0 text-muted-foreground/40" />
|
||||
<div className="w-9 h-1 bg-muted-foreground/15 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`motion-meter h-full rounded-full ${host.ram > 80 ? "bg-red-400" : host.ram > 60 ? "bg-yellow-400" : "bg-accent-brand/60"}`}
|
||||
style={{ width: `${host.ram}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[9px] tabular-nums text-muted-foreground/50">
|
||||
{host.ram}%
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[9px] tabular-nums text-muted-foreground/50">
|
||||
{host.ram}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`flex flex-col gap-0.5 ${alwaysShowTray || actionsOnly || shouldUseClickTray ? "" : "pt-1.5"}`}
|
||||
>
|
||||
{/* Connection buttons — only shown here when not already shown above */}
|
||||
{!alwaysShowTray && !actionsOnly && !shouldUseClickTray && (
|
||||
<div className="flex items-center flex-wrap gap-[1.75px]">
|
||||
{connectionButtons}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Management buttons row */}
|
||||
<div
|
||||
className={`flex items-center gap-[1.75px] border-t border-border/30 ${alwaysShowTray || actionsOnly || shouldUseClickTray ? "pt-[5.25px]" : "pt-[3.5px] mt-[1.75px]"}`}
|
||||
className={`flex flex-col gap-0.5 ${alwaysShowTray || actionsOnly || shouldUseClickTray ? "" : "pt-1.5"}`}
|
||||
>
|
||||
{managementButtons}
|
||||
{/* Connection buttons, only shown here when not already shown above */}
|
||||
{!alwaysShowTray && !actionsOnly && !shouldUseClickTray && (
|
||||
<div className="flex items-center flex-wrap gap-[1.75px]">
|
||||
{connectionButtons}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Management buttons row */}
|
||||
<div
|
||||
className={`flex items-center gap-[1.75px] border-t border-border/30 ${alwaysShowTray || actionsOnly || shouldUseClickTray ? "pt-[5.25px]" : "pt-[3.5px] mt-[1.75px]"}`}
|
||||
>
|
||||
{managementButtons}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{authOverrideProtocol && (
|
||||
<HostAuthOverrideModal
|
||||
open
|
||||
|
||||
@@ -502,7 +502,7 @@ export function SidebarTree({
|
||||
|
||||
function handleDeleteFolder(folder: HostFolder) {
|
||||
const folderPath = folder.path ?? folder.name;
|
||||
const { total } = folderHostCount(folder);
|
||||
const total = folderHostCount(folder);
|
||||
setConfirmDialog({
|
||||
message: t("hosts.deleteFolderConfirm", {
|
||||
name: folder.name,
|
||||
@@ -754,6 +754,7 @@ export function SidebarTree({
|
||||
const row = visibleRows[index];
|
||||
if (!row) return FOLDER_ROW_HEIGHT;
|
||||
if (isFolder(row.item)) return FOLDER_ROW_HEIGHT;
|
||||
if (isCompactDensity) return 29;
|
||||
const tagExtra = showTags && row.item.tags?.length ? TAG_ROW_EXTRA : 0;
|
||||
// The resource bars only render for an online host that reported CPU/RAM.
|
||||
// Reserving their height unconditionally left a gap under every offline
|
||||
|
||||
@@ -49,7 +49,7 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// The server keys statuses by sync id; its own ids mean nothing here.
|
||||
remoteCoreApiMock.get.mockResolvedValue({
|
||||
data: { "sync-2": { status: "reachable" }, "sync-9": { status: "x" } },
|
||||
data: { "sync-2": { status: "offline" }, "sync-9": { status: "x" } },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("status check origin routing", () => {
|
||||
|
||||
await expect(getAllServerStatuses()).resolves.toEqual({
|
||||
1: { status: "online" },
|
||||
2: { status: "reachable" },
|
||||
2: { status: "offline" },
|
||||
});
|
||||
|
||||
expect(sshHostApiMock.get).toHaveBeenCalledWith("/status", {
|
||||
@@ -91,6 +91,19 @@ describe("status check origin routing", () => {
|
||||
__silentRetry: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips the tick when the host list cannot be read", async () => {
|
||||
sshHostApiMock.get.mockImplementation(async (path: string) => {
|
||||
if (path === "/db/host") throw new Error("down");
|
||||
return { data: {} };
|
||||
});
|
||||
|
||||
await expect(getAllServerStatuses()).rejects.toThrow("down");
|
||||
expect(sshHostApiMock.get).not.toHaveBeenCalledWith(
|
||||
"/status",
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("status check routes", () => {
|
||||
|
||||
@@ -71,6 +71,7 @@ vi.mock("sonner", () => ({
|
||||
}));
|
||||
|
||||
import { Auth } from "../../auth/Auth";
|
||||
import { startPreLoginPlugins } from "@/plugin-host/loader";
|
||||
import {
|
||||
registerLoginMethod,
|
||||
registerSecondFactor,
|
||||
@@ -270,6 +271,30 @@ describe("login methods on the login screen", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("recovers an unavailable second-factor UI without restarting the login", async () => {
|
||||
window.history.replaceState({}, "", "/?second_factor=1&second_factors=pin");
|
||||
render(<Auth onLogin={vi.fn()} />);
|
||||
expect(await screen.findByText("auth.secondFactorNoUI")).toBeTruthy();
|
||||
let dispose = () => {};
|
||||
vi.mocked(startPreLoginPlugins).mockImplementationOnce(async () => {
|
||||
dispose = registerSecondFactor({
|
||||
id: "pin",
|
||||
pluginId: "corp",
|
||||
titleKey: "corp.pin",
|
||||
component: () => <span>recovered challenge</span>,
|
||||
});
|
||||
});
|
||||
try {
|
||||
fireEvent.click(screen.getByText("auth.retrySecondFactorUI"));
|
||||
expect(await screen.findByText("recovered challenge")).toBeTruthy();
|
||||
expect(startPreLoginPlugins).toHaveBeenLastCalledWith({
|
||||
retryFailed: true,
|
||||
});
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("offers every registered factor when the server lists none", async () => {
|
||||
const dispose = registerSecondFactor({
|
||||
id: "pin",
|
||||
|
||||
@@ -66,8 +66,8 @@ describe("getStatusClasses", () => {
|
||||
expect(getStatusClasses(true, "accent", "dot")).toContain("accent-brand");
|
||||
});
|
||||
|
||||
it("draws reachable as a warning in both schemes", () => {
|
||||
expect(getStatusClasses("reachable", "accent", "dot")).toContain("amber");
|
||||
expect(getStatusClasses("reachable", "status", "dot")).toContain("amber");
|
||||
it("draws an unchecked host as loading", () => {
|
||||
expect(getStatusClasses("unknown", "accent", "dot")).toContain("pulse");
|
||||
expect(getStatusClasses("unknown", "status", "dot")).toContain("pulse");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { ServerStatusStore } from "../../lib/server-status-store";
|
||||
describe("ServerStatusStore", () => {
|
||||
it("notifies only listeners for hosts whose status value changed", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([1, 2, 3]));
|
||||
|
||||
const on1 = vi.fn();
|
||||
const on2 = vi.fn();
|
||||
@@ -43,7 +42,6 @@ describe("ServerStatusStore", () => {
|
||||
|
||||
it("does not notify when the status map is unchanged", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([1]));
|
||||
store.applyStatuses(
|
||||
new Map([[1, { status: "online", lastChecked: "t1" }]]),
|
||||
);
|
||||
@@ -57,47 +55,23 @@ describe("ServerStatusStore", () => {
|
||||
expect(store.getStatus(1)).toBe("online");
|
||||
});
|
||||
|
||||
it("returns offline for disabled hosts", () => {
|
||||
it("calls a host it has no result for unknown, never offline", () => {
|
||||
const store = new ServerStatusStore();
|
||||
expect(store.getStatus(5)).toBe("unknown");
|
||||
});
|
||||
|
||||
it("forgets everything on clear", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.applyStatuses(new Map([[5, { status: "online", lastChecked: "t" }]]));
|
||||
expect(store.getStatus(5)).toBe("offline");
|
||||
store.setEnabledHostIds(new Set([5]));
|
||||
expect(store.getStatus(5)).toBe("online");
|
||||
});
|
||||
|
||||
it("preserves reachable as distinct from authenticated online", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([7]));
|
||||
store.applyStatuses(
|
||||
new Map([[7, { status: "reachable", lastChecked: "t" }]]),
|
||||
);
|
||||
expect(store.getStatus(7)).toBe("reachable");
|
||||
});
|
||||
|
||||
it("notifies when a status reason changes without a status change", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([7]));
|
||||
store.applyStatuses(
|
||||
new Map([[7, { status: "reachable", lastChecked: "t1" }]]),
|
||||
);
|
||||
store.setInitialLoadComplete(true);
|
||||
const listener = vi.fn();
|
||||
store.subscribeHost(7, listener);
|
||||
store.subscribeHost(5, listener);
|
||||
|
||||
store.applyStatuses(
|
||||
new Map([
|
||||
[
|
||||
7,
|
||||
{
|
||||
status: "reachable",
|
||||
reason: "host_key_changed",
|
||||
lastChecked: "t2",
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
store.clear();
|
||||
|
||||
expect(store.getStatus(5)).toBe("unknown");
|
||||
expect(store.getInitialLoadComplete()).toBe(false);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(store.getHostSnapshot(7)).toBe("reachable:host_key_changed");
|
||||
});
|
||||
|
||||
it("emits meta listeners when initial load completes", () => {
|
||||
|
||||
@@ -232,6 +232,20 @@ describe("plugin loader", () => {
|
||||
expect(isPluginFrontendActive("alpha")).toBe(true);
|
||||
});
|
||||
|
||||
it("retries failed login bundles only when explicitly requested", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async () => new Response(JSON.stringify([summary("login")])),
|
||||
);
|
||||
await startPreLoginPlugins();
|
||||
expect(isPluginFrontendActive("login")).toBe(false);
|
||||
modules.login = railPlugin("login");
|
||||
await startPreLoginPlugins();
|
||||
expect(isPluginFrontendActive("login")).toBe(false);
|
||||
await startPreLoginPlugins({ retryFailed: true });
|
||||
expect(isPluginFrontendActive("login")).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a pre-login load started after sign-in", async () => {
|
||||
modules.login = railPlugin("login");
|
||||
modules.alpha = railPlugin("alpha");
|
||||
|
||||
@@ -23,8 +23,15 @@ vi.mock("@/api/plugins-api", async (importOriginal) => ({
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => {
|
||||
const t = (key: string, options?: { name?: string }) =>
|
||||
options?.name ? `${key}:${options.name}` : key;
|
||||
const t = (
|
||||
key: string,
|
||||
options?: { name?: string; defaultValue?: string },
|
||||
) =>
|
||||
key === "ai:plugin.name"
|
||||
? "AI 助手"
|
||||
: options?.name
|
||||
? `${key}:${options.name}`
|
||||
: (options?.defaultValue ?? key);
|
||||
return { useTranslation: () => ({ t }) };
|
||||
});
|
||||
|
||||
@@ -112,10 +119,22 @@ describe("FeatureSettingsSection", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("AI Assistant")).toBeTruthy();
|
||||
expect(screen.getByText("AI 助手")).toBeTruthy();
|
||||
expect(api.getPluginUserSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the manifest name for plugins without a translated title", () => {
|
||||
render(
|
||||
<FeatureSettingsSection
|
||||
plugin={plugin({ id: "third-party", name: "Custom Feature" })}
|
||||
scope="admin"
|
||||
open={false}
|
||||
onToggle={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Custom Feature")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("loads and saves the scope's values once open", async () => {
|
||||
api.getPluginUserSettings.mockResolvedValue({ enabled: false });
|
||||
api.updatePluginUserSettings.mockResolvedValue({ enabled: true });
|
||||
@@ -179,7 +198,7 @@ describe("FeatureSettingsSection", () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText("settings.featureUnavailable:AI Assistant"),
|
||||
await screen.findByText("settings.featureUnavailable:AI 助手"),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
|
||||
const getHostTags = vi.fn();
|
||||
const saveHostTags = vi.fn();
|
||||
|
||||
vi.mock("@/api/host-tags-api", () => ({
|
||||
getHostTags: () => getHostTags(),
|
||||
saveHostTags: (tags: string[]) => saveHostTags(tags),
|
||||
}));
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
vi.mock("sonner", () => ({ toast: { error: vi.fn() } }));
|
||||
|
||||
import { AdminHostTags } from "@/sidebar/AdminHostTags";
|
||||
|
||||
describe("AdminHostTags", () => {
|
||||
beforeEach(() => {
|
||||
getHostTags.mockReset().mockResolvedValue(["prod"]);
|
||||
saveHostTags.mockReset().mockImplementation(async (tags) => tags);
|
||||
});
|
||||
|
||||
it("adds a tag on space and saves the list", async () => {
|
||||
render(<AdminHostTags />);
|
||||
await screen.findByText("prod");
|
||||
const input = screen.getByLabelText("admin.hostTags");
|
||||
await waitFor(() =>
|
||||
expect((input as HTMLInputElement).disabled).toBe(false),
|
||||
);
|
||||
fireEvent.change(input, { target: { value: "web" } });
|
||||
fireEvent.keyDown(input, { key: " " });
|
||||
await screen.findByText("web");
|
||||
expect(saveHostTags).toHaveBeenCalledWith(["prod", "web"]);
|
||||
});
|
||||
|
||||
it("removes a tag and saves the list", async () => {
|
||||
render(<AdminHostTags />);
|
||||
await screen.findByText("prod");
|
||||
fireEvent.click(screen.getByLabelText("common.remove"));
|
||||
await waitFor(() => expect(screen.queryByText("prod")).toBeNull());
|
||||
expect(saveHostTags).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { AdminUserManagePanel } from "../../sidebar/AdminUserManagePanel";
|
||||
@@ -71,7 +71,20 @@ function renderPanel(
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
for (const fn of Object.values(api)) fn.mockClear();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Host, HostFolder } from "@/types/ui-types";
|
||||
import { withLiveHostStatus, withLiveStatus } from "@/sidebar/live-host-status";
|
||||
|
||||
const host = (id: number, extra: Partial<Host> = {}) =>
|
||||
({ id: String(id), name: `h${id}`, online: false, ...extra }) as Host;
|
||||
|
||||
const statuses: Record<number, "online" | "offline"> = {
|
||||
1: "online",
|
||||
2: "offline",
|
||||
};
|
||||
const getStatus = (id: number) => statuses[id] ?? "unknown";
|
||||
|
||||
describe("withLiveStatus", () => {
|
||||
it("copies live statuses into nested folders", () => {
|
||||
const tree: HostFolder = {
|
||||
name: "root",
|
||||
children: [host(1), { name: "f", children: [host(2), host(3)] }],
|
||||
};
|
||||
const live = withLiveStatus(tree, getStatus);
|
||||
const [first, folder] = live.children as [Host, HostFolder];
|
||||
const [second, third] = folder.children as Host[];
|
||||
expect(first).toMatchObject({ status: "online", online: true });
|
||||
expect(second).toMatchObject({ status: "offline", online: false });
|
||||
expect(third).toMatchObject({ status: "unknown", online: false });
|
||||
});
|
||||
|
||||
it("gives a host with status checks off no status", () => {
|
||||
const [live] = withLiveHostStatus(
|
||||
[host(1, { statusCheckEnabled: false })],
|
||||
getStatus,
|
||||
);
|
||||
expect(live.status).toBeUndefined();
|
||||
expect(live.online).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the same object when nothing changed", () => {
|
||||
const same = host(1, { status: "online", online: true });
|
||||
expect(withLiveHostStatus([same], getStatus)[0]).toBe(same);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from "@testing-library/react";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { TabStrip } from "@/sidebar/HostManagerTabs";
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("reveals clipped tabs and updates the scroll controls after scrolling and resizing", () => {
|
||||
let resize = () => {};
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
constructor(callback: () => void) {
|
||||
resize = callback;
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(200);
|
||||
const width = vi
|
||||
.spyOn(HTMLElement.prototype, "scrollWidth", "get")
|
||||
.mockReturnValue(600);
|
||||
const scrollBy = vi.fn();
|
||||
HTMLElement.prototype.scrollBy = scrollBy;
|
||||
const onTabChange = vi.fn();
|
||||
const view = render(
|
||||
<TabStrip
|
||||
tabs={[
|
||||
{ id: "general", label: "General", icon: null },
|
||||
{ id: "metrics", label: "Metrics", icon: null },
|
||||
]}
|
||||
activeTab="general"
|
||||
onTabChange={onTabChange}
|
||||
/>,
|
||||
);
|
||||
const left = screen.getByLabelText(
|
||||
"common.scrollTabsLeft",
|
||||
) as HTMLButtonElement;
|
||||
const right = screen.getByLabelText(
|
||||
"common.scrollTabsRight",
|
||||
) as HTMLButtonElement;
|
||||
expect(left.disabled).toBe(true);
|
||||
expect(right.disabled).toBe(false);
|
||||
fireEvent.click(right);
|
||||
expect(scrollBy).toHaveBeenCalledWith({ left: 150, behavior: "smooth" });
|
||||
const strip = view.container.querySelector(".overflow-x-auto") as HTMLElement;
|
||||
strip.scrollLeft = 400;
|
||||
fireEvent.scroll(strip);
|
||||
expect(left.disabled).toBe(false);
|
||||
expect(right.disabled).toBe(true);
|
||||
fireEvent.click(screen.getByText("Metrics"));
|
||||
expect(onTabChange).toHaveBeenCalledWith("metrics");
|
||||
strip.scrollLeft = 0;
|
||||
width.mockReturnValue(200);
|
||||
act(resize);
|
||||
expect(screen.queryByLabelText("common.scrollTabsRight")).toBeNull();
|
||||
});
|
||||
@@ -27,12 +27,11 @@ vi.mock("react-i18next", () => ({
|
||||
|
||||
vi.mock("@/lib/ServerStatusContext", () => ({
|
||||
useHostStatus: () => null,
|
||||
useHostStatusReason: () => null,
|
||||
useServerStatus: () => ({
|
||||
getStatus: () => "online",
|
||||
initialLoadComplete: true,
|
||||
}),
|
||||
useServerStatusMeta: () => ({ initialLoadComplete: true, isLoading: false }),
|
||||
useServerStatusMeta: () => ({ initialLoadComplete: true }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-status-color-scheme", () => ({
|
||||
|
||||
@@ -27,24 +27,24 @@ function makeHost(overrides: Partial<Host> = {}): Host {
|
||||
// not the key paths the translator falls back to when a resource is missing.
|
||||
const t = (key: string): string =>
|
||||
({
|
||||
"hosts.status.available": "Available",
|
||||
"hosts.status.reachable": "Reachable, not authenticated",
|
||||
"hosts.status.online": "Online",
|
||||
"hosts.status.checking": "Checking",
|
||||
"hosts.status.offline": "Offline",
|
||||
"hosts.status.monitoringDisabled": "Monitoring disabled",
|
||||
})[key] ?? key;
|
||||
|
||||
describe("buildStatusTooltip", () => {
|
||||
it("returns the translated 'Available' label for online status", () => {
|
||||
it("returns the translated 'Online' label for online status", () => {
|
||||
const host = makeHost();
|
||||
const tooltip = buildStatusTooltip(host, "online", t);
|
||||
expect(tooltip).toContain("Available");
|
||||
expect(tooltip).toContain("Online");
|
||||
expect(tooltip).not.toContain("hosts.status.");
|
||||
});
|
||||
|
||||
it("returns the translated 'Reachable, not authenticated' label for reachable status", () => {
|
||||
it("returns 'Checking' before the host has been checked", () => {
|
||||
const host = makeHost();
|
||||
const tooltip = buildStatusTooltip(host, "reachable", t);
|
||||
expect(tooltip).toContain("Reachable, not authenticated");
|
||||
const tooltip = buildStatusTooltip(host, "unknown", t);
|
||||
expect(tooltip).toContain("Checking");
|
||||
expect(tooltip).not.toContain("hosts.status.");
|
||||
});
|
||||
|
||||
@@ -78,13 +78,13 @@ describe("buildStatusTooltip", () => {
|
||||
dispose();
|
||||
expect(tooltip).toContain("SSH");
|
||||
expect(tooltip).toContain("Demo Desktop");
|
||||
expect(tooltip).toContain("Available");
|
||||
expect(tooltip).toContain("Online");
|
||||
});
|
||||
|
||||
it("returns just the status label when no protocols are enabled", () => {
|
||||
const host = makeHost({ enableSsh: false });
|
||||
const tooltip = buildStatusTooltip(host, "online", t);
|
||||
expect(tooltip).toBe("Available");
|
||||
expect(tooltip).toBe("Online");
|
||||
});
|
||||
|
||||
it("does not render key paths when a translator is supplied", () => {
|
||||
|
||||
Reference in new issue
Block a user