Improve logging recovery, resource scaling, and app deploy logging.

Auto-reconnect Elasticsearch port-forward after cluster or API restarts, poll log status in the UI, and apply storage changes through billing upgrade for all workloads. Add Redis/RabbitMQ PVC resize, Helm ES credentials for Fluent Bit, and fix deploy progress overlay behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-25 21:41:00 +03:30
parent bf9e827f85
commit dc9830383b
18 changed files with 1068 additions and 414 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ export function DeletingTableRowCell({
message?: string;
}) {
return (
<td colSpan={colSpan} className="px-6 py-4 bg-white/95" aria-live="polite" aria-busy="true">
<td colSpan={colSpan} className="px-6 py-4 bg-white/80 backdrop-blur-sm" aria-live="polite" aria-busy="true">
<div className="flex min-h-[52px] w-full items-center justify-center gap-2 text-sm font-semibold text-gray-800">
<Clock className="w-4 h-4 shrink-0 animate-spin text-red-600" />
<span>{message}</span>
@@ -34,7 +34,7 @@ export function DeletingTableRowCell({
export function DeletingCardOverlay({ message = 'Deleting…' }: { message?: string }) {
return (
<div
className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-xl bg-white/92 backdrop-blur-[2px] text-sm font-semibold text-gray-800"
className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-xl bg-white/75 backdrop-blur-sm text-sm font-semibold text-gray-800"
aria-live="polite"
aria-busy="true"
>
@@ -2,7 +2,7 @@
import { useEffect, useMemo } from 'react';
import { usePathname } from 'next/navigation';
import { useQuery, useQueries } from '@tanstack/react-query';
import { useQuery, useQueries, keepPreviousData } from '@tanstack/react-query';
import api from '@/lib/api';
import type { Application } from '@/types';
import { useAuthStore } from '@/lib/store';
@@ -49,16 +49,15 @@ export function DeploymentProgressManager() {
.get<{ progress: BuildProgress | null }>(`/deployments/applications/${app.id}/build-progress`)
.then((r) => r.data.progress),
refetchInterval: 1500,
placeholderData: keepPreviousData,
})),
});
const activeItems = useMemo(() => {
return deployingApps
.map((app, i) => ({
app,
progress: progressQueries[i]?.data ?? null,
}))
.filter(({ progress }) => isActiveBuildProgress(progress));
return deployingApps.map((app, i) => ({
app,
progress: progressQueries[i]?.data ?? null,
}));
}, [deployingApps, progressQueries]);
const routeAppId =
@@ -66,10 +65,10 @@ export function DeploymentProgressManager() {
pathname.match(/\/dashboard\/services\/([^/]+)/)?.[1];
useEffect(() => {
if (activeItems.length === 0) {
if (deployingApps.length === 0) {
useDeployProgressStore.setState({ minimized: false, focusedAppId: null });
}
}, [activeItems.length]);
}, [deployingApps.length]);
useEffect(() => {
if (activeItems.length === 0) return;
@@ -84,12 +83,15 @@ export function DeploymentProgressManager() {
const focusedItem =
activeItems.find((item) => item.app.id === focusedAppId) ?? activeItems[0];
const focusedProgress = focusedItem?.progress;
const focusedProgress =
focusedItem?.progress ??
({ phase: 'building', percent: 0, message: 'Loading progress…' } satisfies BuildProgress);
const showModal =
!minimized &&
!!focusedItem &&
!!focusedProgress &&
isActiveBuildProgress(focusedProgress);
(!focusedItem.progress ||
isActiveBuildProgress(focusedItem.progress) ||
focusedItem.progress.phase === 'done');
const handleMinimize = () => {
if (focusedItem) minimize(focusedItem.app.id);
@@ -99,7 +101,7 @@ export function DeploymentProgressManager() {
expand(appId);
};
if (activeItems.length === 0) return null;
if (deployingApps.length === 0) return null;
return (
<>
@@ -109,7 +111,7 @@ export function DeploymentProgressManager() {
</div>
)}
{showModal && focusedProgress && (
{showModal && (
<BuildProgressModal
appId={focusedItem.app.id}
appName={focusedItem.app.name}
@@ -226,23 +226,6 @@ export function ManagedServiceResourcesPanel({
},
});
const resizeDbMutation = useMutation({
mutationFn: (size: string) => api.patch(`/applications/${serviceId}/db-storage`, { size }),
onSuccess: (res) => {
if (res.data.success) {
toast.success(res.data.message || 'Storage expanded');
queryClient.invalidateQueries({ queryKey: ['db-storage', serviceId] });
queryClient.invalidateQueries({ queryKey: ['storage-usage', serviceId] });
} else {
toast.error(res.data.message || 'Failed to expand storage');
}
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to resize storage');
},
});
const dbUploadMutation = useMutation({
mutationFn: (file: File) => {
const formData = new FormData();
@@ -280,9 +263,15 @@ export function ManagedServiceResourcesPanel({
[dbUploadMutation],
);
const currentDbGi =
parseInt((dbStorageData?.currentSize || app.dbStorageSize || '1Gi').replace('Gi', ''), 10) || 1;
const buildUpgradePayload = useCallback((): UpgradePayload => {
if (app.productType === 'managed_database') {
return { ...dbResources };
const payload: UpgradePayload = { ...dbResources };
const newGi = parseInt(dbStorageSize, 10);
if (newGi > currentDbGi) payload.dbStorageSize = `${newGi}Gi`;
return payload;
}
if (app.productType === 'managed_redis') {
return {
@@ -304,7 +293,7 @@ export function ManagedServiceResourcesPanel({
storageGi: rabbitResources.storageGi ?? app.optionalServiceResources?.rabbitmq?.storageGi ?? 2,
},
};
}, [app, dbResources, redisResources, rabbitResources]);
}, [app, dbResources, redisResources, rabbitResources, dbStorageSize, currentDbGi]);
const applyResources = () => {
if (needsRenewal) {
@@ -316,6 +305,10 @@ export function ManagedServiceResourcesPanel({
const w = workloadKey(app);
if (!app.billingCycle) {
if (Object.keys(payload).length > 0 && (payload.dbStorageSize || payload.redisResources || payload.rabbitmqResources)) {
scaleMutation.mutate(payload);
return;
}
if (w === 'database') {
directPatchResourcesMutation.mutate({ workload: 'database', ...dbResources });
} else if (w === 'redis') {
@@ -351,37 +344,12 @@ export function ManagedServiceResourcesPanel({
}
};
const handleExpandStorage = () => {
if (needsRenewal) {
toast.warn('Renew the service before expanding storage');
return;
}
const newGi = parseInt(dbStorageSize, 10);
if (newGi <= currentDbGi) {
toast.warn('New size must be larger than current allocation');
return;
}
const newSize = `${newGi}Gi`;
const payload: UpgradePayload = { dbStorageSize: newSize };
if (!app.billingCycle) {
resizeDbMutation.mutate(newSize);
return;
}
setPendingUpgradePayload(payload);
calculateUpgradeCostMutation.mutate(payload);
};
const resourcesPending =
directPatchResourcesMutation.isPending ||
scaleMutation.isPending ||
calculateUpgradeCostMutation.isPending ||
createUpgradeInvoiceMutation.isPending;
const currentDbGi =
parseInt((dbStorageData?.currentSize || app.dbStorageSize || '1Gi').replace('Gi', ''), 10) || 1;
const metricsWorkload =
resourceUsage?.workloads?.find((w) => w.key === workloadKey(app)) ||
(resourceUsage?.configured
@@ -558,22 +526,8 @@ export function ManagedServiceResourcesPanel({
</button>
</div>
<span className="text-sm text-gray-600">GB</span>
<button
type="button"
className="btn-primary text-xs px-3 py-1.5 disabled:opacity-50"
disabled={
resourcesPending ||
!isDeployed ||
parseInt(dbStorageSize, 10) <= currentDbGi
}
onClick={handleExpandStorage}
>
{resizeDbMutation.isPending || calculateUpgradeCostMutation.isPending
? 'Expanding…'
: 'Expand'}
</button>
<p className="text-xs text-gray-400 w-full">
Only expansion is allowed.
Only expansion is allowed. Use Apply changes below.
{app.billingCycle
? ' Additional storage is charged for the remaining billing period.'
: ''}