lopata 0.10.2 → 0.11.0

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.
@@ -7196,6 +7196,180 @@ function HomeView() {
7196
7196
  }, undefined, true, undefined, this);
7197
7197
  }
7198
7198
 
7199
+ // src/dashboard/views/hosts.tsx
7200
+ var STATUS_COLORS = {
7201
+ ok: "bg-emerald-500/15 text-emerald-500",
7202
+ missing: "bg-red-500/15 text-red-500",
7203
+ wrong_address: "bg-red-500/15 text-red-500",
7204
+ wildcard: "bg-yellow-500/15 text-yellow-500"
7205
+ };
7206
+ var STATUS_LABELS = {
7207
+ ok: "ok",
7208
+ missing: "missing",
7209
+ wrong_address: "wrong address",
7210
+ wildcard: "wildcard"
7211
+ };
7212
+ function CopyButton({ text, label }) {
7213
+ const [copied, setCopied] = d2(false);
7214
+ const timerRef = A2(null);
7215
+ y2(() => {
7216
+ return () => {
7217
+ if (timerRef.current)
7218
+ clearTimeout(timerRef.current);
7219
+ };
7220
+ }, []);
7221
+ const handleCopy = () => {
7222
+ navigator.clipboard.writeText(text).then(() => {
7223
+ setCopied(true);
7224
+ if (timerRef.current)
7225
+ clearTimeout(timerRef.current);
7226
+ timerRef.current = setTimeout(() => setCopied(false), 2000);
7227
+ });
7228
+ };
7229
+ return /* @__PURE__ */ u3("button", {
7230
+ onClick: handleCopy,
7231
+ class: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-mono rounded-md border border-border bg-panel hover:bg-panel-hover text-text-secondary hover:text-ink transition-colors",
7232
+ children: copied ? /* @__PURE__ */ u3(k, {
7233
+ children: [
7234
+ /* @__PURE__ */ u3("svg", {
7235
+ class: "w-3.5 h-3.5 text-emerald-500",
7236
+ viewBox: "0 0 16 16",
7237
+ fill: "none",
7238
+ stroke: "currentColor",
7239
+ "stroke-width": "2",
7240
+ children: /* @__PURE__ */ u3("path", {
7241
+ d: "M3 8.5l3 3 7-7",
7242
+ "stroke-linecap": "round",
7243
+ "stroke-linejoin": "round"
7244
+ }, undefined, false, undefined, this)
7245
+ }, undefined, false, undefined, this),
7246
+ "Copied!"
7247
+ ]
7248
+ }, undefined, true, undefined, this) : /* @__PURE__ */ u3(k, {
7249
+ children: [
7250
+ /* @__PURE__ */ u3("svg", {
7251
+ class: "w-3.5 h-3.5",
7252
+ viewBox: "0 0 16 16",
7253
+ fill: "none",
7254
+ stroke: "currentColor",
7255
+ "stroke-width": "1.5",
7256
+ children: [
7257
+ /* @__PURE__ */ u3("rect", {
7258
+ x: "5",
7259
+ y: "5",
7260
+ width: "8",
7261
+ height: "8",
7262
+ rx: "1",
7263
+ "stroke-linecap": "round",
7264
+ "stroke-linejoin": "round"
7265
+ }, undefined, false, undefined, this),
7266
+ /* @__PURE__ */ u3("path", {
7267
+ d: "M11 5V3.5A1.5 1.5 0 009.5 2h-6A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5",
7268
+ "stroke-linecap": "round",
7269
+ "stroke-linejoin": "round"
7270
+ }, undefined, false, undefined, this)
7271
+ ]
7272
+ }, undefined, true, undefined, this),
7273
+ label
7274
+ ]
7275
+ }, undefined, true, undefined, this)
7276
+ }, undefined, false, undefined, this);
7277
+ }
7278
+ function HostsView() {
7279
+ const { data } = useQuery("hosts.check");
7280
+ const results = data?.results ?? [];
7281
+ const failing = results.filter((r3) => r3.status === "missing" || r3.status === "wrong_address");
7282
+ const missingHostnames = [...new Set(failing.map((r3) => r3.hostname))];
7283
+ const hostsLine = `127.0.0.1 ${missingHostnames.join(" ")}`;
7284
+ const fixCommand = data?.hostsFilePath ? `sudo sh -c 'echo "${hostsLine}" >> ${data.hostsFilePath}'` : "";
7285
+ return /* @__PURE__ */ u3("div", {
7286
+ class: "p-4 sm:p-8",
7287
+ children: [
7288
+ /* @__PURE__ */ u3(PageHeader, {
7289
+ title: "Hosts Check",
7290
+ subtitle: data?.hostsFilePath ? `Reading ${data.hostsFilePath}` : "Checking host routing setup"
7291
+ }, undefined, false, undefined, this),
7292
+ data?.error ? /* @__PURE__ */ u3("div", {
7293
+ class: "rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-400",
7294
+ children: data.error
7295
+ }, undefined, false, undefined, this) : !results.length ? /* @__PURE__ */ u3(EmptyState, {
7296
+ message: "No host routing configured. Add hosts to workers in lopata.config.ts."
7297
+ }, undefined, false, undefined, this) : /* @__PURE__ */ u3(k, {
7298
+ children: [
7299
+ /* @__PURE__ */ u3(Table, {
7300
+ headers: ["Hostname", "Worker", "Address", "Status"],
7301
+ rows: results.map((r3) => [
7302
+ /* @__PURE__ */ u3("span", {
7303
+ class: "font-mono text-xs font-medium",
7304
+ children: r3.hostname
7305
+ }, undefined, false, undefined, this),
7306
+ /* @__PURE__ */ u3("span", {
7307
+ class: "text-text-secondary",
7308
+ children: r3.workerName
7309
+ }, undefined, false, undefined, this),
7310
+ /* @__PURE__ */ u3("span", {
7311
+ class: "font-mono text-xs text-text-secondary",
7312
+ children: r3.status === "ok" ? r3.address : r3.status === "wrong_address" ? r3.address : "—"
7313
+ }, undefined, false, undefined, this),
7314
+ /* @__PURE__ */ u3(StatusBadge, {
7315
+ status: STATUS_LABELS[r3.status] ?? r3.status,
7316
+ colorMap: STATUS_COLORS
7317
+ }, undefined, false, undefined, this)
7318
+ ])
7319
+ }, undefined, false, undefined, this),
7320
+ missingHostnames.length > 0 && /* @__PURE__ */ u3("div", {
7321
+ class: "mt-6 rounded-lg border border-yellow-500/30 bg-yellow-500/5 p-4",
7322
+ children: [
7323
+ /* @__PURE__ */ u3("div", {
7324
+ class: "text-sm font-medium text-yellow-500 mb-2",
7325
+ children: "Missing hosts file entries"
7326
+ }, undefined, false, undefined, this),
7327
+ /* @__PURE__ */ u3("div", {
7328
+ class: "text-xs text-text-secondary mb-3",
7329
+ children: "Modifying the hosts file requires root privileges. Copy the command below and run it in your terminal:"
7330
+ }, undefined, false, undefined, this),
7331
+ /* @__PURE__ */ u3("div", {
7332
+ class: "flex items-stretch gap-2",
7333
+ children: [
7334
+ /* @__PURE__ */ u3("pre", {
7335
+ class: "flex-1 text-xs font-mono bg-surface p-3 rounded border border-border select-all overflow-x-auto",
7336
+ children: fixCommand
7337
+ }, undefined, false, undefined, this),
7338
+ /* @__PURE__ */ u3(CopyButton, {
7339
+ text: fixCommand,
7340
+ label: "Copy command"
7341
+ }, undefined, false, undefined, this)
7342
+ ]
7343
+ }, undefined, true, undefined, this),
7344
+ /* @__PURE__ */ u3("div", {
7345
+ class: "mt-3 text-[11px] text-text-muted",
7346
+ children: [
7347
+ "Or manually add this line to ",
7348
+ data?.hostsFilePath,
7349
+ ":"
7350
+ ]
7351
+ }, undefined, true, undefined, this),
7352
+ /* @__PURE__ */ u3("div", {
7353
+ class: "flex items-stretch gap-2 mt-1",
7354
+ children: [
7355
+ /* @__PURE__ */ u3("pre", {
7356
+ class: "flex-1 text-xs font-mono bg-surface p-3 rounded border border-border select-all",
7357
+ children: hostsLine
7358
+ }, undefined, false, undefined, this),
7359
+ /* @__PURE__ */ u3(CopyButton, {
7360
+ text: hostsLine,
7361
+ label: "Copy line"
7362
+ }, undefined, false, undefined, this)
7363
+ ]
7364
+ }, undefined, true, undefined, this)
7365
+ ]
7366
+ }, undefined, true, undefined, this)
7367
+ ]
7368
+ }, undefined, true, undefined, this)
7369
+ ]
7370
+ }, undefined, true, undefined, this);
7371
+ }
7372
+
7199
7373
  // src/dashboard/views/kv.tsx
