feat: numa setup-phone — QR-based mobile DoT onboarding #38

Merged
razvandimescu merged 3 commits from feat/setup-phone into main 2026-04-11 00:08:56 +08:00
3 changed files with 50 additions and 3 deletions
Showing only changes of commit 025318340f - Show all commits

View File

@@ -288,6 +288,7 @@ body {
.path-tag.SERVFAIL { background: rgba(181, 68, 58, 0.12); color: var(--rose); }
.path-tag.BLOCKED { background: rgba(163, 152, 136, 0.15); color: var(--text-dim); }
.path-tag.COALESCED { background: rgba(138, 104, 158, 0.12); color: var(--violet-dim); }
.src-tag { font-size: 0.6rem; color: var(--text-dim); letter-spacing: 0.02em; }
/* Sidebar panels */
.sidebar {
@@ -787,6 +788,13 @@ function formatTime(epoch) {
return d.toLocaleTimeString([], { hour12: false });
}
function shortSrc(addr) {
if (!addr) return '';
const ip = addr.replace(/:\d+$/, '');
if (ip === '127.0.0.1' || ip === '::1') return 'localhost';
return ip;
}
function formatRemaining(secs) {
if (secs == null) return 'permanent';
if (secs < 60) return `${secs}s left`;
@@ -912,8 +920,8 @@ function applyLogFilter() {
? ` <button class="btn-delete" onclick="allowDomain('${e.domain}')" title="Allow this domain" style="color:var(--emerald);font-size:0.65rem;">allow</button>`
: '';
return `
<tr>
<td>${formatTime(e.timestamp_epoch)}</td>
<tr title="Source: ${e.src || 'unknown'}">
<td>${formatTime(e.timestamp_epoch)}<br><span class="src-tag">${shortSrc(e.src)}</span></td>
<td>${e.query_type}</td>
<td class="domain-cell" title="${e.domain}">${e.domain}${allowBtn}</td>
<td><span class="path-tag ${e.path}">${e.path}</span></td>

View File

@@ -94,7 +94,21 @@ async fn main() -> numa::Result<()> {
eprintln!("Config path defaults to numa.toml");
return Ok(());
}
_ => {}
_ => {
if !arg1.is_empty()
&& arg1 != "run"
&& !arg1.contains('/')
&& !arg1.contains('\\')
&& !arg1.ends_with(".toml")
{
eprintln!(
"\x1b[1;38;2;192;98;58mNuma\x1b[0m — unknown command: \x1b[1m{}\x1b[0m\n",
arg1
);
eprintln!("Run \x1b[1mnuma help\x1b[0m for a list of commands.");
std::process::exit(1);
}
}
}
let config_path = if arg1.is_empty() || arg1 == "run" {

View File

@@ -48,6 +48,31 @@ pub async fn run() -> Result<(), String> {
let lan_ip = crate::lan::detect_lan_ip()
.ok_or("could not detect LAN IP — are you connected to a network?")?;
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], SETUP_PORT));
let api_reachable = tokio::time::timeout(
std::time::Duration::from_millis(500),
tokio::net::TcpStream::connect(addr),
)
.await
.map(|r| r.is_ok())
.unwrap_or(false);
if !api_reachable {
eprintln!();
eprintln!(
" \x1b[1;38;2;192;98;58mNuma\x1b[0m — mobile API is not reachable on port {}.",
SETUP_PORT
);
eprintln!();
eprintln!(" The phone won't be able to download the profile until the mobile");
eprintln!(" API is running. Add this to your numa.toml and restart Numa:");
eprintln!();
eprintln!(" [mobile]");
eprintln!(" enabled = true");
eprintln!();
return Err("mobile API not running".into());
}
let url = format!("http://{}:{}/mobileconfig", lan_ip, SETUP_PORT);
let qr = render_qr(&url)?;