feat: rebuild desktop sync as linked server accounts, fix packaged plugin bundles and UI scaling

This commit is contained in:
LukeGus committed 2026-09-26 00:41:25 -05:00
1 parent 7c02daccf4
commit 6cd9a2846d
242 files changed
+39127 -8558

No files matched your search

-107
View File
@@ -1,107 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Server } from "lucide-react";
import { isElectron } from "@/lib/electron";
import { Button } from "@/components/button.tsx";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/dialog.tsx";
type DesktopSettings = {
defaultConnectionOrigin: "local" | "remote";
migrationNoticeAcknowledged?: boolean;
};
// One-time notice for Electron installs that previously pointed the whole
// app at a remote Termix server (the pre-2.6.0 architecture). That install
// now runs a fully local, standalone backend by default -- the hosts,
// credentials, and snippets that lived on the old remote server won't show
// up here until the user explicitly turns on Remote Sync and reconnects to
// that same server. A fresh install never had a legacy serverUrl, so this
// never fires for anyone who didn't go through the old flow, and it only
// ever shows once per install (tracked in desktop-settings.json).
export function MigrationNoticeDialog({
onOpenRemoteSync,
}: {
onOpenRemoteSync: (serverUrl: string) => void;
}) {
const { t } = useTranslation();
const [legacyServerUrl, setLegacyServerUrl] = useState<string | null>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
if (!isElectron()) return;
let cancelled = false;
Promise.all([
window.electronAPI?.invoke?.("get-legacy-server-config") as Promise<{
serverUrl: string | null;
} | null>,
window.electronAPI?.invoke?.("get-desktop-settings") as Promise<
DesktopSettings | undefined
>,
])
.then(([legacyConfig, settings]) => {
if (cancelled) return;
const serverUrl = legacyConfig?.serverUrl || null;
if (serverUrl && !settings?.migrationNoticeAcknowledged) {
setLegacyServerUrl(serverUrl);
setOpen(true);
}
})
.catch(() => {});
return () => {
cancelled = true;
};
}, []);
const acknowledge = async () => {
setOpen(false);
const settings = ((await window.electronAPI?.invoke?.(
"get-desktop-settings",
)) as DesktopSettings | undefined) ?? { defaultConnectionOrigin: "local" };
await window.electronAPI?.invoke?.("save-desktop-settings", {
...settings,
migrationNoticeAcknowledged: true,
});
};
const handleSetUpSync = async () => {
const url = legacyServerUrl || "";
await acknowledge();
onOpenRemoteSync(url);
};
if (!legacyServerUrl) return null;
return (
<Dialog open={open} onOpenChange={(next) => !next && acknowledge()}>
<DialogContent className="bg-card border border-border max-w-lg">
<DialogHeader>
<div className="flex items-center gap-2">
<Server className="size-4 text-accent-brand" />
<DialogTitle>{t("migrationNotice.title")}</DialogTitle>
</div>
<DialogDescription className="text-sm text-foreground/90 pt-2 space-y-2">
<p>{t("migrationNotice.body1")}</p>
<p>{t("migrationNotice.body2", { url: legacyServerUrl })}</p>
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex items-center gap-2 sm:justify-between">
<Button variant="outline" size="sm" onClick={acknowledge}>
{t("migrationNotice.dismiss")}
</Button>
<Button size="sm" onClick={handleSetUpSync}>
{t("migrationNotice.setUpSync")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
-50
View File
@@ -1,50 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { AlertTriangle } from "lucide-react";
import { isElectron } from "@/lib/electron";
interface RemoteSyncStatus {
connected: boolean;
syncing: boolean;
lastSyncedAt: string | null;
lastError: string | null;
needsReauth: boolean;
}
// Non-blocking banner shown when a connected remote sync server needs
// re-authentication. Never gates or hides any other UI -- the local app
// keeps working fully regardless of remote sync state.
export function RemoteSyncBanner({ onReconnect }: { onReconnect: () => void }) {
const { t } = useTranslation();
const [status, setStatus] = useState<RemoteSyncStatus | null>(null);
useEffect(() => {
if (!isElectron()) return;
window.electronAPI
?.invoke?.("get-remote-sync-status")
.then((s) => setStatus((s as RemoteSyncStatus) ?? null))
.catch(() => {});
const unsubscribe = window.electronAPI?.onRemoteSyncStatusChanged?.(
(nextStatus: RemoteSyncStatus) => setStatus(nextStatus),
);
return () => unsubscribe?.();
}, []);
if (!status?.connected || !status.needsReauth) return null;
return (
<div className="flex items-center justify-between gap-3 px-4 py-2 bg-yellow-500/10 border-b border-yellow-500/30 text-xs">
<div className="flex items-center gap-2">
<AlertTriangle className="size-3.5 text-yellow-600 dark:text-yellow-400 shrink-0" />
<span>{t("remoteSync.bannerMessage")}</span>
</div>
<button
type="button"
onClick={onReconnect}
className="font-bold text-accent-brand hover:text-accent-brand/70 transition-colors shrink-0"
>
{t("remoteSync.bannerReconnect")}
</button>
</div>
);
}
+15 -7
View File
@@ -1,3 +1,4 @@
import { rem, remScale } from "@/lib/rem";
import {
useCallback,
useEffect,
@@ -212,7 +213,8 @@ function MasonryTile({
const el = innerRef.current;
if (!el) return;
const report = () => {
const h = fixedHeight ?? el.getBoundingClientRect().height;
// Row units are Normal-size pixels; a measured height is scaled back.
const h = fixedHeight ?? el.getBoundingClientRect().height / remScale();
onReportRows(heightToRowSpan(h, ROW_UNIT, ROW_GAP));
};
report();
@@ -247,7 +249,7 @@ function MasonryTile({
"min-h-0",
fixedHeight != null && "overflow-hidden [&>*]:h-full",
)}
style={fixedHeight != null ? { height: fixedHeight } : undefined}
style={fixedHeight != null ? { height: rem(fixedHeight) } : undefined}
>
{children}
</div>
@@ -358,10 +360,16 @@ export function CardGridCanvas({
e.preventDefault();
e.stopPropagation();
const startY = e.clientY;
// Heights are stored in Normal-size pixels, whatever the interface size.
const scale = remScale();
const startH =
tileRefs.current.get(id)?.getBoundingClientRect().height ?? 120;
(tileRefs.current.get(id)?.getBoundingClientRect().height ??
120 * scale) / scale;
const onMove = (ev: globalThis.MouseEvent) => {
onChange(setHeight(slots, id, startH + (ev.clientY - startY)), columns);
onChange(
setHeight(slots, id, startH + (ev.clientY - startY) / scale),
columns,
);
};
const onUp = () => {
window.removeEventListener("mousemove", onMove);
@@ -396,10 +404,10 @@ export function CardGridCanvas({
className="grid items-start"
style={{
gridTemplateColumns: `repeat(${effectiveColumns}, minmax(0, 1fr))`,
gridAutoRows: `${ROW_UNIT}px`,
gridAutoRows: rem(ROW_UNIT),
gridAutoFlow: "row dense",
columnGap: `${ROW_GAP}px`,
rowGap: `${ROW_GAP}px`,
columnGap: rem(ROW_GAP),
rowGap: rem(ROW_GAP),
}}
>
{sorted.map((slot) => {
+9 -2
View File
@@ -21,6 +21,8 @@ type Select2Props = Omit<
"value" | "defaultValue" | "onChange" | "required"
> & {
placeholder?: string;
align?: "start" | "center" | "end";
contentClassName?: string;
};
function getText(children: React.ReactNode): string {
@@ -85,6 +87,8 @@ function Select2({
onChange,
disabled,
placeholder,
align = "start",
contentClassName,
name,
id,
required,
@@ -171,8 +175,11 @@ function Select2({
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="z-[99999] w-[var(--radix-popover-trigger-width)] min-w-52 p-0"
align={align}
className={cn(
"z-[99999] w-[var(--radix-popover-trigger-width)] min-w-52 p-0",
contentClassName,
)}
>
<div className="flex items-center border-b border-border px-2">
<Search className="mr-2 size-4 shrink-0 text-muted-foreground" />
-124
View File
@@ -1,124 +0,0 @@
import React from "react";
import { Alert, AlertTitle, AlertDescription } from "@/components/alert.tsx";
import { Button } from "@/components/button.tsx";
import { ExternalLink, Download, AlertTriangle, Info } from "lucide-react";
import { useTranslation } from "react-i18next";
interface VersionAlertProps {
updateInfo: {
success: boolean;
status?: "up_to_date" | "requires_update" | "beta";
localVersion?: string;
remoteVersion?: string;
latest_release?: {
tag_name: string;
name: string;
published_at: string;
html_url: string;
body: string;
};
cached?: boolean;
cache_age?: number;
error?: string;
};
onDownload?: () => void;
}
export function VersionAlert({ updateInfo, onDownload }: VersionAlertProps) {
const { t } = useTranslation();
if (!updateInfo.success) {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t("versionCheck.error")}</AlertTitle>
<AlertDescription>
{updateInfo.error || t("versionCheck.checkFailed")}
</AlertDescription>
</Alert>
);
}
if (updateInfo.status === "up_to_date") {
return (
<Alert>
<Download className="h-4 w-4" />
<AlertTitle>{t("versionCheck.upToDate")}</AlertTitle>
<AlertDescription>
{t("versionCheck.currentVersion", {
version: updateInfo.localVersion,
})}
</AlertDescription>
</Alert>
);
}
if (updateInfo.status === "beta") {
return (
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>{t("versionCheck.betaVersion")}</AlertTitle>
<AlertDescription>
{t("versionCheck.betaVersionDesc", {
current: updateInfo.localVersion,
latest: updateInfo.remoteVersion,
})}
</AlertDescription>
</Alert>
);
}
if (updateInfo.status === "requires_update") {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t("versionCheck.updateAvailable")}</AlertTitle>
<AlertDescription className="space-y-3">
<div>
{t("versionCheck.newVersionAvailable", {
current: updateInfo.localVersion,
latest: updateInfo.remoteVersion,
})}
</div>
{updateInfo.latest_release && (
<div className="text-sm text-muted-foreground">
<div className="font-medium">
{updateInfo.latest_release.name}
</div>
<div className="text-xs">
{t("versionCheck.releasedOn", {
date: new Date(
updateInfo.latest_release.published_at,
).toLocaleDateString(),
})}
</div>
</div>
)}
<div className="flex gap-2 pt-2">
{updateInfo.latest_release?.html_url && (
<Button
variant="outline"
size="sm"
onClick={() => {
if (onDownload) {
onDownload();
} else {
window.open(updateInfo.latest_release!.html_url, "_blank");
}
}}
className="flex items-center gap-1"
>
<ExternalLink className="h-3 w-3" />
{t("versionCheck.downloadUpdate")}
</Button>
)}
</div>
</AlertDescription>
</Alert>
);
}
return null;
}