Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
41b63c1
feat(agent): introduce remote deployment agent
Lftobs Aug 13, 2026
cd7599f
feat(orchestrator): add server preparation and
Lftobs Aug 21, 2026
6b16da8
refactor(api): inject dependencies in tests
Lftobs Aug 21, 2026
89d93ad
Merge remote-tracking branch 'origin/feat/multi-servers' into agent-a…
Lftobs Aug 21, 2026
6acb64f
refactor(db): migrate from SQLite to PostgreSQL
Lftobs Aug 24, 2026
22df367
refactore(db): migrate to PostgreSQL
Lftobs Aug 24, 2026
5bcbd13
fix(orchestrator): use HTTP health check instead of TCP for failover
Lftobs Aug 24, 2026
e6800ba
docs: add agent Caddy routes lifecycle documentation
Lftobs Aug 24, 2026
513c94f
feat(api): add domain DNS/TLS status check endpoint
Lftobs Aug 24, 2026
9029b3d
feat(web): add live DNS/TLS status badges to domains tab
Lftobs Aug 24, 2026
b21fcdb
feat(api): re-render all ingress routes when ingress server changes
Lftobs Aug 24, 2026
37db5b1
refactor(test): migrate integration tests to
Lftobs Aug 24, 2026
340b740
fix(api): add docker_tcp server support and
Lftobs Aug 24, 2026
0195b17
refactor(api): standardize API responses and
Lftobs Aug 24, 2026
0228a7f
chore: improve stability and environment security
Lftobs Aug 25, 2026
eaa9e7d
chore(release): v0.3.0-rc.1
Lftobs Aug 25, 2026
23ef255
fix(auth): add missing PAM service config for dequel group auth
Lftobs Aug 26, 2026
ff812f0
fix(e2e): resolve remote server deployment, buildkit, caddy ingress, …
Lftobs Aug 27, 2026
a3cacb8
fix(rc): use next tag for RC image testing
Lftobs Aug 28, 2026
e27b1b8
fix(rc): resolve login, caddy reload, and missing db migration
Lftobs Aug 28, 2026
a8389a9
fix(migration): make ssh_key migration idempotent for existing DBs
Lftobs Aug 28, 2026
32f7f8d
fix: add timeout to Loki fetch, default LogsTab to runtime mode
Lftobs Aug 28, 2026
c7acdcc
chore: bump version to v0.3.0-rc.4
Lftobs Aug 28, 2026
c24095f
refactor: remove Docker Compose profiles, start all services by default
Lftobs Aug 29, 2026
dfd99ce
chore: bump version to v0.3.0-rc.5
Lftobs Aug 29, 2026
3bca144
feat(ui): add SSH private key input to server form
Lftobs Aug 29, 2026
0e21aa4
chore: bump version to v0.3.0-rc.6
Lftobs Aug 29, 2026
9b73101
security: encrypt SSH private keys at rest
Lftobs Aug 29, 2026
133882a
chore: bump version to v0.3.0-rc.7
Lftobs Aug 29, 2026
54b9eed
chore: ignore local verify-dequel agent skills
Lftobs Aug 29, 2026
2627324
fix(ingress): default local ingress, emit :80 worker routes, plain ca…
Lftobs Aug 29, 2026
960ab4e
feat(cli): rc/prerelease update targets with zero-downtime rolling re…
Lftobs Aug 29, 2026
d9ccd2c
fix(caddy): update domain and email configuration
Lftobs Aug 30, 2026
6e6b7aa
fix: skip empty string subdomain in compose service validation
Lftobs Aug 30, 2026
28af211
Merge branch 'bugs-fixes' of github.com:Lftobs/dequel into bugs-fixes
Lftobs Aug 30, 2026
905719f
feat(ui): make dequel dashboard 100% mobile responsive
Lftobs Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/agent/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ export const config = {
export const requireControlPlaneUrl = () => {
if (!config.controlPlaneUrl) throw new Error("DEQUEL_CONTROL_PLANE is required");
return config.controlPlaneUrl;
};
};
2 changes: 1 addition & 1 deletion apps/agent/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,4 +417,4 @@ const applyRoute = async (payload: RemoteRoutePayload, signal: AbortSignal): Pro
return { routeFile: payload.routeFile, status: payload.action === "add" ? "active" : "removed" };
};