7200
7374
  function KvView({ route }) {
7201
7375
  const parts = route.split("/").filter(Boolean);
@@ -9982,6 +10156,7 @@ var NAV_GROUPS = [
9982
10156
  items: [
9983
10157
  { path: "/workers", label: "Workers", icon: "workers" },
9984
10158
  { path: "/routes", label: "Routes", icon: "routes" },
10159
+ { path: "/hosts", label: "Hosts Check", icon: "routes" },
9985
10160
  { path: "/do", label: "Durable Objects", icon: "do" },
9986
10161
  { path: "/containers", label: "Containers", icon: "containers" },
9987
10162
  { path: "/workflows", label: "Workflows", icon: "workflows" },
@@ -10120,6 +10295,8 @@ function App() {
10120
10295
  return /* @__PURE__ */ u3(WorkersView, {}, undefined, false, undefined, this);
10121
10296
  if (route.startsWith("/routes"))
10122
10297
  return /* @__PURE__ */ u3(RoutesView, {}, undefined, false, undefined, this);
10298
+ if (route.startsWith("/hosts"))
10299
+ return /* @__PURE__ */ u3(HostsView, {}, undefined, false, undefined, this);
10123
10300
  if (route.startsWith("/generations"))
10124
10301
  return /* @__PURE__ */ u3(GenerationsView, {}, undefined, false, undefined, this);
10125
10302
  if (route.startsWith("/kv"))
@@ -564,6 +564,10 @@
564
564
  margin-top: calc(var(--spacing) * 4);
565
565
  }
566
566
 
567
+ .mt-6 {
568
+ margin-top: calc(var(--spacing) * 6);
569
+ }
570
+
567
571
  .mt-8 {
568
572
  margin-top: calc(var(--spacing) * 8);
569
573
  }
@@ -684,6 +688,10 @@
684
688
  height: calc(var(--spacing) * 2.5);
685
689
  }
686
690
 
691
+ .h-3\.5 {
692
+ height: calc(var(--spacing) * 3.5);
693
+ }
694
+
687
695
  .h-4 {
688
696
  height: calc(var(--spacing) * 4);
689
697
  }
@@ -760,6 +768,10 @@
760
768
  width: calc(var(--spacing) * 2.5);
761
769
  }
762
770
 
771
+ .w-3\.5 {
772
+ width: calc(var(--spacing) * 3.5);
773
+ }
774
+
763
775
  .w-4 {
764
776
  width: calc(var(--spacing) * 4);
765
777
  }
@@ -969,6 +981,10 @@
969
981
  align-items: flex-start;
970
982
  }
