├── .npmrc ├── .tool-versions ├── src ├── routes │ ├── +layout.js │ ├── (auth) │ │ ├── (project) │ │ │ ├── disk │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── create │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── (detail) │ │ │ │ │ ├── +layout.js │ │ │ │ │ ├── +layout.svelte │ │ │ │ │ ├── metrics │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ └── detail │ │ │ │ │ │ └── +page.svelte │ │ │ │ └── +page.svelte │ │ │ ├── role │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── users │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── bind │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── create │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ └── +page.svelte │ │ │ ├── +layout.server.js │ │ │ ├── domain │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── detail │ │ │ │ │ └── +page.js │ │ │ │ ├── cdn-downgrade │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── create │ │ │ │ │ └── +page.svelte │ │ │ ├── email │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── route │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ └── create │ │ │ │ │ └── +page.svelte │ │ │ ├── dropbox │ │ │ │ ├── +layout.js │ │ │ │ └── +page.svelte │ │ │ ├── registry │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ └── +page.svelte │ │ │ ├── deployment │ │ │ │ ├── +layout.js │ │ │ │ ├── (detail) │ │ │ │ │ ├── events │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── logs │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── revision │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── detail │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── +layout.js │ │ │ │ │ ├── +layout.svelte │ │ │ │ │ └── metrics │ │ │ │ │ │ └── +page.svelte │ │ │ │ ├── +page.js │ │ │ │ ├── deploy │ │ │ │ │ └── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ └── _components │ │ │ │ │ └── Header.svelte │ │ │ ├── pull-secret │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── create │ │ │ │ │ └── +page.svelte │ │ │ ├── service-account │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── create │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ └── +page.svelte │ │ │ ├── workload-identity │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── +page.svelte │ │ │ │ └── create │ │ │ │ │ └── +page.svelte │ │ │ ├── +layout.svelte │ │ │ ├── +page.js │ │ │ ├── +layout.js │ │ │ └── +page.svelte │ │ ├── project │ │ │ ├── +page.js │ │ │ ├── create │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ └── +page.svelte │ │ ├── billing │ │ │ ├── +page.js │ │ │ ├── report │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── detail │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── create │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ └── +page.svelte │ │ ├── +layout.js │ │ ├── ModalSelectProject.svelte │ │ ├── +layout.svelte │ │ ├── Navbar.svelte │ │ └── Sidebar.svelte │ ├── +layout.svelte │ ├── auth │ │ ├── signout │ │ │ └── +server.js │ │ ├── callback │ │ │ └── +server.js │ │ └── signin │ │ │ └── +server.js │ └── api │ │ ├── dropbox │ │ └── +server.js │ │ ├── registry │ │ └── [fn] │ │ │ └── +server.js │ │ └── [fn] │ │ └── +server.js ├── app.d.ts ├── lib │ ├── components │ │ ├── LoadingRow.svelte │ │ ├── NoDataRow.svelte │ │ ├── ErrorRow.svelte │ │ ├── StatusIcon.svelte │ │ ├── Secret.svelte │ │ ├── DeploymentStatusIcon.svelte │ │ └── Chart.svelte │ ├── api │ │ └── index.js │ ├── format │ │ └── index.js │ ├── modal │ │ └── index.js │ └── hc │ │ └── index.js ├── app.html ├── hooks.server.js └── style │ └── main.scss ├── .dockerignore ├── static ├── favicon.png ├── favicon.webp └── images │ ├── logo.png │ └── logo.webp ├── .editorconfig ├── .env ├── vite.config.js ├── jsconfig.json ├── playwright.config.js ├── .gitignore ├── .github └── workflows │ └── check.yaml ├── Makefile ├── wrangler.toml ├── README.md ├── svelte.config.js ├── Dockerfile ├── LICENSE ├── package.json └── eslint.config.js /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | bun 1.3.4 2 | nodejs 24.12.0 3 | -------------------------------------------------------------------------------- /src/routes/+layout.js: -------------------------------------------------------------------------------- 1 | // export const ssr = false 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .svelte-kit/ 3 | build/ 4 | .idea/ -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/favicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/HEAD/static/favicon.webp -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/HEAD/static/images/logo.png -------------------------------------------------------------------------------- /static/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/HEAD/static/images/logo.webp -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.svelte] 4 | indent_style = tab 5 | 6 | [*.{js,cjs}] 7 | indent_style = tab 8 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'disk', 4 | overrideRedirect: '/disk' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'role', 4 | overrideRedirect: '/role' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/+layout.server.js: -------------------------------------------------------------------------------- 1 | export function load ({ locals }) { 2 | return { 3 | restoreProject: locals.project || '' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'domain', 4 | overrideRedirect: '/domain' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/email/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'email', 4 | overrideRedirect: '/email' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/route/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'route', 4 | overrideRedirect: '/route' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/dropbox/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'dropbox', 4 | overrideRedirect: '/dropbox' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/registry/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'registry', 4 | overrideRedirect: '/registry' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/project/+page.js: -------------------------------------------------------------------------------- 1 | export async function load ({ parent }) { 2 | const { projects } = await parent() 3 | 4 | return { 5 | projects 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'deployment', 4 | overrideRedirect: '/deployment' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/pull-secret/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'pull-secret', 4 | overrideRedirect: '/pull-secret' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | API_ENDPOINT=https://api.deploys.app 2 | OAUTH2_CLIENT_ID=localhost 3 | OAUTH2_CLIENT_SECRET=localhost 4 | PUBLIC_API_ENDPOINT=https://api.deploys.app 5 | PUBLIC_SENTRY_DSN= 6 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'service-account', 4 | overrideRedirect: '/service-account' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/workload-identity/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'workload-identity', 4 | overrideRedirect: '/workload-identity' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | sveltekit() 7 | ] 8 | }) 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "strict": true, 7 | "noImplicitAny": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/events/+page.js: -------------------------------------------------------------------------------- 1 | export async function load ({ parent }) { 2 | const { 3 | deployment 4 | } = await parent() 5 | 6 | return { 7 | deployment 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/logs/+page.js: -------------------------------------------------------------------------------- 1 | export async function load ({ parent }) { 2 | const { 3 | deployment 4 | } = await parent() 5 | 6 | return { 7 | deployment 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@playwright/test').PlaywrightTestConfig} */ 2 | const config = { 3 | webServer: { 4 | command: 'npm run build && npm run preview', 5 | port: 3000 6 | } 7 | } 8 | 9 | export default config 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env.* 7 | !.env.example 8 | .pnp.* 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/sdks 14 | !.yarn/versions 15 | .idea 16 | .vscode 17 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | interface Locals { 6 | token: string 7 | project: string 8 | } 9 | } 10 | } 11 | 12 | export {} 13 | -------------------------------------------------------------------------------- /src/lib/components/LoadingRow.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
{text}
40 |
--------------------------------------------------------------------------------
/src/routes/api/registry/[fn]/+server.js:
--------------------------------------------------------------------------------
1 | const endpoint = 'https://registry.deploys.app/api'
2 |
3 | /** @type {import('./$types').RequestHandler} */
4 | export async function POST ({ locals, params, request }) {
5 | const token = locals.token
6 |
7 | // fast-path to reject unauthorized requests
8 | if (!token) {
9 | return new Response(JSON.stringify({
10 | ok: false,
11 | error: {
12 | message: 'api: unauthorized'
13 | }
14 | }), {
15 | headers: {
16 | 'content-type': 'application/json'
17 | }
18 | })
19 | }
20 |
21 | const resp = await fetch(`${endpoint}/${params.fn}`, {
22 | method: 'POST',
23 | body: request.body,
24 | // @ts-expect-error workaround for missing type
25 | duplex: 'half',
26 | headers: {
27 | accept: 'application/json',
28 | 'content-type': request.headers.get('content-type') ?? 'application/json',
29 | authorization: `bearer ${token}`
30 | }
31 | })
32 | return new Response(resp.body, {
33 | status: resp.status,
34 | headers: {
35 | 'content-type': resp.headers.get('content-type') ?? ''
36 | }
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/src/routes/(auth)/(project)/deployment/(detail)/+layout.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
30 |
31 | registry.deploys.app/{project}/{'{repository}'}:{'{tag}'}
16 || Repository | 21 |
|---|
| 27 | 29 | {repo.name} 30 | 31 | | 32 |
| Domain | 26 |Quota | 27 |Created at | 28 |
|---|---|---|
| {it.domain} | 34 |- | 35 |{format.datetime(it.createdAt)} | 36 |
| Last Seen | 38 |Type | 39 |Reason | 40 |Message | 41 |
|---|---|---|---|
| {format.datetime(it.lastSeen)} | 47 |{it.type} | 48 |{it.reason} | 49 |{it.message} | 50 |
{downloadUrl}
61 | | Billing name | 27 |Billing ID | 28 |Active | 29 |
|---|---|---|
| 35 | {it.name} 36 | | 37 |{it.id} | 38 |39 | {#if it.active} 40 | 41 | {:else} 42 | 43 | {/if} 44 | | 45 |
| Name | 30 |Location | 31 |Created at | 32 |
|---|---|---|
|
38 | |
43 | {it.location} | 44 |{format.datetime(it.createdAt)} | 45 |
| Name | 30 |Location | 31 |Created at | 32 |Created by | 33 |
|---|---|---|---|
|
39 | |
44 | {it.location} | 45 |{format.datetime(it.createdAt)} | 46 |{it.createdBy} | 47 |
| Name | 30 |Created At | 31 |32 | | |
|---|---|---|---|
| 38 | 39 | {it.email} 40 | 41 | | 42 |{it.name} | 43 |{format.datetime(it.createdAt)} | 44 |45 | 46 | 49 | 50 | | 51 |
| Revision | 40 |Image | 41 |Deployed At | 42 |Deployed By | 43 |44 | |
|---|---|---|---|---|
| {it.revision} | 50 |{it.image} | 51 |{format.datetime(it.createdAt)} | 52 |{it.createdBy} | 53 |54 | {#if index > 0} 55 | 56 | {/if} 57 | | 58 |
| Disk name | 30 |Size | 31 |Location | 32 |Created at | 33 |34 | |
|---|---|---|---|---|
|
40 | |
45 | {it.size} GiB | 46 |{it.location} | 47 |{format.datetime(it.createdAt)} | 48 |49 | 50 | 53 | 54 | | 55 |
| Role | 36 |Name | 37 |Created At | 38 |Created By | 39 |40 | |
|---|---|---|---|---|
| 46 | {#if roleCanUpdate(it.role)} 47 | 48 | {it.role} 49 | 50 | {:else} 51 | {it.role} 52 | {/if} 53 | | 54 |{it.name} | 55 |{format.datetime(it.createdAt)} | 56 |{it.createdBy} | 57 |58 | {#if roleCanUpdate(it.role)} 59 | 60 | 63 | 64 | {/if} 65 | | 66 |
| Roles | 53 |54 | | |
|---|---|---|
| {it.email} | 60 |
61 | {#each it.roles as r}
62 | {r} 63 | {/each} 64 | |
65 | 66 | 67 | 70 | 71 | 74 | | 75 |
| Tag | 48 |Digest | 49 |Created At | 50 |
|---|---|---|
| 56 | {tag.tag} 57 | 58 | 59 | 60 | | 61 |62 | {format.shortDigest(tag.digest)} 63 | 64 | 65 | 66 | | 67 |{format.datetime(tag.createdAt)} | 68 |
| Name | 30 |Type | 31 | 32 |Memory | 33 |Replicas | 34 |Location | 35 |Last deployed | 36 | 37 |
|---|---|---|---|---|---|
|
43 | |
48 | {format.deploymentType(it.type)} | 49 | 50 |{format.memory(it.resources.requests.memory)} | 51 |52 | {#if it.minReplicas > 0} 53 | {#if it.minReplicas === it.maxReplicas} 54 | {it.minReplicas} 55 | {:else} 56 | {it.minReplicas} - {it.maxReplicas} 57 | {/if} 58 | {:else} 59 | - 60 | {/if} 61 | | 62 |{it.location} | 63 |{format.datetime(it.createdAt)} | 64 | 65 |
| Route | 53 |Target | 54 |Location | 55 |Config | 56 | 57 | 58 |59 | |
|---|---|---|---|---|
| 65 | https://{it.domain}{it.path} 68 | | 69 |{it.target} | 70 |{it.location} | 71 |72 | {#if it.config.basicAuth} 73 | 74 | {/if} 75 | | 76 | 77 | 78 |79 | 82 | | 83 |
| Name | 62 |ID | 63 |Number | 64 |65 | |
|---|---|---|---|
| 71 | 72 | {it.name} 73 | 74 | | 75 |{it.project} | 76 |{it.id} | 77 |78 | 79 | 82 | 83 | 86 | | 87 |
| Domain | 49 |Wildcard | 50 |CDN | 51 |Location | 52 | 53 | 54 |55 | |
|---|---|---|---|---|
|
61 | |
64 | 65 | {#if it.wildcard} 66 | 67 | {:else} 68 | 69 | {/if} 70 | | 71 |72 | {#if it.cdn} 73 | 74 | {:else} 75 | 76 | {/if} 77 | | 78 |{it.location} | 79 | 80 | 81 |82 | 85 | | 86 |
88 |
89 | {#if projectInfo}
90 | {format.gsaBinding(projectInfo.id, workloadIdentity.name, workloadIdentity.gsa, 'acoshift-1362')}
91 | {/if}
92 |
93 | | Project | 157 |Name | 158 |Usage Value | 159 |Billing Value | 160 |
|---|---|---|---|
| {it.projectSid} | 166 |{it.name} | 167 |{it.usageValue} | 168 |{it.billingValue} | 169 |
| URL | 54 |55 | 56 | {`https://${deployment.url}`} 57 | 58 | 59 | 60 | 61 | | 62 |
| Internal URL | 66 |67 | http://{deployment.internalUrl} 68 | 69 | 70 | 71 | | 72 |
| Address | 77 |{deployment.address}:{deployment.nodePort} | 78 |
| Internal Address | 83 |84 | {deployment.internalAddress}:{deployment.port} 85 | 86 | 87 | 88 | | 89 |
| Type | 93 |94 | {format.deploymentType(deployment.type)} 95 | {#if deployment.internal} 96 | (Internal) 97 | {/if} 98 | | 99 |
| Location | 102 |103 | {deployment.location} 104 | 105 | 106 | 107 | | 108 |
| Image | 111 |112 | {deployment.image} 113 | 114 | 115 | 116 | | 117 |
| Port | 121 |{deployment.port}{deployment.protocol ? `:${deployment.protocol}` : ''} | 122 |
| Port | 126 |{deployment.port}:{deployment.nodePort} | 127 |
| Disk | 132 |133 | {#if deployment.disk} 134 | {deployment.disk.name} 135 | (mount: {deployment.disk.mountPath || '-'}, sub: {deployment.disk.subPath || '-'}) 136 | {:else} 137 | - 138 | {/if} 139 | | 140 |
| Replicas | 145 |146 | {#if deployment.minReplicas > 0} 147 | {#if deployment.minReplicas === deployment.maxReplicas} 148 | {deployment.minReplicas} 149 | {:else} 150 | {deployment.minReplicas} - {deployment.maxReplicas} 151 | {/if} 152 | {:else} 153 | - 154 | {/if} 155 | | 156 |
| Schedule | 161 |{deployment.schedule} | 162 |
| Command | 166 |{deployment.command.join(' ') || '-'} | 167 |
| Args | 170 |{deployment.args.join(' ') || '-'} | 171 |
| Pull Secret | 174 |{deployment.pullSecret || '-'} | 175 |
| Workload Identity | 179 |{deployment.workloadIdentity || '-'} | 180 |
| CPU limited | 188 |{format.cpuLimited(deployment.resources.limits.cpu)} | 189 |
| Memory allocated | 192 |{format.memory(deployment.resources.requests.memory)} | 193 |
| Sidecars | 196 |{deployment.sidecars?.length || 0} | 197 |
| Deployed At | 200 |{format.datetime(deployment.createdAt)} | 201 |
| Deployed By | 204 |{deployment.createdBy} | 205 |
| Allocated Price | 208 |{deployment.allocatedPrice.toLocaleString(undefined, { maximumFractionDigits: 2 })} THB/month/replica | 209 |
| Env | 226 |Value | 227 |
|---|---|
| {k} | 233 |
234 | |
236 |
| Path | 250 |Data | 251 |
|---|---|
| {k} | 257 |{v} | 258 |