export const validateRoutePayload = validateRoutePayloadImpl;
export const validateRoutePayload = validateRoutePayloadImpl;
2 changes: 1 addition & 1 deletion apps/agent/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ export const parseP2PResponse = (raw: unknown): P2PResponse | null => {
} catch {
return null;
}
};
};
2 changes: 1 addition & 1 deletion apps/agent/src/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ const parseMemToMb = (mem: string): number => {
case "KiB": case "KB": return val / 1024;
default: return val;
}
};
};
2 changes: 1 addition & 1 deletion apps/api/src/agents/job-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,4 @@ export const processAgentJobUpdate = async (serverId: string, update: Exclude<P2
export const nextJobBatch = async (serverId: string) => {
const job = await leaseNextAgentJob(serverId);
return { jobs: job ? [job] : [], cancelJobIds: await listCancelledJobIds(serverId) };
};
};
2 changes: 1 addition & 1 deletion apps/api/src/agents/stats-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ class AgentStatsCache {
}
}

export const agentStatsCache = new AgentStatsCache();
export const agentStatsCache = new AgentStatsCache();
2 changes: 1 addition & 1 deletion apps/api/src/executors/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@ export const agentExecutor: DeploymentExecutor = {
await updateDeploymentStatus(deployment.id, "failed", { failureReason: "Cancelled" });
await appendLog(deployment.id, "system", "Deployment cancelled by user");
},
};
};
2 changes: 1 addition & 1 deletion apps/api/src/executors/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,4 @@ export const sshExecutor: DeploymentExecutor = {
await updateDeploymentStatus(deployment.id, "failed", { failureReason: "Cancelled" });
await emitLog(deployment.id, "system", "Deployment cancelled by user (remote build may continue on the server)");
},
};
};
2 changes: 1 addition & 1 deletion apps/api/src/utils/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const validateComposeServiceMapping = (mapping: unknown): string | null =
if (typeof m.serviceName !== "string" || !SERVICE_NAME_RE.test(m.serviceName)) {
return "serviceName may only contain letters, numbers, underscores and hyphens";
}
if (m.subdomain !== undefined && m.subdomain !== null) {
if (m.subdomain !== undefined && m.subdomain !== null && m.subdomain !== "") {
if (typeof m.subdomain !== "string" || !SUBDOMAIN_RE.test(m.subdomain)) {
return "subdomain must be a valid hostname label (lowercase letters, numbers, hyphens)";
}
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/databases/DatabaseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function DatabaseCard({ database, project, onChanged, onDelete }: Databas
</Button>
</div>

<div className="grid grid-cols-2 gap-3 text-xs">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
<div className="rounded-lg border border-border bg-black/20 p-3">
<p className="text-zinc-500">Internal endpoint</p>
<p className="mt-1 break-all font-mono text-zinc-200">{database.internalHost}:{database.internalPort}</p>
Expand All @@ -92,20 +92,20 @@ export function DatabaseCard({ database, project, onChanged, onDelete }: Databas

<div className="rounded-lg border border-border bg-[#08080a] p-3">
<div className="flex items-center justify-between gap-3">
<p className="min-w-0 truncate font-mono text-[11px] text-zinc-400">
<p className="min-w-0 break-all font-mono text-[11px] text-zinc-400">
{credentials?.externalConnectionString ?? credentials?.internalConnectionString ?? "Credentials hidden"}
</p>
<div className="flex shrink-0 gap-1">
<button type="button" onClick={reveal} className="p-1 text-zinc-500 hover:text-zinc-200" aria-label={credentials ? "Hide credentials" : "Reveal credentials"}>
<button type="button" onClick={reveal} className="p-1.5 text-zinc-500 hover:text-zinc-200" aria-label={credentials ? "Hide credentials" : "Reveal credentials"}>
{credentials ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
{credentials && <button type="button" onClick={() => navigator.clipboard.writeText(credentials.externalConnectionString ?? credentials.internalConnectionString)} className="p-1 text-zinc-500 hover:text-zinc-200" aria-label="Copy connection string"><Copy className="h-4 w-4" /></button>}
{credentials && <button type="button" onClick={() => navigator.clipboard.writeText(credentials.externalConnectionString ?? credentials.internalConnectionString)} className="p-1.5 text-zinc-500 hover:text-zinc-200" aria-label="Copy connection string"><Copy className="h-4 w-4" /></button>}
</div>
</div>
{credentialsError && <p role="alert" className="mt-2 text-xs text-red-400">{credentialsError}</p>}
</div>

<div className="flex gap-2 border-t border-border pt-4">
<div className="flex flex-wrap gap-2 border-t border-border pt-4">
{database.status === "stopped" ? (
<Button size="sm" disabled={isBusy} onClick={() => lifecycle("start")}><Play className="mr-2 h-3.5 w-3.5" /> Start</Button>
) : database.status === "failed" ? (
Expand Down
62 changes: 32 additions & 30 deletions apps/web/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,39 @@ export function Header({
setSidebarOpen,
}: HeaderProps) {
return (
<header className="h-14 border-b border-[#1a1a1f] bg-[#0c0c0e] flex items-center px-4 sm:px-6 gap-2 text-xs text-zinc-500">
<button
onClick={() => setSidebarOpen(true)}
className="lg:hidden p-1.5 -ml-1 mr-1 rounded-md hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors"
aria-label="Open sidebar"
>
<Menu className="h-4.5 w-4.5" />
</button>
<header className="h-14 border-b border-[#1a1a1f] bg-[#0c0c0e] flex items-center justify-between px-3.5 sm:px-6 gap-2 text-xs text-zinc-500 sticky top-0 z-30">
<div className="flex items-center gap-1.5 sm:gap-2 min-w-0">
<button
onClick={() => setSidebarOpen(true)}
className="lg:hidden p-2 -ml-1 mr-1 rounded-lg hover:bg-zinc-800 text-zinc-400 hover:text-zinc-200 transition-colors"
aria-label="Open sidebar"
>
<Menu className="h-5 w-5" />
</button>

<Link to="/" className="hover:text-zinc-200 transition-colors">
Workspace
</Link>
<ChevronRight className="h-3 w-3 text-zinc-600" />
{currentProject ? (
<>
<Link
to="/project/$projectId"
params={{ projectId: currentProjectId! }}
search={{ tab: "deployments" }}
className="hover:text-zinc-200 transition-colors font-medium text-zinc-300"
>
{currentProject.name}
</Link>
<ChevronRight className="h-3 w-3 text-zinc-600" />
<span className="text-zinc-100 font-bold uppercase tracking-wider text-[10px] bg-zinc-800 px-1.5 py-0.5 rounded">
{new URLSearchParams(location.search).get("tab") || "deployments"}
</span>
</>
) : (
<span className="text-zinc-300 font-medium">Dashboard</span>
)}
<Link to="/" className="hover:text-zinc-200 transition-colors shrink-0">
Workspace
</Link>
<ChevronRight className="h-3 w-3 text-zinc-600 shrink-0" />
{currentProject ? (
<>
<Link
to="/project/$projectId"
params={{ projectId: currentProjectId! }}
search={{ tab: "deployments" }}
className="hover:text-zinc-200 transition-colors font-medium text-zinc-300 truncate max-w-[110px] sm:max-w-[200px]"
>
{currentProject.name}
</Link>
<ChevronRight className="h-3 w-3 text-zinc-600 shrink-0" />
<span className="text-zinc-100 font-bold uppercase tracking-wider text-[9px] sm:text-[10px] bg-zinc-800 px-1.5 py-0.5 rounded shrink-0">
{new URLSearchParams(location.search).get("tab") || "deployments"}
</span>
</>
) : (
<span className="text-zinc-300 font-medium truncate">Dashboard</span>
)}
</div>
</header>
);
}
20 changes: 10 additions & 10 deletions apps/web/src/components/project/alerts/AlertsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function AlertsTab({ projectId }: AlertsTabProps) {
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h2 className="text-lg font-semibold text-foreground">
Alert Rules
Expand All @@ -129,7 +129,7 @@ export function AlertsTab({ projectId }: AlertsTabProps) {
</div>
<Button
onClick={() => setIsOpen(true)}
className="bg-primary hover:bg-primary/90 text-primary-foreground font-semibold flex items-center gap-1.5 px-4 h-9 rounded-xl transition-all shadow-md"
className="bg-primary hover:bg-primary/90 text-primary-foreground font-semibold flex items-center justify-center gap-1.5 px-4 h-9 rounded-xl transition-all shadow-md w-full sm:w-auto"
>
<Plus className="h-4.5 w-4.5" /> Add Rule
</Button>
Expand All @@ -141,15 +141,15 @@ export function AlertsTab({ projectId }: AlertsTabProps) {
return (
<div
key={a.id}
className="flex items-center justify-between p-4 rounded-xl border border-border bg-card/40 hover:bg-card/80 transition-all gap-4 group"
className="flex items-start sm:items-center justify-between p-3.5 sm:p-4 rounded-xl border border-border bg-card/40 hover:bg-card/80 transition-all gap-3 group"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0 border border-primary/15">
<div className="flex items-start sm:items-center gap-3 sm:gap-4 min-w-0">
<div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0 border border-primary/15 mt-0.5 sm:mt-0">
<Icon className="h-5 w-5" />
</div>
<div>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-semibold text-sm capitalize text-foreground">
<span className="font-semibold text-sm capitalize text-foreground truncate">
{a.type.replace(
"_",
" ",
Expand All @@ -162,7 +162,7 @@ export function AlertsTab({ projectId }: AlertsTabProps) {
{a.channel}
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-1">
<p className="text-xs text-muted-foreground mt-1 leading-relaxed">
Triggers when{" "}
<span className="capitalize">
{a.type.replace(
Expand All @@ -178,7 +178,7 @@ export function AlertsTab({ projectId }: AlertsTabProps) {
</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={() =>
Expand Down Expand Up @@ -208,7 +208,7 @@ export function AlertsTab({ projectId }: AlertsTabProps) {
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-lg opacity-80 group-hover:opacity-100 transition-all"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-lg opacity-100 sm:opacity-80 sm:group-hover:opacity-100 transition-all"
onClick={() =>
setDeletingAlertId(a.id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export function BuildStrategySection({
{/* Docker Compose Service Gateway Mapping */}
{buildType === 'compose' && (
<div className="space-y-4 pt-4 border-t border-[#1a1a22]">
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h4 className="text-xs font-bold text-zinc-200">Compose Ingress Service Mapping</h4>
<p className="text-[11px] text-zinc-400 mt-0.5">
Expand All @@ -276,7 +276,7 @@ export function BuildStrategySection({
<Button
type="button"
onClick={addComposeServiceRow}
className="bg-[#1c1c24] hover:bg-[#262632] text-orange-400 text-xs border border-[#2e2e3c]"
className="bg-[#1c1c24] hover:bg-[#262632] text-orange-400 text-xs border border-[#2e2e3c] w-full sm:w-auto"
>
<Plus className="h-3.5 w-3.5 mr-1" /> Add Service
</Button>
Expand Down
Loading
Loading