levix-bot 3.0.0-beta → 3.1.1-beta

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.
package/app.cjs CHANGED
@@ -292,6 +292,12 @@ app.get("/", noStore, (req, res) => {
292
292
  return res.render("login", { error: null });
293
293
  });
294
294
 
295
+ app.get("/login", noStore, (req, res) => {
296
+ if (!secrets.hasDashboardPassword()) return res.redirect("/setup");
297
+ if (req.session.loggedIn) return res.redirect("/");
298
+ return res.render("login", { error: null });
299
+ });
300
+
295
301
  app.post("/login", (req, res, next) => {
296
302
  if (!secrets.hasDashboardPassword()) return res.redirect("/setup");
297
303
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levix-bot",
3
- "version": "3.0.0-beta",
3
+ "version": "3.1.1-beta",
4
4
  "description": "A self-hosted personal WhatsApp bot with an AI agent, group moderation and a web control panel.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -7,10 +7,21 @@
7
7
  import { createRequire } from "module";
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
+ const fs = require("node:fs");
10
11
  const logger = require("../utils/logger.cjs");
11
12
  const settings = require("../config/settings.cjs");
13
+ const paths = require("../config/paths.cjs");
12
14
  const { attach } = require("./events.cjs");
13
15
 
16
+ /** Unix socket path for the Android host. Bionic Node cannot bind TCP. */
17
+ export function androidPanelSocketPath() {
18
+ return paths.dataPath("panel.sock");
19
+ }
20
+
21
+ export function isAndroidHost() {
22
+ return process.env.LEVIX_ANDROID === "1";
23
+ }
24
+
14
25
  /**
15
26
  * Start the panel and bind its port.
16
27
  *
@@ -55,6 +66,7 @@ export async function bootstrapPanel({ core } = {}) {
55
66
 
56
67
  const port = settings.get("port");
57
68
  const host = resolveBindAddress();
69
+ const androidSock = isAndroidHost() ? androidPanelSocketPath() : null;
58
70
 
59
71
  await new Promise((resolve, reject) => {
60
72
  // "EADDRINUSE" plus a stack trace is not an answer anyone can act on, and
@@ -72,10 +84,19 @@ export async function bootstrapPanel({ core } = {}) {
72
84
  }
73
85
  reject(error);
74
86
  });
75
- server.listen(port, host, resolve);
87
+ if (androidSock) {
88
+ try {
89
+ fs.unlinkSync(androidSock);
90
+ } catch {
91
+ // first run has no leftover socket
92
+ }
93
+ server.listen(androidSock, resolve);
94
+ } else {
95
+ server.listen(port, host, resolve);
96
+ }
76
97
  });
77
98
 
78
- return { port, host, server };
99
+ return { port, host, server, socketPath: androidSock };
79
100
  }
80
101
 
81
102
  /**
@@ -89,15 +110,18 @@ export async function bootstrapPanel({ core } = {}) {
89
110
  export function resolveBindAddress() {
90
111
  const configured = settings.get("bind_address");
91
112
  if (configured) return configured;
92
- // The phone is the host: keep the panel on loopback until a later phase
93
- // opens it in a WebView. LAN bind stays an explicit setting.
94
- if (process.env.LEVIX_ANDROID === "1") return "127.0.0.1";
113
+ // Desktop / Docker: every interface. Android never reaches this — it
114
+ // binds a unix socket instead (see androidPanelSocketPath). The WebView
115
+ // still opens http://127.0.0.1:port and the host serves that over the socket.
95
116
  return "0.0.0.0";
96
117
  }
97
118
 
98
119
  /** The address a person should type, given how this install is reachable. */
99
120
  export function panelUrl({ port, firstRun = false } = {}) {
100
121
  const domain = settings.get("public_domain");
101
- const base = domain ? `https://${domain}` : `http://localhost:${port}`;
122
+ const host =
123
+ domain ||
124
+ (isAndroidHost() ? "127.0.0.1" : "localhost");
125
+ const base = domain ? `https://${host}` : `http://${host}:${port}`;
102
126
  return firstRun ? `${base}/setup` : base;
103
127
  }
package/src/index.js CHANGED
@@ -194,9 +194,16 @@ async function startWithPanel({ open }) {
194
194
  try {
195
195
  panel = await bootstrapPanel({ core });
196
196
  } catch (error) {
197
- if (error.code === "EADDRINUSE") {
197
+ if (error.code === "EADDRINUSE" || error.code === "ECONNREFUSED") {
198
198
  console.error(`\n ${error.message}\n`);
199
199
  logger.error(error.message);
200
+ if (process.env.LEVIX_ANDROID === "1") {
201
+ // WhatsApp still works without the panel. process.exit here also
202
+ // trips pino's sonic-boom ("not ready yet") as a second crash.
203
+ console.log("Panel failed " + (error.code || error.message));
204
+ console.log("Levix ready");
205
+ return { ...core, panel: null, url: null };
206
+ }
200
207
  process.exit(1);
201
208
  }
202
209
  throw error;
@@ -78,7 +78,10 @@ function hasForwardingHeader(headers = {}) {
78
78
  * the right key for rate limiting a password or setup-code guess.
79
79
  */
80
80
  function clientAddress(req) {
81
- return req?.socket?.remoteAddress || req?.connection?.remoteAddress || "";
81
+ const addr = req?.socket?.remoteAddress || req?.connection?.remoteAddress;
82
+ if (addr) return addr;
83
+ if (process.env.LEVIX_ANDROID === "1") return "127.0.0.1";
84
+ return "";
82
85
  }
83
86
 
84
87
  /**