codebay 0.0.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.mochi/import-css/index-0nwv3654.css +18 -0
  2. package/.mochi/import-css/index-xndx4980.css +54 -0
  3. package/.mochi/manifest.json +1612 -0
  4. package/.mochi/server-island.js +1 -0
  5. package/.mochi/svelte-client/HydratableIsland-wrq79y0w.js +1 -0
  6. package/.mochi/svelte-client/_hydrate-AppShell_2rtnxn6pgp2rh-7xgbmxxg.js +2 -0
  7. package/.mochi/svelte-client/_hydrate-AvatarsView_257s9yq1wwr4f-3cnbjpz2.js +13 -0
  8. package/.mochi/svelte-client/_hydrate-InstanceView_2ma45l9bybfhl-2ntmdm3e.js +1 -0
  9. package/.mochi/svelte-client/_hydrate-SettingsView_8hmarwmf8onq-t08spym1.js +23 -0
  10. package/.mochi/svelte-client/chunk-04qv4w6w.js +2 -0
  11. package/.mochi/svelte-client/chunk-5jnepvzx.js +2 -0
  12. package/.mochi/svelte-client/chunk-hcztrd75.js +1 -0
  13. package/.mochi/svelte-client/chunk-qxwt25hn.js +4 -0
  14. package/.mochi/svelte-client/chunk-sjmcgh7f.js +2 -0
  15. package/.mochi/svelte-client/chunk-tr67wtwn.js +2 -0
  16. package/.mochi/svelte-compile/App-f0q5n4rk.server.js +1168 -0
  17. package/.mochi/svelte-compile/Avatars-p8gfef7x.server.js +332 -0
  18. package/.mochi/svelte-compile/ClientStats-z3tn9ad6.server.js +99 -0
  19. package/.mochi/svelte-compile/DefaultError-wh77vebf.server.js +52 -0
  20. package/.mochi/svelte-compile/Instance-atjwsczx.server.js +356 -0
  21. package/.mochi/svelte-compile/Settings-q88xw6h8.server.js +1173 -0
  22. package/.mochi/svelte-compile/chunk-4kwy539a.js +14 -0
  23. package/.mochi/svelte-compile/chunk-9bjk7yx6.js +81 -0
  24. package/.mochi/svelte-compile/chunk-btmcxhqy.js +58 -0
  25. package/.mochi/svelte-compile/chunk-sxqys3nw.js +1668 -0
  26. package/.mochi/svelte-compile/chunk-v6796ga4.js +3295 -0
  27. package/.mochi/svelte-compile/chunk-we0ggtws.js +34 -0
  28. package/.mochi/svelte-css/AppBar-3bxinv4pabv8l.css +1 -0
  29. package/.mochi/svelte-css/AppShell-36mpg5smnbmuw.css +1 -0
  30. package/.mochi/svelte-css/Avatar-24th63awm9mna.css +1 -0
  31. package/.mochi/svelte-css/AvatarEditor-1ryrs1md5g36y.css +1 -0
  32. package/.mochi/svelte-css/AvatarsView-3hq37u4dgtljn.css +1 -0
  33. package/.mochi/svelte-css/BackLink-2wlu0eovz9j86.css +1 -0
  34. package/.mochi/svelte-css/BranchBox-1yqu1q2mgnjwz.css +1 -0
  35. package/.mochi/svelte-css/Brand-1la7zw5uhcbl4.css +1 -0
  36. package/.mochi/svelte-css/Button-2vtbrzb5kgu25.css +1 -0
  37. package/.mochi/svelte-css/CheckmarkIcon-3qlazj2oc50jc.css +1 -0
  38. package/.mochi/svelte-css/ClientStats-19qzckq40p2q9.css +1 -0
  39. package/.mochi/svelte-css/CoinButton-1rej3sh3tkjud.css +1 -0
  40. package/.mochi/svelte-css/CredMenu-9hwgeptp7crb.css +1 -0
  41. package/.mochi/svelte-css/DashboardView-1cy4xpqhja9sx.css +1 -0
  42. package/.mochi/svelte-css/DefaultError-2b0daamydxgb2.css +1 -0
  43. package/.mochi/svelte-css/ErrorIcon-2x0tzsqxskw28.css +1 -0
  44. package/.mochi/svelte-css/FolderBrowser-1x428zb3f2kti.css +1 -0
  45. package/.mochi/svelte-css/HealthBox-er2her0jukwl.css +1 -0
  46. package/.mochi/svelte-css/IdeBar-2qoqi2ya9zdn3.css +1 -0
  47. package/.mochi/svelte-css/IdeLoader-26rlko8e00j3g.css +1 -0
  48. package/.mochi/svelte-css/InstanceCard-16f77aysm7lgp.css +1 -0
  49. package/.mochi/svelte-css/InstanceView-wq85bxtusont.css +1 -0
  50. package/.mochi/svelte-css/LoaderIcon-soq5wt3p05ye.css +1 -0
  51. package/.mochi/svelte-css/PortsBox-1t25b5yg7jva3.css +1 -0
  52. package/.mochi/svelte-css/SettingsCog-jftyadvo64qo.css +1 -0
  53. package/.mochi/svelte-css/SettingsView-1nfdwnm16cpp2.css +1 -0
  54. package/.mochi/svelte-css/Skeleton-8k69usiju43h.css +1 -0
  55. package/.mochi/svelte-css/StatusBadge-3mq0j7z6aq4j.css +1 -0
  56. package/.mochi/svelte-css/ThemePicker-1ir8jyocm8mvi.css +1 -0
  57. package/.mochi/svelte-css/ToastBar-3havgkmtofntq.css +1 -0
  58. package/.mochi/svelte-css/ToastIcon-2z5wrkc95khg0.css +1 -0
  59. package/.mochi/svelte-css/ToastMessage-2jfwb2og2lfa8.css +1 -0
  60. package/.mochi/svelte-css/ToastWrapper-1fbnym10rm2nv.css +1 -0
  61. package/.mochi/svelte-css/Toaster-kk60j193854n.css +1 -0
  62. package/.mochi/svelte-css/TopBar-2q87li6yt8ids.css +1 -0
  63. package/LICENSE +21 -0
  64. package/README.md +34 -3
  65. package/bin/codebay.ts +22 -0
  66. package/migrations/0001_init.sql +20 -0
  67. package/migrations/0002_port_forwards.sql +9 -0
  68. package/migrations/0003_options.sql +4 -0
  69. package/migrations/0004_instance_image.sql +1 -0
  70. package/package.json +87 -9
  71. package/public/sounds/done.wav +0 -0
  72. package/public/sounds/waiting.wav +0 -0
  73. package/src/__fixtures__/HelloWorld.svelte +4 -0
  74. package/src/additional-svelte-typings.d.ts +1 -0
  75. package/src/api.ts +61 -0
  76. package/src/avatars/anchor.ts +17 -0
  77. package/src/avatars/bear.ts +17 -0
  78. package/src/avatars/bee.ts +17 -0
  79. package/src/avatars/bob-marley.ts +17 -0
  80. package/src/avatars/car.ts +17 -0
  81. package/src/avatars/carpathians.ts +17 -0
  82. package/src/avatars/cat-body.ts +17 -0
  83. package/src/avatars/cat-sits.ts +17 -0
  84. package/src/avatars/cat.ts +17 -0
  85. package/src/avatars/celtic-cross.ts +17 -0
  86. package/src/avatars/cherry.ts +17 -0
  87. package/src/avatars/cotton.ts +17 -0
  88. package/src/avatars/crab.ts +17 -0
  89. package/src/avatars/crown.ts +17 -0
  90. package/src/avatars/diamond.ts +17 -0
  91. package/src/avatars/dog.ts +17 -0
  92. package/src/avatars/elephant.ts +17 -0
  93. package/src/avatars/fish.ts +17 -0
  94. package/src/avatars/fleur-de-lis.ts +17 -0
  95. package/src/avatars/flower.ts +17 -0
  96. package/src/avatars/fox.ts +17 -0
  97. package/src/avatars/frog.ts +17 -0
  98. package/src/avatars/ghost.ts +17 -0
  99. package/src/avatars/giraffe.ts +17 -0
  100. package/src/avatars/glasses.ts +17 -0
  101. package/src/avatars/heart.ts +17 -0
  102. package/src/avatars/index.ts +128 -0
  103. package/src/avatars/invader.ts +17 -0
  104. package/src/avatars/kangaroo.ts +17 -0
  105. package/src/avatars/key.ts +17 -0
  106. package/src/avatars/lightning.ts +17 -0
  107. package/src/avatars/mushroom.ts +17 -0
  108. package/src/avatars/name-prompts.ts +30 -0
  109. package/src/avatars/octopus.ts +17 -0
  110. package/src/avatars/owl.ts +17 -0
  111. package/src/avatars/penguin.ts +17 -0
  112. package/src/avatars/planet.ts +17 -0
  113. package/src/avatars/rabbit.ts +17 -0
  114. package/src/avatars/rat.ts +17 -0
  115. package/src/avatars/robot.ts +17 -0
  116. package/src/avatars/rocket.ts +17 -0
  117. package/src/avatars/sea.ts +17 -0
  118. package/src/avatars/serialize.ts +65 -0
  119. package/src/avatars/skull.ts +17 -0
  120. package/src/avatars/snail.ts +17 -0
  121. package/src/avatars/snake.ts +17 -0
  122. package/src/avatars/snakegame.ts +17 -0
  123. package/src/avatars/star.ts +17 -0
  124. package/src/avatars/svelte.ts +17 -0
  125. package/src/avatars/target.ts +17 -0
  126. package/src/avatars/tree.ts +17 -0
  127. package/src/avatars/types.ts +24 -0
  128. package/src/avatars/ukraine.ts +17 -0
  129. package/src/avatars/whale.ts +17 -0
  130. package/src/avatars/wheat.ts +17 -0
  131. package/src/avatars/yinyan.ts +17 -0
  132. package/src/components/AppBar.svelte +35 -0
  133. package/src/components/AppShell.svelte +302 -0
  134. package/src/components/Avatar.svelte +235 -0
  135. package/src/components/AvatarEditor.svelte +297 -0
  136. package/src/components/AvatarsView.svelte +115 -0
  137. package/src/components/BranchBox.svelte +32 -0
  138. package/src/components/Brand.svelte +41 -0
  139. package/src/components/Button.svelte +161 -0
  140. package/src/components/CoinButton.svelte +121 -0
  141. package/src/components/CredMenu.svelte +171 -0
  142. package/src/components/DashboardView.svelte +198 -0
  143. package/src/components/FolderBrowser.svelte +635 -0
  144. package/src/components/HealthBox.svelte +161 -0
  145. package/src/components/IdeBar.svelte +186 -0
  146. package/src/components/IdeLoader.svelte +135 -0
  147. package/src/components/InstanceCard.svelte +220 -0
  148. package/src/components/InstanceView.svelte +567 -0
  149. package/src/components/PortsBox.svelte +74 -0
  150. package/src/components/SettingsCog.svelte +22 -0
  151. package/src/components/SettingsView.svelte +1494 -0
  152. package/src/components/Skeleton.svelte +43 -0
  153. package/src/components/StatusBadge.svelte +54 -0
  154. package/src/components/ThemePicker.svelte +77 -0
  155. package/src/components/TopBar.svelte +64 -0
  156. package/src/components/UiShowcase.svelte +578 -0
  157. package/src/components/ui-showcase/ComponentDemo.svelte +85 -0
  158. package/src/container-injections/attention-hooks.ts +117 -0
  159. package/src/container-injections/claude-aliases.ts +43 -0
  160. package/src/container-injections/claude-code-credentials.ts +145 -0
  161. package/src/container-injections/claude-code-custom.ts +125 -0
  162. package/src/container-injections/claude-no-coauthor.ts +35 -0
  163. package/src/container-injections/claude-skip-permissions.ts +38 -0
  164. package/src/container-injections/claude-statusline.ts +109 -0
  165. package/src/container-injections/git-identity.ts +63 -0
  166. package/src/container-injections/git-safe-directory.ts +18 -0
  167. package/src/container-injections/github-credentials.ts +162 -0
  168. package/src/container-injections/host-env-vars.ts +108 -0
  169. package/src/container-injections/tmux.ts +67 -0
  170. package/src/fontsource.d.ts +3 -0
  171. package/src/index.ts +81 -0
  172. package/src/lib/auth.server.ts +95 -0
  173. package/src/lib/bridge.server.ts +34 -0
  174. package/src/lib/config.server.ts +140 -0
  175. package/src/lib/crypto.server.ts +12 -0
  176. package/src/lib/db.server.ts +196 -0
  177. package/src/lib/devcontainer.server.ts +511 -0
  178. package/src/lib/docker-client.server.ts +61 -0
  179. package/src/lib/docker.server.ts +139 -0
  180. package/src/lib/exec.server.ts +92 -0
  181. package/src/lib/git.server.ts +68 -0
  182. package/src/lib/health.server.ts +105 -0
  183. package/src/lib/injections.server.ts +72 -0
  184. package/src/lib/instances.server.ts +552 -0
  185. package/src/lib/links.ts +9 -0
  186. package/src/lib/picker.server.ts +46 -0
  187. package/src/lib/ports.server.ts +9 -0
  188. package/src/lib/proxy.server.ts +170 -0
  189. package/src/lib/repo-url.ts +60 -0
  190. package/src/lib/spawn.server.ts +14 -0
  191. package/src/lib/theme.server.ts +22 -0
  192. package/src/live.ts +50 -0
  193. package/src/pages/App.svelte +14 -0
  194. package/src/pages/Avatars.svelte +9 -0
  195. package/src/pages/Instance.svelte +9 -0
  196. package/src/pages/Settings.svelte +76 -0
  197. package/src/pages/UI.svelte +10 -0
  198. package/src/routes.ts +444 -0
  199. package/src/settings.ts +14 -0
  200. package/src/shell.html +100 -0
  201. package/src/sound.ts +69 -0
  202. package/src/theme.ts +24 -0
  203. package/src/types.ts +79 -0
  204. package/svelte.config.js +5 -0
