Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e",
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7",
"@appwrite.io/pink-legacy": "^1.0.3",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions src/lib/helpers/buildTimeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Models } from '@appwrite.io/console';

/**
* Checks if a build has exceeded the maximum build timeout duration
*/
function isBuildTimedOut(createdAt: string, status: string, timeoutSeconds: number): boolean {
if (!['waiting', 'processing', 'building'].includes(status)) {
return false;
}

if (!timeoutSeconds || timeoutSeconds <= 0) {
return false;
}

const created = new Date(createdAt);
const elapsedSeconds = Math.floor((Date.now() - created.getTime()) / 1000);

return elapsedSeconds > timeoutSeconds;
}

/**
* Gets the effective status for a build, considering timeout
*/
export function getEffectiveBuildStatus(
originalStatus: string,
createdAt: string,
consoleVariables: Models.ConsoleVariables | undefined
): string {
const timeoutSeconds = getBuildTimeoutSeconds(consoleVariables);
if (isBuildTimedOut(createdAt, originalStatus, timeoutSeconds)) {
return 'failed';
}
return originalStatus;
}

/**
* Helper to get timeout value from console variables
*/
function getBuildTimeoutSeconds(consoleVariables: Models.ConsoleVariables | undefined): number {
if (!consoleVariables?._APP_COMPUTE_BUILD_TIMEOUT) {
return 0;
}
const timeout = parseInt(String(consoleVariables._APP_COMPUTE_BUILD_TIMEOUT), 10);
return isNaN(timeout) ? 0 : timeout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import { DeploymentSource, DeploymentCreatedBy, DeploymentDomains } from '$lib/components/git';
import { func } from '../store';
import { capitalize } from '$lib/helpers/string';
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { isCloud } from '$lib/system';
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
import Link from '$lib/elements/link.svelte';
Expand All @@ -36,6 +38,9 @@
footer?: Snippet;
} = $props();

let effectiveStatus = $derived(
getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables)
);
let totalSize = $derived(humanFileSize(deployment?.totalSize ?? 0));
</script>

Expand Down Expand Up @@ -122,11 +127,11 @@
</Layout.Stack>