971
983
 
984
+ .items-stretch {
985
+ align-items: stretch;
986
+ }
987
+
972
988
  .justify-between {
973
989
  justify-content: space-between;
974
990
  }
@@ -1607,6 +1623,16 @@
1607
1623
  background-color: var(--color-yellow-400);
1608
1624
  }
1609
1625
 
1626
+ .bg-yellow-500\/5 {
1627
+ background-color: #edb2000d;
1628
+ }
1629
+
1630
+ @supports (color: color-mix(in lab, red, red)) {
1631
+ .bg-yellow-500\/5 {
1632
+ background-color: color-mix(in oklab, var(--color-yellow-500) 5%, transparent);
1633
+ }
1634
+ }
1635
+
1610
1636
  .bg-yellow-500\/15 {
1611
1637
  background-color: #edb20026;
1612
1638
  }
@@ -2149,6 +2175,11 @@
2149
2175
  outline-style: none;
2150
2176
  }
2151
2177
 
2178
+ .select-all {
2179
+ -webkit-user-select: all;
2180
+ user-select: all;
2181
+ }
2182
+
2152
2183
  .select-none {
2153
2184
  -webkit-user-select: none;
2154
2185
  user-select: none;
@@ -8,7 +8,7 @@
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
10
10
 
11
- <link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-jzyhpjad.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-jpd3vqkt.js"></script></head>
11
+ <link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-n1xkdmxt.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-5h0jr7cs.js"></script></head>
12
12
  <body class="h-full bg-surface text-ink" style="font-family: system-ui, -apple-system, sans-serif;">
13
13
  <script>
14
14
  // Apply saved theme before first paint to prevent flash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lopata",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,6 +9,7 @@ import { handlers as durableObjects } from './handlers/do'
9
9
  import { handlers as email } from './handlers/email'
10
10
  import { handlers as errors } from './handlers/errors'
11
11
  import { handlers as generations } from './handlers/generations'
12
+ import { handlers as hosts } from './handlers/hosts'
12
13
  import { handlers as kv } from './handlers/kv'
13
14
  import { handlers as overview } from './handlers/overview'
14
15
  import { handlers as queue } from './handlers/queue'
@@ -42,6 +43,7 @@ const allHandlers = {
42
43
  ...analyticsEngine,
43
44
  ...warnings,
44
45
  ...routes,
46
+ ...hosts,
45
47
  }
46
48
 
47
49
  export type Procedures = {
@@ -0,0 +1,15 @@
1
+ import { checkHostPatterns, readSystemHostsFile } from '../../hosts-check'
2
+ import type { HandlerContext, HostsCheckResponse } from '../types'
3
+
4
+ export const handlers = {
5
+ 'hosts.check'(_input: {}, ctx: HandlerContext): HostsCheckResponse {
6
+ const hostsFile = readSystemHostsFile()
7
+
8
+ if ('error' in hostsFile) {
9
+ return { results: [], hostsFilePath: hostsFile.path, error: hostsFile.error }
10
+ }
11
+
12
+ const results = checkHostPatterns(hostsFile.entries, ctx.lopataConfig)
13
+ return { results, hostsFilePath: hostsFile.path }
14
+ },
15
+ }
package/src/api/types.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  export type { GenerationInfo } from '../generation'
4
4
  import type { GenerationInfo } from '../generation'
5
+ import type { HostCheckResult } from '../hosts-check'
5
6
 
6
7
  export interface Paginated<T> {
7
8
  items: T[]
@@ -396,6 +397,12 @@ export interface HandlerContext {
396
397
  hostRoutes: HostRouteInfo[]
397
398
  }
398
399
 
400
+ export interface HostsCheckResponse {
401
+ results: HostCheckResult[]
402
+ hostsFilePath: string
403
+ error?: string
404
+ }
405
+
399
406
  export interface RouteInfo {
400
407
  pattern: string
401
408
  workerName: string
@@ -0,0 +1,78 @@
1
+ import { checkHostPatterns, readSystemHostsFile } from '../hosts-check'
2
+ import { loadLopataConfig } from '../lopata-config'
3
+ import type { CliContext } from './context'
4
+
5
+ export async function run(_ctx: CliContext, args: string[]) {
6
+ const action = args[0]
7
+
8
+ if (action !== 'check') {
9
+ console.error('Usage: lopata hosts check')
10
+ process.exit(1)
11
+ }
12
+
13
+ const baseDir = process.cwd()
14
+ const lopataConfig = await loadLopataConfig(baseDir)
15
+
16
+ if (!lopataConfig?.workers?.some(w => w.hosts?.length)) {
17
+ console.log('No host routing configured. Nothing to check.')
18
+ console.log('Host routing is configured via the "hosts" field in lopata.config.ts workers.')
19
+ return
20
+ }
21
+
22
+ const hostsFile = readSystemHostsFile()
23
+ if ('error' in hostsFile) {
24
+ console.error(hostsFile.error)
25
+ console.error('Make sure you have read permissions.')
26
+ process.exit(1)
27
+ }
28
+
29
+ const results = checkHostPatterns(hostsFile.entries, lopataConfig)
30
+ const hostsPath = hostsFile.path
31
+
32
+ console.log(`Hosts file: ${hostsPath}`)
33
+ console.log('')
34
+
35
+ for (const r of results) {
36
+ switch (r.status) {
37
+ case 'ok':
38
+ console.log(` ✓ ${r.hostname} (worker: ${r.workerName}) → ${r.address}`)
39
+ break
40
+ case 'missing':
41
+ console.log(` ✗ ${r.hostname} (worker: ${r.workerName}) — not found in hosts file`)
42
+ break
43
+ case 'wrong_address':
44
+ console.log(` ✗ ${r.hostname} (worker: ${r.workerName}) — points to ${r.address}, expected 127.0.0.1`)
45
+ break
46
+ case 'wildcard':
47
+ console.log(` ⚠ ${r.hostname} (worker: ${r.workerName}) — wildcard pattern, cannot be checked in hosts file`)
48
+ console.log(` You need to add specific subdomains to your hosts file manually.`)
49
+ if (process.platform === 'darwin') {
50
+ console.log(` Alternatively, use dnsmasq or a local DNS resolver to handle wildcard domains.`)
51
+ }
52
+ break
53
+ }
54
+ }
55
+
56
+ console.log('')
57
+
58
+ const failing = results.filter(r => r.status === 'missing' || r.status === 'wrong_address')
59
+ if (failing.length > 0) {
60
+ console.log('Some hostnames are missing or misconfigured.')
61
+ console.log('')
62
+ console.log('Add the missing entries to your hosts file:')
63
+ console.log('')
64
+
65
+ const uniqueHostnames = [...new Set(failing.map(m => m.hostname))]
66
+ console.log(` ${hostsPath}:`)
67
+ console.log(` 127.0.0.1 ${uniqueHostnames.join(' ')}`)
68
+ console.log('')
69
+
70
+ if (process.platform !== 'win32') {
71
+ console.log(`Run: sudo sh -c 'echo "127.0.0.1 ${uniqueHostnames.join(' ')}" >> ${hostsPath}'`)
72
+ } else {
73
+ console.log('Open Notepad as Administrator and add the line above to the hosts file.')
74
+ }
75
+ } else if (!results.some(r => r.status === 'wildcard')) {
76
+ console.log('All host routes are correctly configured.')
77
+ }
78
+ }
package/src/cli.ts CHANGED
@@ -55,6 +55,11 @@ switch (command) {
55
55
  await mod.run(ctx, commandArgs.slice(1))
56
56
  break
57
57
  }
58
+ case 'hosts': {
59
+ const mod = await import('./cli/hosts')
60
+ await mod.run(ctx, commandArgs.slice(1))
61
+ break
62
+ }
58
63
  case 'trace': {
59
64
  const mod = await import('./cli/traces')
60
65
  await mod.run(ctx, commandArgs.slice(1))
@@ -91,6 +96,7 @@ Commands:
91
96
  queues message purge <queue> Purge queue messages
92
97
  cache list List cache names
93
98
  cache purge [--name CACHE] Purge cache entries
99
+ hosts check Check hosts file for configured host routes
94
100
  trace list [options] List traces (--limit, --since, --search, --cursor)
95
101
  trace get <traceId> Get trace detail as JSON
96
102
 
@@ -0,0 +1,75 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import type { LopataConfig } from './lopata-config'
3
+
4
+ export interface HostsEntry {
5
+ address: string
6
+ hostnames: string[]
7
+ }
8
+
9
+ export interface HostCheckResult {
10
+ hostname: string
11
+ workerName: string
12
+ status: 'ok' | 'missing' | 'wrong_address' | 'wildcard'
13
+ address?: string
14
+ }
15
+
16
+ const LOCALHOST_ADDRESSES = new Set(['127.0.0.1', '::1', 'localhost'])
17
+
18
+ export function getHostsFilePath(): string {
19
+ if (process.platform === 'win32') {
20
+ return 'C:\\Windows\\System32\\drivers\\etc\\hosts'
21
+ }
22
+ return '/etc/hosts'
23
+ }
24
+
25
+ export function parseHostsFile(content: string): HostsEntry[] {
26
+ const entries: HostsEntry[] = []
27
+ for (const line of content.split('\n')) {
28
+ const trimmed = line.trim()
29
+ if (trimmed === '' || trimmed.startsWith('#')) continue
30
+ const parts = trimmed.split(/\s+/)
31
+ if (parts.length < 2) continue
32
+ entries.push({ address: parts[0]!, hostnames: parts.slice(1) })
33
+ }
34
+ return entries
35
+ }
36
+
37
+ export function readSystemHostsFile(): { path: string; entries: HostsEntry[] } | { path: string; error: string } {
38
+ const path = getHostsFilePath()
39
+ try {
40
+ const content = readFileSync(path, 'utf-8')
41
+ return { path, entries: parseHostsFile(content) }
42
+ } catch {
43
+ return { path, error: `Could not read hosts file at ${path}` }
44
+ }
45
+ }
46
+
47
+ export function checkHostPatterns(
48
+ hostsEntries: HostsEntry[],
49
+ lopataConfig: LopataConfig | null,
50
+ ): HostCheckResult[] {
51
+ const results: HostCheckResult[] = []
52
+
53
+ if (!lopataConfig?.workers) return results
54
+
55
+ for (const worker of lopataConfig.workers) {
56
+ if (!worker.hosts) continue
57
+ for (const host of worker.hosts) {
58
+ if (host.startsWith('*.')) {
59
+ results.push({ hostname: host, workerName: worker.name, status: 'wildcard' })
60
+ continue
61
+ }
62
+
63
+ const entry = hostsEntries.find(e => e.hostnames.includes(host))
64
+ if (!entry) {
65
+ results.push({ hostname: host, workerName: worker.name, status: 'missing' })
66
+ } else if (!LOCALHOST_ADDRESSES.has(entry.address)) {
67
+ results.push({ hostname: host, workerName: worker.name, status: 'wrong_address', address: entry.address })
68
+ } else {
69
+ results.push({ hostname: host, workerName: worker.name, status: 'ok', address: entry.address })
70
+ }
71
+ }
72
+ }
73
+
74
+ return results
75
+ }