Add global recent-deployments feed for the projects dashboard

The RECENT DEPLOYMENTS section queried the builds table, so
image-sourced services — which deploy without building — never
appeared and the dashboard reported "no deployments yet" while
containers were running.

- GET /api/v1/deployments lists latest deployments across the
  caller's services, joined with service + project names, ordered
  newest-first with a bounded limit
- ProjectsPage consumes it via listRecentDeployments; the DEPLOYS
  stat card now counts real deployments
- is_admin is now projected in register/upsert user responses, and
  the Better Auth mirror grants admin to the first mirrored account
  (previously only the legacy register path did)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 21:07:01 +02:00
1 parent b7e2e93261
commit 3bf44dbcd5
8 files changed
+258 -21

No files matched your search

@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
createProject,
getHostMonitoring,
listBuilds,
listRecentDeployments,
listProjects,
type ProjectEntity,
type ProjectStats,
@@ -156,10 +156,10 @@ export function ProjectsPage() {
queryFn: getHostMonitoring,
refetchInterval: 30_000,
});
const buildsQuery = useQuery({
const deploysQuery = useQuery({
queryKey: ['recent-deploys'],
enabled: !isDemoMode,
queryFn: () => listBuilds({ page: 1, limit: 5 }),
queryFn: () => listRecentDeployments(10),
refetchInterval: 30_000,
});
@@ -193,16 +193,16 @@ export function ProjectsPage() {
const memPct = isDemoMode ? 72 : host ? Math.round(host.memory.usagePercent) : null;
const diskPct = isDemoMode ? 73 : host ? Math.round(host.storage.usagePercent) : null;
const fmtGB = (v: number) => `${(v / (1024 * 1024 * 1024)).toFixed(0)}G`;
const builds = isDemoMode ? [] : buildsQuery.data?.builds ?? [];
const deployCount = buildsQuery.data?.total ?? builds.length;
const deployments = isDemoMode ? [] : deploysQuery.data ?? [];
const deployCount = deployments.length;
const feed = isDemoMode
? demoDeploys
: builds.map((b) => ({
id: b.id,
name: b.imageName || b.serviceId || b.id,
project: '',
status: (b.status || 'queued').toUpperCase(),
when: formatRelative(b.startedAt ?? b.completedAt),
: deployments.slice(0, 5).map((d) => ({
id: d.id,
name: d.serviceName || d.imageName || d.serviceId || d.id,
project: d.projectName,
status: (d.status || 'queued').toUpperCase(),
when: formatRelative(d.startedAt ?? d.completedAt ?? d.createdAt),
}));
const statusClass = (status: string) =>
@@ -278,7 +278,7 @@ export function ProjectsPage() {
<div className="v-k"><span>DEPLOYS</span></div>
<div className="v-v">{isDemoMode ? 18 : deployCount}</div>
<div className="v-meter"><i style={{ width: '94%' }} /></div>
<div className="v-d"><b style={{ color: 'var(--success)' }}>{isDemoMode ? '17 ok' : `${builds.filter((b) => (b.status ?? '').toLowerCase() !== 'failed').length} recent`}</b></div>
<div className="v-d"><b style={{ color: 'var(--success)' }}>{isDemoMode ? '17 ok' : `${deployments.filter((d) => (d.status ?? '').toLowerCase() !== 'failed').length} recent`}</b></div>
</div>
</div>
+53
View File
@@ -886,6 +886,59 @@ export interface paths {
patch?: never;
trace?: never;
};
"/deployments": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* List recent deployments
* @description Latest deployments across all services owned by the caller — feeds the dashboard deploy feed
*/
get: {
parameters: {
query?: {
limit?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Recent deployments */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
deployments?: {
id?: string;
service_id?: string;
service_name?: string;
project_name?: string;
status?: string;
image_name?: string;
started_at?: string | null;
completed_at?: string | null;
created_at?: string;
}[];
};
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/deployments/{id}": {
parameters: {
query?: never;
+31
View File
@@ -1146,6 +1146,37 @@ export async function listDeployments(serviceId: string): Promise<DeploymentEnti
return normalizeDeploymentArray(payload.deployments);
}
export type RecentDeploymentEntity = {
id: string;
serviceId: string;
serviceName: string;
projectName: string;
status: string;
imageName?: string;
startedAt?: string;
completedAt?: string;
createdAt?: string;
};
export async function listRecentDeployments(limit = 10): Promise<RecentDeploymentEntity[]> {
const payload = await requestJson<{ deployments?: Array<Record<string, unknown>> }>(
`/deployments?limit=${limit}`,
);
return (payload.deployments ?? [])
.map((d) => ({
id: String(d.id ?? ''),
serviceId: String(d.service_id ?? ''),
serviceName: String(d.service_name ?? ''),
projectName: String(d.project_name ?? ''),
status: String(d.status ?? ''),
imageName: d.image_name ? String(d.image_name) : undefined,
startedAt: d.started_at ? String(d.started_at) : undefined,
completedAt: d.completed_at ? String(d.completed_at) : undefined,
createdAt: d.created_at ? String(d.created_at) : undefined,
}))
.filter((d) => d.id !== '');
}
export async function createDeployment(
serviceId: string,
input: CreateDeploymentInput = {},