<Layout.Stack direction="row" gap="xl">
{#if deployment.status === 'failed'}
{#if effectiveStatus === 'failed'}
<Layout.Stack gap="xxs" inline>
{@render titleSnippet('Status')}
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
<Status status={deployment.status} label={deployment.status} />
<Status status={effectiveStatus} label={effectiveStatus} />
</Typography.Text>
</Layout.Stack>
{:else}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
} from '@appwrite.io/pink-svelte';
import { capitalize } from '$lib/helpers/string';
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { timer } from '$lib/actions/timer';
import { app } from '$lib/stores/app';
import { IconDotsHorizontal, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte';
Expand All @@ -36,22 +38,29 @@
import { readOnly } from '$lib/stores/billing';
import RedeployModal from '../(modals)/redeployModal.svelte';

export let data;
let { data } = $props();

let showDelete = false;
let showCancel = false;
let showActivate = false;
let showRedeploy = false;
let effectiveStatus = $derived(
getEffectiveBuildStatus(
data.deployment.status,
data.deployment.$createdAt,
$regionalConsoleVariables
)
);
let showDelete = $state(false);
let showCancel = $state(false);
let showActivate = $state(false);
let showRedeploy = $state(false);

onMount(() => {
return realtime.forProject(page.params.region, 'console', (response) => {
return realtime.forConsole(page.params.region, 'console', (message) => {
if (
response.events.includes(
message.events.includes(
`functions.${page.params.function}.deployments.${page.params.deployment}.update`
)
) {
const payload = response.payload as Models.Deployment;
if (payload.status === 'ready') {
const payload = message.payload as Models.Deployment;
if (['ready', 'failed'].includes(payload.status)) {
invalidate(Dependencies.DEPLOYMENT);
}
}
Expand All @@ -78,7 +87,7 @@
<DeploymentCard proxyRuleList={data.proxyRuleList} deployment={data.deployment}>
{#snippet footer()}
<Layout.Stack direction="row" alignItems="center" inline>
{#if data.deployment.status === 'processing' || data.deployment.status === 'building' || data.deployment.status === 'waiting'}
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
<Button
text
on:click={() => {
Expand Down Expand Up @@ -152,9 +161,9 @@
<Card.Base padding="s">
<Accordion
title="Deployment logs"
badge={capitalize(data.deployment.status)}
badge={capitalize(effectiveStatus)}
open
badgeType={badgeTypeDeployment(data.deployment.status)}
badgeType={badgeTypeDeployment(effectiveStatus)}
hideDivider>
<Layout.Stack gap="xl">
{#key data.deployment.buildLogs}
Expand All @@ -167,7 +176,7 @@

<svelte:fragment slot="end">
<Layout.Stack direction="row" alignItems="center" inline>
{#if ['processing', 'building'].includes(data.deployment.status)}
{#if ['processing', 'building'].includes(effectiveStatus)}
<Typography.Code color="--fgcolor-neutral-secondary">
<Layout.Stack direction="row" alignItems="center" inline>
<p use:timer={{ start: data.deployment.$createdAt }}></p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import Delete from './(modals)/deleteModal.svelte';
import { capitalize } from '$lib/helpers/string';
import { deploymentStatusConverter } from '$lib/stores/git';
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import DownloadActionMenuItem from './(components)/downloadActionMenuItem.svelte';
import { Menu } from '$lib/components/menu';
import { sdk } from '$lib/stores/sdk';
Expand Down Expand Up @@ -82,9 +84,13 @@
{/each}
<Table.Header.Cell column="actions" {root} />
{/snippet}

{#snippet children(root)}
{#each data.deploymentList.deployments as deployment (deployment.$id)}
{@const effectiveStatus = getEffectiveBuildStatus(
deployment.status,
deployment.$createdAt,
$regionalConsoleVariables
)}
<Table.Row.Link
{root}
id={deployment.$id}
Expand All @@ -96,23 +102,21 @@
<Id value={deployment.$id}>{deployment.$id}</Id>
{/key}
{:else if column.id === 'status'}
{@const status = deployment.status}

{#if data?.activeDeployment?.$id === deployment?.$id}
<Status status="complete" label="Active" />
{:else}
<Status
status={deploymentStatusConverter(status)}
label={capitalize(status)} />
status={deploymentStatusConverter(effectiveStatus)}
label={capitalize(effectiveStatus)} />
{/if}
{:else if column.id === 'type'}
<DeploymentSource {deployment} />
{:else if column.id === '$updatedAt'}
<DeploymentCreatedBy {deployment} />
{:else if column.id === 'buildDuration'}
{#if ['waiting'].includes(deployment.status)}
{#if ['waiting'].includes(effectiveStatus)}
-
{:else if ['processing', 'building'].includes(deployment.status)}
{:else if ['processing', 'building'].includes(effectiveStatus)}
<span use:timer={{ start: deployment.$createdAt }}></span>
{:else}
{formatTimeDetailed(deployment.buildDuration)}
Expand Down Expand Up @@ -167,7 +171,7 @@

<DownloadActionMenuItem {deployment} {toggle} />

{#if deployment.status === 'processing' || deployment.status === 'building' || deployment.status === 'waiting'}
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
<ActionMenu.Item.Button
trailingIcon={IconXCircle}
on:click={() => {
Expand All @@ -180,7 +184,7 @@
Cancel
</ActionMenu.Item.Button>
{/if}
{#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment.status !== 'waiting'}
{#if effectiveStatus !== 'building' && effectiveStatus !== 'processing' && effectiveStatus !== 'waiting'}
<ActionMenu.Item.Button
leadingIcon={IconTrash}
status="danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
IconXCircle
} from '@appwrite.io/pink-icons-svelte';
import { ActionMenu, Icon, Tooltip } from '@appwrite.io/pink-svelte';
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';

export let selectedDeployment: Models.Deployment;
export let deployment: Models.Deployment;
Expand Down Expand Up @@ -51,6 +53,11 @@
</Button>
<svelte:fragment slot="menu" let:toggle>
<ActionMenu.Root>
{@const effectiveStatus = getEffectiveBuildStatus(
deployment.status,
deployment.$createdAt,
$regionalConsoleVariables
)}
Comment on lines +56 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Menu never updates when the timeout is crossed

effectiveStatus is calculated once when this template renders. Because no reactive clock or prop changes fire afterward, a deployment that should time out continues to look “building,” so “Cancel” stays visible and “Delete” stays hidden indefinitely. Please drive this through a time-reactive status (e.g., pass one down from the table/card that ticks) or introduce a local ticking signal so getEffectiveBuildStatus actually re-runs as time advances.

{#if !inCard}
<Tooltip disabled={selectedDeployment?.sourceSize !== 0} placement={'bottom'}>
<div>
Expand All @@ -70,7 +77,7 @@
<div slot="tooltip">Source is empty</div>
</Tooltip>
{/if}
{#if deployment?.status === 'ready' && deployment?.$id !== activeDeployment}
{#if effectiveStatus === 'ready' && deployment?.$id !== activeDeployment}
<ActionMenu.Item.Button
leadingIcon={IconLightningBolt}
on:click={(e) => {
Expand All @@ -82,7 +89,7 @@
Activate
</ActionMenu.Item.Button>
{/if}
{#if deployment?.status === 'ready' || deployment?.status === 'failed' || deployment?.status === 'building'}
{#if effectiveStatus === 'ready' || effectiveStatus === 'failed' || effectiveStatus === 'building'}
<SubMenu>
<ActionMenu.Root noPadding>
<ActionMenu.Item.Button
Expand All @@ -101,7 +108,7 @@
</ActionMenu.Item.Anchor>

<ActionMenu.Item.Anchor
disabled={deployment?.status !== 'ready'}
disabled={effectiveStatus !== 'ready'}
on:click={toggle}
href={getOutputDownload(deployment.$id)}
external>
Expand All @@ -112,7 +119,7 @@
</SubMenu>
{/if}

{#if deployment?.status === 'processing' || deployment?.status === 'building' || deployment.status === 'waiting'}
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
<ActionMenu.Item.Button
leadingIcon={IconXCircle}
status="danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<script lang="ts">
import { capitalize } from '$lib/helpers/string';
import { app } from '$lib/stores/app';
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import type { Models } from '@appwrite.io/console';
import { Badge, Card, Layout, Logs, Spinner, Typography } from '@appwrite.io/pink-svelte';
import LogsTimer from './logsTimer.svelte';
Expand All @@ -38,15 +40,19 @@
emptyCopy?: string;
} = $props();
let effectiveStatus = $derived(
getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables)
);
Comment on lines +43 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Logs badge stays “building” forever once mounted

effectiveStatus is derived solely from deployment data and console variables; neither changes when the timeout threshold is crossed, so the derived value never flips to “failed.” The spinner view and badge therefore keep showing “building.” Add a ticking dependency (e.g., a $state timer updated every second and referenced inside the $derived) or feed this component an already time-reactive status so it re-computes as time passes.

🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/sites/(components)/logs.svelte
around lines 43–45, the derived effectiveStatus only depends on deployment and
console variables so it never recomputes when the build timeout elapses; add a
ticking reactive dependency (e.g., create a small writable store or local $state
counter updated via setInterval on mount and cleared on destroy) and reference
that $state inside the $derived call (or pass a time-reactive value such as
Date.now() from that timer into getEffectiveBuildStatus) so the derived value
recomputes every second and flips from “building” to “failed” when the threshold
is crossed.

function setCopy() {
if (deployment.status === 'failed') {
if (effectiveStatus === 'failed') {
return 'Your deployment has failed.';
} else if (deployment.status === 'building') {
} else if (effectiveStatus === 'building') {
//Do not remove empty space before the string it's an invisible character
return 'Preparing for build ... \n';
} else if (deployment.status === 'waiting') {
} else if (effectiveStatus === 'waiting') {
return 'Preparing for build ... \n';
} else if (deployment.status === 'processing') {
} else if (effectiveStatus === 'processing') {
return 'Preparing for build ... \n';
} else {
return emptyCopy;
Expand All @@ -62,16 +68,16 @@
Deployment logs
</Typography.Text>
<Badge
content={capitalize(deployment.status)}
content={capitalize(effectiveStatus)}
size="xs"
variant="secondary"
type={badgeTypeDeployment(deployment.status)} />
type={badgeTypeDeployment(effectiveStatus)} />
</Layout.Stack>
<LogsTimer status={deployment.status} {deployment} />
<LogsTimer status={effectiveStatus} {deployment} />
</Layout.Stack>
{/if}

{#if ['waiting', 'processing'].includes(deployment.status) || (deployment.status === 'building' && !deployment?.buildLogs?.length)}
{#if ['waiting', 'processing'].includes(effectiveStatus) || (effectiveStatus === 'building' && !deployment?.buildLogs?.length)}
<Card.Base variant="secondary">
<Layout.Stack direction="row" justifyContent="center" gap="s">
<Spinner /> Waiting for build to start...
Expand Down
Loading