diff --git a/bun.lock b/bun.lock index 5c92cd04d..1308ed221 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7dc3a8f", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c1feb89", "@appwrite.io/pink-legacy": "^1.0.3", @@ -24,6 +24,7 @@ "dayjs": "^1.11.13", "deep-equal": "^2.2.3", "echarts": "^5.6.0", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478", "ignore": "^6.0.2", "nanoid": "^5.1.5", "nanotar": "^0.1.1", @@ -107,7 +108,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@7dc3a8f", { "dependencies": { "bignumber.js": "9.0.0", "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478", { "dependencies": { "bignumber.js": "9.0.0", "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index 985efb2fa..75c0cecca 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@7dc3a8f", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@110a478", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@c1feb89", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/routes/(console)/project-[region]-[project]/+layout.svelte b/src/routes/(console)/project-[region]-[project]/+layout.svelte index eeeff2dd7..5c55169cb 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.svelte +++ b/src/routes/(console)/project-[region]-[project]/+layout.svelte @@ -25,6 +25,34 @@ canWriteSites } from '$lib/stores/roles'; import CsvImportBox from '$lib/components/csvImportBox.svelte'; + import { currentPlan } from '$lib/stores/organization'; + import { isCloud } from '$lib/system'; + import PausedProjectModal from './pausedProjectModal.svelte'; + + /** + * Calculate if the project is paused based on console access date and plan's inactivity threshold. + */ + function isProjectPaused( + consoleAccessedAt: string | null | undefined, + projectInactivityDays: number | undefined + ): boolean { + if (!isCloud) return false; + if (!projectInactivityDays || projectInactivityDays <= 0) return false; + if (!consoleAccessedAt) return false; + + const lastAccess = new Date(consoleAccessedAt); + const now = new Date(); + const diffMs = now.getTime() - lastAccess.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + return diffDays >= projectInactivityDays; + } + + let showPausedModal: boolean; + $: showPausedModal = isProjectPaused( + ($project as { consoleAccessedAt?: string })?.consoleAccessedAt, + $currentPlan?.projectInactivityDays + ); onMount(() => { return realtime.forProject(page.params.region, ['project', 'console'], (response) => { @@ -114,6 +142,10 @@ +{#if isCloud} + +{/if} +
diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index bbb433100..568f6cf07 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -21,16 +21,6 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { const project = await sdk.forConsole.projects.get({ projectId: params.project }); project.region ??= 'default'; - // Track console access for cloud only (fire-and-forget, backend has 6-day cooldown) - if (isCloud) { - generateFingerprintToken() - .then((fingerprint) => { - sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint; - return sdk.forConsole.projects.updateConsoleAccess({ projectId: params.project }); - }) - .catch(() => {}); - } - // fast path without a network call! let organization = (organizations as Models.OrganizationList)?.teams?.find( (org) => org.$id === project.teamId @@ -103,6 +93,30 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { plansInfo.set(organization.billingPlanId, organizationPlan); } + // Track console access for cloud only (fire-and-forget, backend has 6-day cooldown) + // Don't call if project is paused - user must explicitly resume via createConsoleAccess + if (isCloud) { + const projectInactivityDays = organizationPlan?.projectInactivityDays ?? 0; + const consoleAccessedAt = (project as { consoleAccessedAt?: string }).consoleAccessedAt; + + let isPaused = false; + if (projectInactivityDays > 0 && consoleAccessedAt) { + const lastAccess = new Date(consoleAccessedAt); + const now = new Date(); + const diffDays = Math.floor((now.getTime() - lastAccess.getTime()) / (1000 * 60 * 60 * 24)); + isPaused = diffDays >= projectInactivityDays; + } + + if (!isPaused) { + generateFingerprintToken() + .then((fingerprint) => { + sdk.forConsole.client.headers['X-Appwrite-Console-Fingerprint'] = fingerprint; + return sdk.forConsole.projects.updateConsoleAccess({ projectId: params.project }); + }) + .catch(() => {}); + } + } + return { project, organization, diff --git a/src/routes/(console)/project-[region]-[project]/pausedProjectModal.svelte b/src/routes/(console)/project-[region]-[project]/pausedProjectModal.svelte new file mode 100644 index 000000000..653f02631 --- /dev/null +++ b/src/routes/(console)/project-[region]-[project]/pausedProjectModal.svelte @@ -0,0 +1,76 @@ + + + + + + This project has been paused due to inactivity on the free plan. + + + Your data is safe and will remain intact. Resume the project to continue using it. + + + {#if error} + (error = null)}> + {error} + + {/if} + + + + + + + +