@meshxdata/fops 0.1.44 → 0.1.46

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 (36) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/package.json +1 -1
  3. package/src/commands/lifecycle.js +101 -5
  4. package/src/commands/setup.js +45 -4
  5. package/src/plugins/bundled/fops-plugin-azure/index.js +29 -0
  6. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-core.js +1185 -0
  7. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-flux.js +1180 -0
  8. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-ingress.js +393 -0
  9. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-naming.js +104 -0
  10. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-network.js +296 -0
  11. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-postgres.js +768 -0
  12. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-reconcilers.js +538 -0
  13. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-secrets.js +849 -0
  14. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-stacks.js +643 -0
  15. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-state.js +145 -0
  16. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-storage.js +496 -0
  17. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks-terraform.js +1032 -0
  18. package/src/plugins/bundled/fops-plugin-azure/lib/azure-aks.js +155 -4245
  19. package/src/plugins/bundled/fops-plugin-azure/lib/azure-keyvault.js +186 -0
  20. package/src/plugins/bundled/fops-plugin-azure/lib/azure-ops.js +29 -0
  21. package/src/plugins/bundled/fops-plugin-azure/lib/azure-results.js +78 -0
  22. package/src/plugins/bundled/fops-plugin-azure/lib/azure.js +1 -1
  23. package/src/plugins/bundled/fops-plugin-azure/lib/commands/infra-cmds.js +758 -0
  24. package/src/plugins/bundled/fops-plugin-azure/lib/commands/registry-cmds.js +250 -0
  25. package/src/plugins/bundled/fops-plugin-azure/lib/commands/test-cmds.js +52 -1
  26. package/src/plugins/bundled/fops-plugin-azure/lib/commands/vm-cmds.js +10 -0
  27. package/src/plugins/bundled/fops-plugin-foundation/lib/apply.js +3 -2
  28. package/src/plugins/bundled/fops-plugin-foundation/lib/helpers.js +21 -0
  29. package/src/plugins/bundled/fops-plugin-foundation/lib/tools-read.js +3 -5
  30. package/src/ui/tui/App.js +13 -13
  31. package/src/web/dist/assets/index-NXC8Hvnp.css +1 -0
  32. package/src/web/dist/assets/index-QH1N4ejK.js +112 -0
  33. package/src/web/dist/index.html +2 -2
  34. package/src/web/server.js +4 -4
  35. package/src/web/dist/assets/index-BphVaAUd.css +0 -1
  36. package/src/web/dist/assets/index-CSckLzuG.js +0 -129
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Registry commands: login, push, pull for AKS-hosted OCI registry via cloudflared.
3
+ */
4
+ import chalk from "chalk";
5
+ import { spawn, execSync } from "child_process";
6
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+
10
+ const CLOUDFLARED_TOKEN_DIR = join(homedir(), ".fops", "registry");
11
+ const DOCKER_CONFIG_PATH = join(homedir(), ".docker", "config.json");
12
+
13
+ export function registerRegistryCommands(azure) {
14
+ const registry = azure
15
+ .command("registry")
16
+ .description("Push images to AKS-hosted OCI registry via Cloudflare Access");
17
+
18
+ registry
19
+ .command("login")
20
+ .description("Authenticate to the registry via Cloudflare Access")
21
+ .option("--cluster <name>", "Cluster name (default: current context)")
22
+ .option("--domain <url>", "Registry domain", "demo.meshx.app")
23
+ .action(async (opts) => {
24
+ await registryLogin(opts);
25
+ });
26
+
27
+ registry
28
+ .command("push <image>")
29
+ .description("Push an image to the AKS registry")
30
+ .option("--cluster <name>", "Cluster name (default: current context)")
31
+ .option("--domain <url>", "Registry domain", "demo.meshx.app")
32
+ .option("--tag <tag>", "Additional tag to apply")
33
+ .action(async (image, opts) => {
34
+ await registryPush(image, opts);
35
+ });
36
+
37
+ registry
38
+ .command("pull <image>")
39
+ .description("Pull an image from the AKS registry")
40
+ .option("--cluster <name>", "Cluster name (default: current context)")
41
+ .option("--domain <url>", "Registry domain", "demo.meshx.app")
42
+ .action(async (image, opts) => {
43
+ await registryPull(image, opts);
44
+ });
45
+
46
+ registry
47
+ .command("logout")
48
+ .description("Remove registry credentials")
49
+ .option("--domain <url>", "Registry domain", "demo.meshx.app")
50
+ .action(async (opts) => {
51
+ await registryLogout(opts);
52
+ });
53
+ }
54
+
55
+ async function registryLogin(opts) {
56
+ const domain = opts.domain;
57
+ const registryUrl = `https://${domain}/registry/`;
58
+
59
+ console.log(chalk.blue("→ Authenticating to registry via Cloudflare Access..."));
60
+
61
+ // Check if cloudflared is installed
62
+ try {
63
+ execSync("which cloudflared", { stdio: "ignore" });
64
+ } catch {
65
+ console.error(chalk.red("✗ cloudflared not found. Install it:"));
66
+ console.error(chalk.gray(" brew install cloudflared"));
67
+ console.error(chalk.gray(" # or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"));
68
+ process.exit(1);
69
+ }
70
+
71
+ // Authenticate with Cloudflare Access
72
+ console.log(chalk.gray(` Opening browser for ${domain}...`));
73
+
74
+ try {
75
+ execSync(`cloudflared access login ${registryUrl}`, {
76
+ stdio: "inherit",
77
+ timeout: 120000
78
+ });
79
+ } catch (err) {
80
+ console.error(chalk.red("✗ Cloudflare Access login failed"));
81
+ process.exit(1);
82
+ }
83
+
84
+ // Get the token from cloudflared
85
+ let token;
86
+ try {
87
+ token = execSync(`cloudflared access token -app=${registryUrl}`, {
88
+ encoding: "utf-8"
89
+ }).trim();
90
+ } catch (err) {
91
+ console.error(chalk.red("✗ Failed to get access token"));
92
+ process.exit(1);
93
+ }
94
+
95
+ // Store token for later use
96
+ if (!existsSync(CLOUDFLARED_TOKEN_DIR)) {
97
+ mkdirSync(CLOUDFLARED_TOKEN_DIR, { recursive: true });
98
+ }
99
+ writeFileSync(join(CLOUDFLARED_TOKEN_DIR, `${domain}.token`), token, { mode: 0o600 });
100
+
101
+ // Configure docker to use the token
102
+ await configureDockerAuth(domain, token);
103
+
104
+ console.log(chalk.green(`✓ Logged in to ${domain}/registry`));
105
+ console.log(chalk.gray(` Token stored in ~/.fops/registry/`));
106
+ console.log(chalk.gray(` Docker configured for ${domain}`));
107
+ }
108
+
109
+ async function configureDockerAuth(domain, token) {
110
+ const registryHost = `${domain}/registry`;
111
+
112
+ let dockerConfig = { auths: {} };
113
+ if (existsSync(DOCKER_CONFIG_PATH)) {
114
+ try {
115
+ dockerConfig = JSON.parse(readFileSync(DOCKER_CONFIG_PATH, "utf-8"));
116
+ if (!dockerConfig.auths) dockerConfig.auths = {};
117
+ } catch {
118
+ dockerConfig = { auths: {} };
119
+ }
120
+ }
121
+
122
+ // CF Access token goes in Authorization header, but docker uses base64 auth
123
+ // We'll use a credential helper approach or encode it
124
+ const auth = Buffer.from(`cf-access-token:${token}`).toString("base64");
125
+
126
+ dockerConfig.auths[registryHost] = { auth };
127
+
128
+ const configDir = join(homedir(), ".docker");
129
+ if (!existsSync(configDir)) {
130
+ mkdirSync(configDir, { recursive: true });
131
+ }
132
+ writeFileSync(DOCKER_CONFIG_PATH, JSON.stringify(dockerConfig, null, 2), { mode: 0o600 });
133
+ }
134
+
135
+ async function registryPush(image, opts) {
136
+ const domain = opts.domain;
137
+ const registryHost = `${domain}/registry`;
138
+
139
+ // Check if logged in
140
+ const tokenPath = join(CLOUDFLARED_TOKEN_DIR, `${domain}.token`);
141
+ if (!existsSync(tokenPath)) {
142
+ console.error(chalk.red("✗ Not logged in. Run: fops azure registry login"));
143
+ process.exit(1);
144
+ }
145
+
146
+ // Refresh token if needed
147
+ const token = readFileSync(tokenPath, "utf-8").trim();
148
+ await configureDockerAuth(domain, token);
149
+
150
+ // Determine source and target image
151
+ const srcImage = image;
152
+ let targetImage;
153
+
154
+ if (image.includes("/")) {
155
+ // Image already has a path, just prepend registry
156
+ const imageParts = image.split("/");
157
+ const nameTag = imageParts.pop();
158
+ targetImage = `${registryHost}/${nameTag}`;
159
+ } else {
160
+ targetImage = `${registryHost}/${image}`;
161
+ }
162
+
163
+ // Add branch tag if specified
164
+ if (opts.tag) {
165
+ const [name] = targetImage.split(":");
166
+ targetImage = `${name}:${opts.tag}`;
167
+ }
168
+
169
+ console.log(chalk.blue(`→ Tagging ${srcImage} as ${targetImage}...`));
170
+
171
+ try {
172
+ execSync(`docker tag ${srcImage} ${targetImage}`, { stdio: "inherit" });
173
+ } catch {
174
+ console.error(chalk.red("✗ Failed to tag image"));
175
+ process.exit(1);
176
+ }
177
+
178
+ console.log(chalk.blue(`→ Pushing ${targetImage}...`));
179
+
180
+ // Start cloudflared access tcp tunnel for the push
181
+ const tunnel = spawn("cloudflared", [
182
+ "access", "tcp",
183
+ "--hostname", `${domain}`,
184
+ "--url", "localhost:5000"
185
+ ], { stdio: "pipe" });
186
+
187
+ // Wait for tunnel to be ready
188
+ await new Promise(resolve => setTimeout(resolve, 2000));
189
+
190
+ try {
191
+ execSync(`docker push ${targetImage}`, { stdio: "inherit" });
192
+ console.log(chalk.green(`✓ Pushed ${targetImage}`));
193
+ } catch (err) {
194
+ console.error(chalk.red("✗ Push failed"));
195
+ tunnel.kill();
196
+ process.exit(1);
197
+ }
198
+
199
+ tunnel.kill();
200
+ }
201
+
202
+ async function registryPull(image, opts) {
203
+ const domain = opts.domain;
204
+ const registryHost = `${domain}/registry`;
205
+
206
+ // Check if logged in
207
+ const tokenPath = join(CLOUDFLARED_TOKEN_DIR, `${domain}.token`);
208
+ if (!existsSync(tokenPath)) {
209
+ console.error(chalk.red("✗ Not logged in. Run: fops azure registry login"));
210
+ process.exit(1);
211
+ }
212
+
213
+ const token = readFileSync(tokenPath, "utf-8").trim();
214
+ await configureDockerAuth(domain, token);
215
+
216
+ const fullImage = image.startsWith(registryHost) ? image : `${registryHost}/${image}`;
217
+
218
+ console.log(chalk.blue(`→ Pulling ${fullImage}...`));
219
+
220
+ try {
221
+ execSync(`docker pull ${fullImage}`, { stdio: "inherit" });
222
+ console.log(chalk.green(`✓ Pulled ${fullImage}`));
223
+ } catch {
224
+ console.error(chalk.red("✗ Pull failed"));
225
+ process.exit(1);
226
+ }
227
+ }
228
+
229
+ async function registryLogout(opts) {
230
+ const domain = opts.domain;
231
+ const tokenPath = join(CLOUDFLARED_TOKEN_DIR, `${domain}.token`);
232
+
233
+ if (existsSync(tokenPath)) {
234
+ const { unlinkSync } = await import("fs");
235
+ unlinkSync(tokenPath);
236
+ }
237
+
238
+ // Remove from docker config
239
+ if (existsSync(DOCKER_CONFIG_PATH)) {
240
+ try {
241
+ const dockerConfig = JSON.parse(readFileSync(DOCKER_CONFIG_PATH, "utf-8"));
242
+ delete dockerConfig.auths?.[`${domain}/registry`];
243
+ writeFileSync(DOCKER_CONFIG_PATH, JSON.stringify(dockerConfig, null, 2));
244
+ } catch {
245
+ // Ignore errors
246
+ }
247
+ }
248
+
249
+ console.log(chalk.green(`✓ Logged out from ${domain}/registry`));
250
+ }
@@ -56,6 +56,39 @@ export function registerTestCommands(azure) {
56
56
  });