@@ -0,0 +1,567 @@
1
+ <script lang="ts">
2
+ import { ideUrl, type Instance, type InstanceHealth } from '../types.ts';
3
+ import ArrowLeft from '@lucide/svelte/icons/arrow-left';
4
+ import ArrowUpRight from '@lucide/svelte/icons/arrow-up-right';
5
+ import HealthBox from './HealthBox.svelte';
6
+ import StatusBadge from './StatusBadge.svelte';
7
+ import Skeleton from './Skeleton.svelte';
8
+ import { forwardedPortUrl } from '../lib/links.ts';
9
+ import { liveSocket, liveStream } from '../live.ts';
10
+ import { apiPost, apiDelete } from '../api.ts';
11
+ import { enhance } from 'mochi-framework';
12
+ import type { MochiEnhanceOptions } from 'mochi-framework';
13
+
14
+ let { id, injectionChecks = 0 }: { id: string; injectionChecks?: number } = $props();
15
+
16
+ let instance = $state<Instance | null>(null);
17
+ let health = $state<InstanceHealth | null>(null);
18
+ let lastFetchedAt = $state<number | null>(null);
19
+ let logs = $state('');
20
+
21
+ // Runs after the DOM updates, so scrollHeight is fresh without a tick().
22
+ function autoscroll(node: HTMLDivElement) {
23
+ void logs; // the dependency this effect exists for
24
+ node.scrollTop = node.scrollHeight;
25
+ }
26
+
27
+ // Reset on every connect, because the server replays its whole buffer each time.
28
+ $effect(() =>
29
+ liveSocket(
30
+ `/api/instances/${id}/logs`,
31
+ (chunk) => (logs += chunk),
32
+ () => (logs = '')
33
+ )
34
+ );
35
+
36
+ $effect(() =>
37
+ liveStream((msg) => {
38
+ if (msg.type === 'instances') {
39
+ const found = msg.data.find((i) => i.id === id);
40
+ if (found) instance = found;
41
+ } else if (msg.type === 'health' && msg.data.id === id) {
42
+ health = msg.data.health;
43
+ lastFetchedAt = Date.now();
44
+ }
45
+ })
46
+ );
47
+
48
+ const url = $derived(instance ? ideUrl(instance) : '#');
49
+
50
+ let newPort = $state('');
51
+ let portError = $state<string | null>(null);
52
+ // Set when the forward set changes this session; cleared once a rebuild applies it.
53
+ let pendingRebuild = $state(false);
54
+ const forwards = $derived(instance?.forwarded_ports ?? []);
55
+ const building = $derived(instance?.status === 'creating');
56
+
57
+ async function portAction(run: () => Promise<unknown>): Promise<boolean> {
58
+ portError = null;
59
+ try {
60
+ await run();
61
+ return true;
62
+ } catch (err) {
63
+ portError = (err as Error).message;
64
+ return false;
65
+ }
66
+ }
67
+
68
+ async function addPort(e: Event) {
69
+ e.preventDefault();
70
+ const port = Number.parseInt(newPort, 10);
71
+ // Mirror the server's range check so an obviously-bad port is rejected without a round-trip.
72
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
73
+ portError = 'Enter a port number between 1 and 65535';
74
+ return;
75
+ }
76
+ const ok = await portAction(() => apiPost(`/api/instances/${id}/ports`, { port }));
77
+ if (ok) {
78
+ newPort = '';
79
+ pendingRebuild = true;
80
+ }
81
+ }
82
+
83
+ async function removePort(port: number) {
84
+ const ok = await portAction(() => apiDelete(`/api/instances/${id}/ports/${port}`));
85
+ if (ok) pendingRebuild = true;
86
+ }
87
+
88
+ async function restart() {
89
+ const ok = await portAction(() => apiPost(`/api/instances/${id}/rebuild`));
90
+ if (ok) pendingRebuild = false;
91
+ }
92
+
93
+ let restartError = $state<string | null>(null);
94
+ // Covers the click until the instance reaches `creating`, where `building` takes over.
95
+ let restarting = $state(false);
96
+
97
+ const restartOpts: MochiEnhanceOptions<Record<string, never>, { error: string }> = {
98
+ onPending: (v) => (restarting = v),
99
+ submit: () => {
100
+ restartError = null;
101
+ return ({ result }) => {
102
+ if (result.type === 'failure') {
103
+ restartError = result.data?.error ?? 'Restart failed';
104
+ } else if (result.type === 'error') {
105
+ restartError = 'Network error. Try again.';
106
+ }
107
+ };
108
+ }
109
+ };
110
+
111
+ let copied = $state(false);
112
+ let copyTimer: ReturnType<typeof setTimeout> | undefined;
113
+
114
+ async function copyLogs() {
115
+ await navigator.clipboard.writeText(logs);
116
+ copied = true;
117
+ clearTimeout(copyTimer);
118
+ copyTimer = setTimeout(() => (copied = false), 2000);
119
+ }
120
+ </script>
121
+
122
+ <header class="topbar">
123
+ <a class="back" href="/"><ArrowLeft size={15} /> All instances</a>
124
+ <div class="title">
125
+ <span class="name">{instance?.name ?? 'Instance'}</span>
126
+ {#if instance}
127
+ <StatusBadge status={instance.status} />
128
+ {:else}
129
+ <Skeleton variant="pill" />
130
+ {/if}
131
+ </div>
132
+ {#if instance?.status === 'running'}
133
+ <form method="POST" action="?/restart" {@attach enhance(restartOpts)}>
134
+ <button class="restart" type="submit" disabled={restarting || building}>
135
+ {restarting || building ? 'Restarting…' : 'Restart container'}
136
+ </button>
137
+ </form>
138
+ <a class="open" href={url} target="_blank" rel="noopener"
139
+ >Open in new tab <ArrowUpRight size={15} /></a
140
+ >
141
+ {/if}
142
+ </header>
143
+ {#if restartError}<p class="restart-err">{restartError}</p>{/if}
144
+
145
+ <main class="stage">
146
+ <div class="meta">
147
+ <span class="k">Source</span>
148
+ {#if instance}<code>{instance.source_path}</code>{:else}<Skeleton variant="wide" />{/if}
149
+
150
+ <span class="k">Image</span>
151
+ {#if instance}
152
+ {#if instance.image_source === 'local'}
153
+ <code>local (project devcontainer.json)</code>
154
+ {:else if instance.image_source}
155
+ <code>{instance.image_source}</code>
156
+ {:else}
157
+ <code class="muted">—</code>
158
+ {/if}
159
+ {:else}
160
+ <Skeleton variant="wide" />
161
+ {/if}
162
+ </div>
163
+
164
+ <div class="healthslot">
165
+ <!-- Treat "instance not yet loaded from the stream" as active so the health
166
+ panel shows the full skeleton (all expected rows) while loading, rather
167
+ than the inactive 2-row fallback. Only a loaded, non-running instance is
168
+ genuinely inactive. -->
169
+ <HealthBox
170
+ {health}
171
+ {lastFetchedAt}
172
+ {injectionChecks}
173
+ active={!instance || instance.status === 'running'}
174
+ />
175
+ </div>
176
+
177
+ <section class="ports panel">
178
+ <div class="ports-bar panel-bar">
179
+ <span>Forwarded ports</span>
180
+ {#if pendingRebuild}
181
+ <button class="rebuild" onclick={restart} disabled={building}>
182
+ {building ? 'Restarting…' : 'Restart to apply'}
183
+ </button>
184
+ {/if}
185
+ </div>
186
+ <div class="ports-body">
187
+ {#if forwards.length}
188
+ <ul class="port-list">
189
+ {#each forwards as f (f.container_port)}
190
+ <li>
191
+ <span class="cp">:{f.container_port}</span>
192
+ <span class="arr">→</span>
193
+ <a href={forwardedPortUrl(f.host_port)} target="_blank" rel="noopener"
194
+ >{forwardedPortUrl(f.host_port).replace('http://', '')}
195
+ <ArrowUpRight size={12} /></a
196
+ >
197
+ <button class="rm" title="Remove" onclick={() => removePort(f.container_port)}
198
+ >×</button
199
+ >
200
+ </li>
201
+ {/each}
202
+ </ul>
203
+ {:else}
204
+ <p class="empty">No ports forwarded yet.</p>
205
+ {/if}
206
+
207
+ <form class="add" onsubmit={addPort}>
208
+ <input
209
+ type="number"
210
+ min="1"
211
+ max="65535"
212
+ placeholder="container port (e.g. 3000)"
213
+ bind:value={newPort}
214
+ spellcheck="false"
215
+ />
216
+ <button type="submit">Add</button>
217
+ </form>
218
+
219
+ {#if portError}<p class="port-err">{portError}</p>{/if}
220
+ {#if pendingRebuild}
221
+ <p class="hint pending">
222
+ Port changes apply when you <strong>Restart</strong> (recreates the container).
223
+ </p>
224
+ {/if}
225
+ <p class="hint">
226
+ Your app must bind to <code>0.0.0.0</code> inside the container to be reachable.
227
+ </p>
228
+ </div>
229
+ </section>
230
+
231
+ <div class="logwrap panel">
232
+ <div class="log-bar panel-bar">
233
+ <span>Boot log</span>
234
+ <button class="copy" onclick={copyLogs} disabled={!logs}>
235
+ {copied ? 'Copied!' : 'Copy logs'}
236
+ </button>
237
+ </div>
238
+ <div class="logs" {@attach autoscroll}>
239
+ <pre>{logs || 'Waiting for output…'}<span class="caret"></span></pre>
240
+ </div>
241
+ {#if instance?.status === 'error' && instance.error}
242
+ <div class="err">{instance.error}</div>
243
+ {/if}
244
+ </div>
245
+ </main>
246
+
247
+ <style>
248
+ .topbar {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 18px;
252
+ padding: 16px 24px;
253
+ border-bottom: 1px solid var(--rule);
254
+ }
255
+ .back {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 5px;
259
+ font-family: var(--font-mono);
260
+ text-transform: uppercase;
261
+ letter-spacing: 0.06em;
262
+ color: var(--ink-soft);
263
+ text-decoration: none;
264
+ font-size: 12px;
265
+ }
266
+ .back:hover {
267
+ color: var(--ink);
268
+ }
269
+ .title {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 12px;
273
+ flex: 1;
274
+ }
275
+ .name {
276
+ font-family: var(--font-display);
277
+ font-weight: 700;
278
+ font-size: 20px;
279
+ text-transform: uppercase;
280
+ letter-spacing: 0.03em;
281
+ }
282
+ .open {
283
+ display: inline-flex;
284
+ align-items: center;
285
+ gap: 6px;
286
+ font-family: var(--font-mono);
287
+ font-weight: 600;
288
+ font-size: 12px;
289
+ text-transform: uppercase;
290
+ letter-spacing: 0.06em;
291
+ color: var(--ink);
292
+ text-decoration: none;
293
+ border: 1px solid var(--ink);
294
+ padding: 7px 12px;
295
+ }
296
+ .open:hover {
297
+ background: var(--ink);
298
+ color: var(--bg);
299
+ }
300
+ .restart {
301
+ display: inline-flex;
302
+ align-items: center;
303
+ gap: 6px;
304
+ font-family: var(--font-mono);
305
+ font-weight: 600;
306
+ font-size: 12px;
307
+ text-transform: uppercase;
308
+ letter-spacing: 0.06em;
309
+ color: var(--ink);
310
+ background: var(--bg);
311
+ border: 1px solid var(--ink);
312
+ padding: 7px 12px;
313
+ cursor: pointer;
314
+ }
315
+ .restart:hover:not(:disabled) {
316
+ background: var(--ink);
317
+ color: var(--bg);
318
+ }
319
+ .restart:disabled {
320
+ opacity: 0.5;
321
+ cursor: not-allowed;
322
+ }
323
+ .restart-err {
324
+ margin: 0;
325
+ padding: 8px 24px 0;
326
+ font-family: var(--font-mono);
327
+ font-size: 12px;
328
+ color: var(--danger);
329
+ }
330
+ .stage {
331
+ max-width: 1200px;
332
+ margin: 0 auto;
333
+ padding: 20px 24px 40px;
334
+ }
335
+ .meta {
336
+ display: grid;
337
+ grid-template-columns: max-content 1fr;
338
+ gap: 6px 14px;
339
+ align-items: center;
340
+ margin-bottom: 18px;
341
+ }
342
+ .meta .k {
343
+ font-family: var(--font-display);
344
+ font-weight: 700;
345
+ font-size: 12px;
346
+ letter-spacing: 0.14em;
347
+ text-transform: uppercase;
348
+ color: var(--ink-faint);
349
+ }
350
+ .meta code {
351
+ font-family: var(--font-mono);
352
+ font-size: 13px;
353
+ color: var(--ink-soft);
354
+ }
355
+ .meta code.muted {
356
+ color: var(--ink-faint);
357
+ }
358
+ .healthslot {
359
+ margin-bottom: 18px;
360
+ }
361
+ .ports {
362
+ margin-bottom: 18px;
363
+ }
364
+ .ports-bar {
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: space-between;
368
+ }
369
+ .rebuild {
370
+ font-family: var(--font-mono);
371
+ font-weight: 600;
372
+ font-size: 10px;
373
+ text-transform: uppercase;
374
+ letter-spacing: 0.08em;
375
+ padding: 4px 9px;
376
+ background: var(--bg);
377
+ color: var(--ink);
378
+ border: 1px solid var(--bg);
379
+ cursor: pointer;
380
+ }
381
+ .rebuild:hover:not(:disabled) {
382
+ background: transparent;
383
+ color: var(--bg);
384
+ }
385
+ .rebuild:disabled {
386
+ opacity: 0.5;
387
+ cursor: not-allowed;
388
+ }
389
+ .ports-body {
390
+ padding: 12px 14px 14px;
391
+ background: var(--bg-card);
392
+ }
393
+ .port-list {
394
+ list-style: none;
395
+ margin: 0 0 12px;
396
+ padding: 0;
397
+ display: flex;
398
+ flex-direction: column;
399
+ gap: 6px;
400
+ }
401
+ .port-list li {
402
+ display: flex;
403
+ align-items: center;
404
+ gap: 8px;
405
+ font-family: var(--font-mono);
406
+ font-size: 13px;
407
+ }
408
+ .port-list .cp {
409
+ font-weight: 600;
410
+ color: var(--ink);
411
+ }
412
+ .port-list .arr {
413
+ color: var(--ink-faint);
414
+ }
415
+ .port-list a {
416
+ display: inline-flex;
417
+ align-items: center;
418
+ gap: 4px;
419
+ color: var(--ink);
420
+ text-decoration: none;
421
+ border-bottom: 1px solid var(--ink-faint);
422
+ }
423
+ .port-list a:hover {
424
+ border-bottom-color: var(--ink);
425
+ }
426
+ .rm {
427
+ margin-left: auto;
428
+ width: 20px;
429
+ height: 20px;
430
+ line-height: 1;
431
+ font-size: 15px;
432
+ border: 1px solid var(--ink-faint);
433
+ background: transparent;
434
+ color: var(--ink-soft);
435
+ cursor: pointer;
436
+ }
437
+ .rm:hover {
438
+ border-color: var(--danger);
439
+ color: var(--danger);
440
+ }
441
+ .empty {
442
+ margin: 0 0 12px;
443
+ font-family: var(--font-mono);
444
+ font-size: 12px;
445
+ color: var(--ink-faint);
446
+ }
447
+ .add {
448
+ display: flex;
449
+ gap: 8px;
450
+ }
451
+ .add input {
452
+ flex: 1;
453
+ min-width: 0;
454
+ font-family: var(--font-mono);
455
+ font-size: 13px;
456
+ padding: 6px 9px;
457
+ border: 1px solid var(--ink);
458
+ background: var(--bg);
459
+ color: var(--ink);
460
+ outline: none;
461
+ }
462
+ .add button {
463
+ font-family: var(--font-mono);
464
+ font-weight: 600;
465
+ font-size: 12px;
466
+ text-transform: uppercase;
467
+ letter-spacing: 0.06em;
468
+ padding: 6px 14px;
469
+ border: 1px solid var(--ink);
470
+ background: var(--bg-card);
471
+ color: var(--ink);
472
+ cursor: pointer;
473
+ }
474
+ .add button:hover {
475
+ background: var(--ink);
476
+ color: var(--bg);
477
+ }
478
+ .port-err {
479
+ margin: 10px 0 0;
480
+ font-family: var(--font-mono);
481
+ font-size: 12px;
482
+ color: var(--danger);
483
+ }
484
+ .hint {
485
+ margin: 10px 0 0;
486
+ font-family: var(--font-mono);
487
+ font-size: 11px;
488
+ color: var(--ink-faint);
489
+ }
490
+ .hint.pending {
491
+ color: var(--ink-soft);
492
+ }
493
+ .hint code {
494
+ color: var(--ink);
495
+ }
496
+ .logwrap {
497
+ overflow: hidden;
498
+ }
499
+ .log-bar {
500
+ display: flex;
501
+ align-items: center;
502
+ justify-content: space-between;
503
+ }
504
+ .copy {
505
+ font-family: var(--font-mono);
506
+ font-weight: 600;
507
+ font-size: 10px;
508
+ text-transform: uppercase;
509
+ letter-spacing: 0.08em;
510
+ padding: 4px 9px;
511
+ background: var(--bg);
512
+ color: var(--ink);
513
+ border: 1px solid var(--bg);
514
+ cursor: pointer;
515
+ }
516
+ .copy:hover:not(:disabled) {
517
+ background: transparent;
518
+ color: var(--bg);
519
+ }
520
+ .copy:disabled {
521
+ opacity: 0.5;
522
+ cursor: not-allowed;
523
+ }
524
+ .logs {
525
+ position: relative;
526
+ background:
527
+ repeating-linear-gradient(0deg, var(--screen-line) 0 1px, transparent 1px 3px),
528
+ var(--screen-bg);
529
+ color: var(--screen-ink);
530
+ padding: 14px;
531
+ height: calc(100vh - 320px);
532
+ min-height: 360px;
533
+ overflow: auto;
534
+ }
535
+ .logs pre {
536
+ margin: 0;
537
+ font-family: var(--font-mono);
538
+ font-size: 12.5px;
539
+ line-height: 1.5;
540
+ white-space: pre-wrap;
541
+ word-break: break-word;
542
+ }
543
+ /* Duplicated from StatusBadge because Svelte scopes `@keyframes` per component,
544
+ so a keyframe defined elsewhere never resolves here. */
545
+ @keyframes lcd-blink {
546
+ 50% {
547
+ background: transparent;
548
+ }
549
+ }
550
+ .caret {
551
+ display: inline-block;
552
+ width: 0.6em;
553
+ height: 1.05em;
554
+ margin-left: 2px;
555
+ background: var(--screen-ink);
556
+ vertical-align: text-bottom;
557
+ animation: lcd-blink 1.05s steps(1) infinite;
558
+ }
559
+ .err {
560
+ padding: 12px 14px;
561
+ background: var(--bg-card);
562
+ color: var(--ink);
563
+ border-top: 2px solid var(--ink);
564
+ font-family: var(--font-mono);
565
+ font-size: 13px;
566
+ }
567
+ </style>
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ import Cable from '@lucide/svelte/icons/cable';
3
+ import { forwardedPortUrl } from '../lib/links.ts';
4
+ import type { PortForward } from '../types.ts';
5
+
6
+ let { ports }: { ports: PortForward[] } = $props();
7
+ </script>
8
+
9
+ {#if ports.length}
10
+ <div class="fports" title="Forwarded ports — click to open">
11
+ <Cable size={12} />
12
+ <span class="list">
13
+ {#each ports as f (f.container_port)}
14
+ <a
15
+ class="fport"
16
+ class:open={f.open}
17
+ href={forwardedPortUrl(f.host_port)}
18
+ target="_blank"
19
+ rel="noopener"
20
+ title={`container :${f.container_port} → ${forwardedPortUrl(f.host_port)} ${f.open ? '(open)' : '(not published yet)'}`}
21
+ >{f.container_port}</a
22
+ >
23
+ {/each}
24
+ </span>
25
+ </div>
26
+ {/if}
27
+
28
+ <style>
29
+ .fports {
30
+ margin-top: 6px;
31
+ display: inline-flex;
32
+ align-items: center;
33
+ gap: 5px;
34
+ max-width: 100%;
35
+ font-family: var(--font-mono);
36
+ font-size: 12px;
37
+ color: var(--ink);
38
+ border: 1px solid var(--ink-faint);
39
+ padding: 2px 7px;
40
+ }
41
+ .fports :global(svg) {
42
+ flex: none;
43
+ }
44
+ /* Flex list drops the source whitespace between links, so commas sit flush. */
45
+ .list {
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ min-width: 0;
49
+ }
50
+ /* Hollow + dimmed = configured but not yet published by the live container. */
51
+ .fport {
52
+ color: var(--ink-soft);
53
+ text-decoration: none;
54
+ }
55
+ .fport::before {
56
+ content: '○\00a0';
57
+ color: var(--ink-faint);
58
+ }
59
+ .fport.open {
60
+ color: var(--ink);
61
+ }
62
+ .fport.open::before {
63
+ content: '●\00a0';
64
+ color: var(--ink);
65
+ }
66
+ .fport:hover {
67
+ text-decoration: underline;
68
+ }
69
+ .fport:not(:last-child)::after {
70
+ content: ',';
71
+ color: var(--ink-faint);
72
+ margin-right: 4px;
73
+ }
74
+ </style>
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import Settings from '@lucide/svelte/icons/settings';
3
+ </script>
4
+
5
+ <a class="cog" href="/settings" aria-label="Settings" title="Settings"><Settings size={16} /></a>
6
+
7
+ <style>
8
+ .cog {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ width: 32px;
13
+ height: 32px;
14
+ border: 1px solid var(--ink);
15
+ background: var(--bg-card);
16
+ color: var(--ink);
17
+ }
18
+ .cog:hover {
19
+ background: var(--ink);
20
+ color: var(--bg);
21
+ }
22
+ </style>