57
57
  let { bearerToken, qaUser, qaPass, useTokenMode } = auth;
58
58
 
59
+ // Fetch CF Access service token (needed to bypass Cloudflare Access)
60
+ // Priority: local .env file → process.env → remote VM .env
61
+ let cfAccessClientId = "";
62
+ let cfAccessClientSecret = "";
63
+ const localEnvPath = path.join(root, ".env");
64
+ try {
65
+ const localEnv = await fsp.readFile(localEnvPath, "utf8");
66
+ for (const line of localEnv.split("\n")) {
67
+ const m = line.match(/^CF_ACCESS_CLIENT_(ID|SECRET)=(.+)$/);
68
+ if (m?.[1] === "ID") cfAccessClientId = m[2].trim().replace(/^["']|["']$/g, "");
69
+ if (m?.[1] === "SECRET") cfAccessClientSecret = m[2].trim().replace(/^["']|["']$/g, "");
70
+ }
71
+ } catch { /* no local .env */ }
72
+ if (!cfAccessClientId) {
73
+ cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || "";
74
+ cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || "";
75
+ }
76
+ if (!cfAccessClientId && ip) {
77
+ try {
78
+ const sshUser = state?.adminUser || "azureuser";
79
+ const { stdout: cfOut } = await sshCmd(execa, ip, sshUser,
80
+ "grep -E '^CF_ACCESS_CLIENT_(ID|SECRET)=' /opt/foundation-compose/.env",
81
+ 10_000,
82
+ );
83
+ for (const line of (cfOut || "").split("\n")) {
84
+ const m = line.match(/^CF_ACCESS_CLIENT_(ID|SECRET)=(.+)$/);
85
+ if (m?.[1] === "ID") cfAccessClientId = m[2].trim();
86
+ if (m?.[1] === "SECRET") cfAccessClientSecret = m[2].trim();
87
+ }
88
+ if (cfAccessClientId) console.log(chalk.green(" ✓ Got CF Access service token from VM"));
89
+ } catch { /* optional — tests still run without it */ }
90
+ }
91
+
59
92
  if (!bearerToken && !qaUser) {
60
93
  console.error(chalk.red("\n No credentials found (local or remote)."));
61
94
  console.error(chalk.dim(" Set BEARER_TOKEN or QA_USERNAME/QA_PASSWORD, or ensure the VM has Auth0 configured in .env\n"));
@@ -94,6 +127,10 @@ export function registerTestCommands(azure) {
94
127
  envContent = setVar(envContent, "BEARER_TOKEN", bearerToken);
95
128
  envContent = setVar(envContent, "TOKEN_AUTH0", bearerToken);
96
129
  }
130
+ if (cfAccessClientId) {
131
+ envContent = setVar(envContent, "CF_ACCESS_CLIENT_ID", cfAccessClientId);
132
+ envContent = setVar(envContent, "CF_ACCESS_CLIENT_SECRET", cfAccessClientSecret);
133
+ }
97
134
 
98
135
  await fsp.writeFile(envPath, envContent);
99
136
  console.log(chalk.green(` ✓ Configured QA .env → ${apiUrl}`));
@@ -155,6 +192,10 @@ export function registerTestCommands(azure) {
155
192
  testEnv.BEARER_TOKEN = bearerToken;
156
193
  testEnv.TOKEN_AUTH0 = bearerToken;
157
194
  }
195
+ if (cfAccessClientId) {
196
+ testEnv.CF_ACCESS_CLIENT_ID = cfAccessClientId;
197
+ testEnv.CF_ACCESS_CLIENT_SECRET = cfAccessClientSecret;
198
+ }
158
199
 
159
200
  const startMs = Date.now();
160
201
  const proc = execa(
@@ -231,9 +272,10 @@ export function registerTestCommands(azure) {
231
272
  .command("show <target>")
232
273
  .description("Show details of the latest test result for a VM/cluster")
233
274
  .option("--run <id>", "Show a specific run by timestamp")
275
+ .option("--json", "Output raw JSON result")
234
276
  .action(async (target, opts) => {
235
277
  const { resultsShow } = await import("../azure-results.js");
236
- await resultsShow({ target, run: opts.run });
278
+ await resultsShow({ target, run: opts.run, json: opts.json });
237
279
  });
238
280
 
239
281
  test
@@ -245,6 +287,15 @@ export function registerTestCommands(azure) {
245
287
  await resultsCompare({ target, last: parseInt(opts.last) });
246
288
  });
247
289
 
290
+ test
291
+ .command("rm <target>")
292
+ .description("Remove all stored test results for a VM (local state + blob storage)")
293
+ .option("--profile <subscription>", "Azure subscription name or ID")
294
+ .action(async (target, opts) => {
295
+ const { resultsRemove } = await import("../azure-results.js");
296
+ await resultsRemove(target, { subscription: opts.profile });
297
+ });
298
+
248
299
  test
249
300
  .command("push [target]")
250
301
  .description("Push local QA results to blob storage (default: all VMs with results)")
@@ -875,6 +875,16 @@ export function registerVmCommands(azure, api, registry) {
875
875
  await azureLogs(service, { vmName: opts.vmName || name });
876
876
  });
877
877
 
878
+ // ── restart ─────────────────────────────────────────────────────────────────
879
+ azure
880
+ .command("restart [name] [service]")
881
+ .description("Restart Foundation services on the VM (all or specific)")
882
+ .option("--vm-name <name>", "Target VM (default: active VM)")
883
+ .action(async (name, service, opts) => {
884
+ const { azureRestart } = await import("../azure.js");
885
+ await azureRestart(service, { vmName: opts.vmName || name });
886
+ });
887
+
878
888
  // ── context ───────────────────────────────────────────────────────────────
879
889
  azure
880
890
  .command("context [name]")
@@ -7,6 +7,7 @@ import { getTemplate, resolveBuilder, resolveSchema, buildPipeline, normalizeSch
7
7
  import { fetchEntityColumns, validateTransformColumnFlow, runComposeCmd, findComposeRoot } from "./tools-write.js";
8
8
  import { StorageClient } from "./storage.js";
9
9
  import { parseListResponse } from "./api-spec.js";
10
+ import { mapSettled } from "./helpers.js";
10
11
 
11
12
  async function ensureExpectations(client, uuid) {
12
13
  try {
@@ -1278,8 +1279,8 @@ export async function applyLandscape(client, operations, opts = {}) {
1278
1279
  const start = Date.now();
1279
1280
  while (Date.now() - start < maxWait) {
1280
1281
  try {
1281
- const statuses = await Promise.allSettled(
1282
- sadpUuids.map((uuid) => client.get(`/data/compute?identifier=${uuid}`)),
1282
+ const statuses = await mapSettled(
1283
+ sadpUuids, (uuid) => client.get(`/data/compute?identifier=${uuid}`), 5,
1283
1284
  );
1284
1285
  const stillRunning = statuses.some((r) => {
1285
1286
  if (r.status !== "fulfilled") return false;
@@ -72,6 +72,27 @@ export function projectList(raw, entityType) {
72
72
  };
73
73
  }
74
74
 
75
+ /**
76
+ * Like Promise.allSettled(items.map(fn)) but limits concurrency.
77
+ * At most `limit` calls to `fn` run at the same time.
78
+ */
79
+ export async function mapSettled(items, fn, limit = 5) {
80
+ const results = new Array(items.length);
81
+ let next = 0;
82
+ async function worker() {
83
+ while (next < items.length) {
84
+ const i = next++;
85
+ try {
86
+ results[i] = { status: "fulfilled", value: await fn(items[i], i) };
87
+ } catch (reason) {
88
+ results[i] = { status: "rejected", reason };
89
+ }
90
+ }
91
+ }
92
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
93
+ return results;
94
+ }
95
+
75
96
  // Lazy resolver for the embeddings search service provided by fops-plugin-embeddings
76
97
  export function makeEmbedSearch(api) {
77
98
  let cached = undefined; // undefined → not resolved yet; null → resolved but missing
@@ -1,4 +1,4 @@
1
- import { fmt, qs, makeEmbedSearch, projectEntity, projectList } from "./helpers.js";
1
+ import { fmt, qs, makeEmbedSearch, projectEntity, projectList, mapSettled } from "./helpers.js";
2
2
  import { runComposeCmd, findComposeRoot } from "./tools-write.js";
3
3
 
4
4
  export function registerReadTools(api, client) {
@@ -397,15 +397,13 @@ export function registerReadTools(api, client) {
397
397
  const products = parseListResponse(dpList);
398
398
  if (!products.length) return "No active compute jobs.";
399
399
 
400
- const results = await Promise.allSettled(
401
- products.map(async (p) => {
400
+ const results = await mapSettled(products, async (p) => {
402
401
  const id = p.identifier || p.id;
403
402
  if (!id) return null;
404
403
  const cs = await client.get(`/data/compute${qs({ identifier: id })}`);
405
404
  const s = cs?.status?.status || cs?.status;
406
405
  return { id, name: p.name, suffix: cs?.status?.suffix || "latest", status: typeof s === "object" ? s : { status: s } };
407
- }),
408
- );
406
+ }, 5);
409
407
 
410
408
  const jobs = results
411
409
  .filter((r) => r.status === "fulfilled" && r.value)
package/src/ui/tui/App.js CHANGED
@@ -639,9 +639,9 @@ function TuiApp({ core, version, root }) {
639
639
  return svcs;
640
640
  });
641
641
  };
642
- poll();
643
- const interval = setInterval(poll, 30000);
644
- return () => { cancelled = true; clearInterval(interval); };
642
+ const loop = async () => { while (!cancelled) { await poll(); if (cancelled) break; await new Promise(r => setTimeout(r, 30000)); } };
643
+ loop();
644
+ return () => { cancelled = true; };
645
645
  }, [root]);
646
646
  // While starting stack, poll services every 5s so "Waiting for X" updates quickly
647
647
  useEffect(() => {
@@ -654,9 +654,9 @@ function TuiApp({ core, version, root }) {
654
654
  return svcs;
655
655
  });
656
656
  };
657
- const t = setTimeout(poll, 2000);
658
- const interval = setInterval(poll, 5000);
659
- return () => { cancelled = true; clearTimeout(t); clearInterval(interval); };
657
+ const loop = async () => { await new Promise(r => setTimeout(r, 2000)); while (!cancelled) { await poll(); if (cancelled) break; await new Promise(r => setTimeout(r, 5000)); } };
658
+ loop();
659
+ return () => { cancelled = true; };
660
660
  }, [root, isRunningCmd, statusText]);
661
661
 
662
662
  // Poll Foundation entities every 60s (meshes, data products, data systems, data sources)
@@ -690,12 +690,12 @@ function TuiApp({ core, version, root }) {
690
690
  });
691
691
  } catch {}
692
692
  };
693
- poll();
694
- const interval = setInterval(poll, 60000);
695
- return () => { cancelled = true; clearInterval(interval); };
693
+ const loop = async () => { while (!cancelled) { await poll(); if (cancelled) break; await new Promise(r => setTimeout(r, 60000)); } };
694
+ loop();
695
+ return () => { cancelled = true; };
696
696
  }, [core]);
697
697
 
698
- // Poll compute jobs (active + recent) every 3s for responsive sidebar
698
+ // Poll compute jobs (active + recent) every 15s for responsive sidebar
699
699
  useEffect(() => {
700
700
  let cancelled = false;
701
701
  const poll = async () => {
@@ -745,9 +745,9 @@ function TuiApp({ core, version, root }) {
745
745
  });
746
746
  } catch {}
747
747
  };
748
- poll();
749
- const interval = setInterval(poll, 3000);
750
- return () => { cancelled = true; clearInterval(interval); };
748
+ const loop = async () => { while (!cancelled) { await poll(); if (cancelled) break; await new Promise(r => setTimeout(r, 15000)); } };
749
+ loop();
750
+ return () => { cancelled = true; };
751
751
  }, [core]);
752
752
 
753
753
  // Sync sessions list from core. Accept fewer tabs only when exactly one was removed (user closed tab).
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.top-full{top:100%}.right-2{right:calc(var(--spacing)*2)}.right-4{right:calc(var(--spacing)*4)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-full{bottom:100%}.left-0{left:calc(var(--spacing)*0)}.left-2{left:calc(var(--spacing)*2)}.left-4{left:calc(var(--spacing)*4)}.z-10{z-index:10}.z-50{z-index:50}.mx-1{margin-inline:calc(var(--spacing)*1)}.mx-4{margin-inline:calc(var(--spacing)*4)}.my-0\.5{margin-block:calc(var(--spacing)*.5)}.my-3{margin-block:calc(var(--spacing)*3)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-2\.5{margin-top:calc(var(--spacing)*2.5)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-auto{margin-top:auto}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-0\.5{margin-left:calc(var(--spacing)*.5)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-8{margin-left:calc(var(--spacing)*8)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing)*1.5)}.h-2{height:calc(var(--spacing)*2)}.h-3{height:calc(var(--spacing)*3)}.h-8{height:calc(var(--spacing)*8)}.h-\[2px\]{height:2px}.h-\[5px\]{height:5px}.h-\[6px\]{height:6px}.h-\[14px\]{height:14px}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-20{max-height:calc(var(--spacing)*20)}.max-h-52{max-height:calc(var(--spacing)*52)}.w-1\.5{width:calc(var(--spacing)*1.5)}.w-1\/2{width:50%}.w-8{width:calc(var(--spacing)*8)}.w-12{width:calc(var(--spacing)*12)}.w-60{width:calc(var(--spacing)*60)}.w-\[2px\]{width:2px}.w-\[5px\]{width:5px}.w-\[6px\]{width:6px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[220px\]{min-width:220px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-grab{cursor:grab}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-\[\#1a1a28\]{border-color:#1a1a28}.border-\[\#1a1a28\]\/50{border-color:#1a1a2880}.border-\[\#2e2e40\]{border-color:#2e2e40}.border-\[\#818cf8\]\/20{border-color:#818cf833}.border-\[\#818cf8\]\/30{border-color:#818cf84d}.border-\[\#26263a\]{border-color:#26263a}.border-\[\#f97316\]{border-color:#f97316}.border-\[\#f97316\]\/20{border-color:#f9731633}.border-\[\#fbbf24\]\/20{border-color:#fbbf2433}.border-\[\#fbbf24\]\/25{border-color:#fbbf2440}.border-cyan-400\/30{border-color:#00d2ef4d}@supports (color:color-mix(in lab,red,red)){.border-cyan-400\/30{border-color:color-mix(in oklab,var(--color-cyan-400)30%,transparent)}}.border-emerald-400\/20{border-color:#00d29433}@supports (color:color-mix(in lab,red,red)){.border-emerald-400\/20{border-color:color-mix(in oklab,var(--color-emerald-400)20%,transparent)}}.border-emerald-400\/30{border-color:#00d2944d}@supports (color:color-mix(in lab,red,red)){.border-emerald-400\/30{border-color:color-mix(in oklab,var(--color-emerald-400)30%,transparent)}}.border-purple-500\/20{border-color:#ac4bff33}@supports (color:color-mix(in lab,red,red)){.border-purple-500\/20{border-color:color-mix(in oklab,var(--color-purple-500)20%,transparent)}}.border-red-400\/20{border-color:#ff656833}@supports (color:color-mix(in lab,red,red)){.border-red-400\/20{border-color:color-mix(in oklab,var(--color-red-400)20%,transparent)}}.border-red-400\/30{border-color:#ff65684d}@supports (color:color-mix(in lab,red,red)){.border-red-400\/30{border-color:color-mix(in oklab,var(--color-red-400)30%,transparent)}}.border-red-500\/20{border-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.border-red-500\/20{border-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.border-transparent{border-color:#0000}.border-yellow-400\/30{border-color:#fac8004d}@supports (color:color-mix(in lab,red,red)){.border-yellow-400\/30{border-color:color-mix(in oklab,var(--color-yellow-400)30%,transparent)}}.bg-\[\#0a0a10\]{background-color:#0a0a10}.bg-\[\#0c0c12\]{background-color:#0c0c12}.bg-\[\#0c0c14\]{background-color:#0c0c14}.bg-\[\#0e0e16\]{background-color:#0e0e16}.bg-\[\#1a1a28\]{background-color:#1a1a28}.bg-\[\#1e1e2e\]{background-color:#1e1e2e}.bg-\[\#3a3a50\]{background-color:#3a3a50}.bg-\[\#818cf8\]{background-color:#818cf8}.bg-\[\#06060a\]{background-color:#06060a}.bg-\[\#08080c\]{background-color:#08080c}.bg-\[\#12121a\]{background-color:#12121a}.bg-\[\#14141c\]{background-color:#14141c}.bg-\[\#18181f\]{background-color:#18181f}.bg-\[\#f97316\]{background-color:#f97316}.bg-\[\#f97316\]\/8{background-color:#f9731614}.bg-\[\#f97316\]\/20{background-color:#f9731633}.bg-\[\#fbbf24\]\/8{background-color:#fbbf2414}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-emerald-400\/8{background-color:#00d29414}@supports (color:color-mix(in lab,red,red)){.bg-emerald-400\/8{background-color:color-mix(in oklab,var(--color-emerald-400)8%,transparent)}}.bg-red-400{background-color:var(--color-red-400)}.bg-red-400\/8{background-color:#ff656814}@supports (color:color-mix(in lab,red,red)){.bg-red-400\/8{background-color:color-mix(in oklab,var(--color-red-400)8%,transparent)}}.bg-red-400\/50{background-color:#ff656880}@supports (color:color-mix(in lab,red,red)){.bg-red-400\/50{background-color:color-mix(in oklab,var(--color-red-400)50%,transparent)}}.bg-red-400\/60{background-color:#ff656899}@supports (color:color-mix(in lab,red,red)){.bg-red-400\/60{background-color:color-mix(in oklab,var(--color-red-400)60%,transparent)}}.bg-red-500\/5{background-color:#fb2c360d}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/5{background-color:color-mix(in oklab,var(--color-red-500)5%,transparent)}}.bg-transparent{background-color:#0000}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-5{padding-block:calc(var(--spacing)*5)}.py-\[1px\]{padding-block:1px}.py-\[2px\]{padding-block:2px}.py-\[3px\]{padding-block:3px}.pr-3{padding-right:calc(var(--spacing)*3)}.pb-0\.5{padding-bottom:calc(var(--spacing)*.5)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-5{padding-left:calc(var(--spacing)*5)}.pl-\[6px\]{padding-left:6px}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#1e1e2a\]{color:#1e1e2a}.text-\[\#2e2e40\]{color:#2e2e40}.text-\[\#3a3a50\]{color:#3a3a50}.text-\[\#4e4e63\]{color:#4e4e63}.text-\[\#5a5a70\]{color:#5a5a70}.text-\[\#6b6b80\]{color:#6b6b80}.text-\[\#6e7a8a\]{color:#6e7a8a}.text-\[\#8b8b9e\]{color:#8b8b9e}.text-\[\#818cf8\]{color:#818cf8}.text-\[\#08080c\]{color:#08080c}.text-\[\#18181f\]{color:#18181f}.text-\[\#26263a\]{color:#26263a}.text-\[\#a0a0b0\]{color:#a0a0b0}.text-\[\#c0bfc6\]{color:#c0bfc6}.text-\[\#e0dfe4\]{color:#e0dfe4}.text-\[\#f97316\]{color:#f97316}.text-\[\#fb923c\]{color:#fb923c}.text-\[\#fbbf24\]{color:#fbbf24}.text-cyan-300\/70{color:#53eafdb3}@supports (color:color-mix(in lab,red,red)){.text-cyan-300\/70{color:color-mix(in oklab,var(--color-cyan-300)70%,transparent)}}.text-cyan-400{color:var(--color-cyan-400)}.text-emerald-300\/70{color:#5ee9b5b3}@supports (color:color-mix(in lab,red,red)){.text-emerald-300\/70{color:color-mix(in oklab,var(--color-emerald-300)70%,transparent)}}.text-emerald-400{color:var(--color-emerald-400)}.text-fuchsia-300\/80{color:#f2a9ffcc}@supports (color:color-mix(in lab,red,red)){.text-fuchsia-300\/80{color:color-mix(in oklab,var(--color-fuchsia-300)80%,transparent)}}.text-fuchsia-400{color:var(--color-fuchsia-400)}.text-purple-400{color:var(--color-purple-400)}.text-red-400{color:var(--color-red-400)}.text-yellow-300\/70{color:#ffe02ab3}@supports (color:color-mix(in lab,red,red)){.text-yellow-300\/70{color:color-mix(in oklab,var(--color-yellow-300)70%,transparent)}}.text-yellow-400{color:var(--color-yellow-400)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.decoration-\[\#f97316\]\/30{text-decoration-color:#f973164d}.underline-offset-2{text-underline-offset:2px}.opacity-0{opacity:0}.opacity-60{opacity:.6}.opacity-\[0\.07\]{opacity:.07}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(249\,115\,22\,0\.3\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#f973164d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media(hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.placeholder\:text-\[\#2e2e40\]::placeholder{color:#2e2e40}.focus-within\:border-\[\#f97316\]\/50:focus-within{border-color:#f9731680}.focus-within\:shadow-\[0_0_20px_rgba\(249\,115\,22\,0\.08\)\]:focus-within{--tw-shadow:0 0 20px var(--tw-shadow-color,#f9731614);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.hover\:bg-\[\#1a1a28\]:hover{background-color:#1a1a28}.hover\:bg-\[\#1c1c28\]:hover{background-color:#1c1c28}.hover\:bg-\[\#12121a\]:hover{background-color:#12121a}.hover\:bg-\[\#14141c\]:hover{background-color:#14141c}.hover\:bg-\[\#18181f\]:hover{background-color:#18181f}.hover\:bg-\[\#f97316\]\/20:hover{background-color:#f9731633}.hover\:bg-\[\#fb923c\]:hover{background-color:#fb923c}.hover\:bg-emerald-400\/10:hover{background-color:#00d2941a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-emerald-400\/10:hover{background-color:color-mix(in oklab,var(--color-emerald-400)10%,transparent)}}.hover\:bg-red-400\/10:hover{background-color:#ff65681a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-red-400\/10:hover{background-color:color-mix(in oklab,var(--color-red-400)10%,transparent)}}.hover\:text-\[\#8b8b9e\]:hover{color:#8b8b9e}.hover\:text-\[\#e0dfe4\]:hover{color:#e0dfe4}.hover\:text-\[\#f87171\]:hover{color:#f87171}.hover\:text-\[\#f97316\]:hover{color:#f97316}.hover\:text-\[\#fb923c\]:hover{color:#fb923c}.hover\:decoration-\[\#fb923c\]\/50:hover{text-decoration-color:#fb923c80}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}}body{color:#e0dfe4;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#08080c;height:100dvh;margin:0;padding:0;font-family:Sora,system-ui,-apple-system,sans-serif;overflow:hidden}#root{flex-direction:column;height:100dvh;display:flex}code,pre,.font-mono{font-family:IBM Plex Mono,JetBrains Mono,Fira Code,monospace}::selection{color:#e0dfe4;background:#f973164d}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#26263a;border-radius:10px}::-webkit-scrollbar-thumb:hover{background:#3f3f56}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:2s cubic-bezier(.4,0,.6,1) infinite pulse}@keyframes fadeIn{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in{animation:.25s ease-out fadeIn}@keyframes slideIn{0%{opacity:0;transform:translate(-6px)}to{opacity:1;transform:translate(0)}}.animate-slide-in{animation:.2s ease-out slideIn}@keyframes cursorBlink{0%,to{opacity:1}50%{opacity:0}}.animate-cursor{animation:1s step-end infinite cursorBlink}@keyframes glowPulse{0%,to{box-shadow:0 0 8px #f9731626}50%{box-shadow:0 0 16px #f973164d}}.noise:after{content:"";pointer-events:none;opacity:.02;z-index:9999;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");position:fixed;top:0;right:0;bottom:0;left:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}