mirror of
https://github.com/Mo3he/Axis_Cam_Tailscale.git
synced 2026-10-01 12:05:37 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7d73ca0f | ||
|
|
5080ec39fb | ||
|
|
219ff70c84 | ||
|
|
7a7ecec47c | ||
|
|
3ab196b7b5 | ||
|
|
6be4d7d8ad | ||
|
|
64ee3e49ab | ||
|
|
4df48e8ae6 | ||
|
|
806c3309b6 | ||
|
|
a4deaeacae | ||
|
|
6ba1c1a1a1 | ||
|
|
25df94e489 | ||
|
|
a72987391c |
@@ -85,7 +85,23 @@ jobs:
|
||||
mv tailscale_bins/tailscale tailscale_bins/tailscale_arm64
|
||||
mv tailscale_bins/tailscaled tailscale_bins/tailscaled_arm64
|
||||
|
||||
# 4. Build each folder, update manifest, and copy .eap files
|
||||
# 4. Strip binaries to reduce package size
|
||||
- name: Strip binaries
|
||||
if: env.build_needed == 'true'
|
||||
run: |
|
||||
# Install cross-architecture strip tools
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf
|
||||
|
||||
# Strip debug info and symbol tables (zero runtime/memory cost)
|
||||
aarch64-linux-gnu-strip -s tailscale_bins/tailscale_arm64
|
||||
aarch64-linux-gnu-strip -s tailscale_bins/tailscaled_arm64
|
||||
arm-linux-gnueabihf-strip -s tailscale_bins/tailscale_arm
|
||||
arm-linux-gnueabihf-strip -s tailscale_bins/tailscaled_arm
|
||||
|
||||
ls -lh tailscale_bins/
|
||||
|
||||
# 5. Build each folder, update manifest, and copy .eap files
|
||||
- name: Build all folders
|
||||
if: env.build_needed == 'true'
|
||||
run: |
|
||||
@@ -143,7 +159,7 @@ jobs:
|
||||
# Clean up
|
||||
rm -rf build tailscale_bins *.tgz
|
||||
|
||||
# 5. Commit updated manifests and .eap files directly to main
|
||||
# 6. Commit updated manifests and .eap files directly to main
|
||||
- name: Commit updates to main
|
||||
if: env.build_needed == 'true'
|
||||
run: |
|
||||
@@ -158,7 +174,7 @@ jobs:
|
||||
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Mo3he/Axis_Cam_Tailscale.git main
|
||||
fi
|
||||
|
||||
# 6. Create GitHub Release with all new .eap files
|
||||
# 7. Create GitHub Release with all new .eap files
|
||||
- name: Create GitHub Release
|
||||
if: env.build_needed == 'true'
|
||||
uses: softprops/action-gh-release@v1
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Tailscale ACAP for Axis Cameras
|
||||
|
||||
🌐 **[Visit the Homepage](https://mo3he.github.io/Axis_Cam_Tailscale/)**
|
||||
|
||||
This repository provides an **ACAP package** that installs the [Tailscale VPN client](https://tailscale.com/) on Axis cameras.
|
||||
|
||||
- ✅ Secure remote access to cameras
|
||||
@@ -7,14 +9,14 @@ This repository provides an **ACAP package** that installs the [Tailscale VPN cl
|
||||
- ✅ Works on **Axis OS 12+** (non-root version)
|
||||
- ✅ Based on **WireGuard VPN** technology
|
||||
|
||||
🌐 **[Visit the Homepage](https://mo3he.github.io/Axis_Cam_Tailscale/)**
|
||||
|
||||
[](https://github.com/Mo3he/Axis_Cam_Tailscale/releases)
|
||||
[](LICENSE)
|
||||

|
||||
[](https://github.com/sponsors/Mo3he)
|
||||
|
||||
### Disclaimer: This is an independent, community-developed ACAP package and is not an official Axis Communications product. It was developed entirely on personal time and is not affiliated with, endorsed by, or supported by Axis Communications AB. Use it at your own risk. For official Axis software, visit axis.com
|
||||
|
||||
> **Tailscale Notice:** Tailscale is a product of Tailscale Inc. This package independently redistributes the Tailscale binaries under the [BSD 3-Clause License](LICENSE) and is not affiliated with, endorsed by, or supported by Tailscale Inc. For the official Tailscale client, visit [tailscale.com](https://tailscale.com).
|
||||
---
|
||||
|
||||
## 📑 Table of Contents
|
||||
|
||||
@@ -17,13 +17,14 @@ logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket="$STATE_DIR/tailscaled.sock" \
|
||||
--socks5-server=localhost:1055 \
|
||||
--tun=userspace-networking &
|
||||
--tun=userspace-networking \
|
||||
2>&1 | logger -t "Tailscale_VPN" &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up 2>&1 | logger -t "Tailscale_VPN"
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
+116
-14
@@ -359,6 +359,9 @@
|
||||
<div class="info-value" id="ts-version">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right;">
|
||||
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
@@ -421,6 +424,10 @@
|
||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
||||
});
|
||||
|
||||
// Cache helpers - survive syslog rotation
|
||||
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
|
||||
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
|
||||
|
||||
function parse(txt) {
|
||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||
@@ -432,25 +439,77 @@
|
||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||
if (m) tsIP = m[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||
if (nmSelf) tsIP = nmSelf[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||
var node = nodeMatch ? nodeMatch[1] : null;
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
var node = null;
|
||||
if (nodeMatches) {
|
||||
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
|
||||
if (nm) node = nm[1];
|
||||
}
|
||||
if (!node) {
|
||||
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
}
|
||||
|
||||
var loginMatch = txt.match(/active login:\s+(\S+)/);
|
||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
||||
var loginMatches = txt.match(/active login:\s+\S+/g);
|
||||
var tailnet = null;
|
||||
if (loginMatches) {
|
||||
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
|
||||
if (lm) tailnet = lm[1];
|
||||
}
|
||||
if (!tailnet) {
|
||||
// Fallback: extract from periodic netmap lines "u=user@email.com"
|
||||
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
|
||||
if (userMatches) {
|
||||
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
|
||||
if (um) tailnet = um[1];
|
||||
}
|
||||
}
|
||||
|
||||
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/);
|
||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
||||
var version = versionMatch ? versionMatch[1] : null;
|
||||
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||
var version = null;
|
||||
if (versionMatches) {
|
||||
var last = versionMatches[versionMatches.length - 1];
|
||||
var vm = last.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (vm) version = vm[1];
|
||||
}
|
||||
if (!version) {
|
||||
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
|
||||
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
|
||||
if (peersMatches) {
|
||||
var lp = peersMatches[peersMatches.length - 1];
|
||||
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (pm) version = pm[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
node = node || cacheGet('node');
|
||||
tailnet = tailnet || cacheGet('tailnet');
|
||||
version = version || cacheGet('version');
|
||||
|
||||
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
||||
// Detect running even after syslog rotation (daemon active but startup lines gone)
|
||||
if (!isRunning && !latestUrl) {
|
||||
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||
/derp-\d+ connected/.test(txt) ||
|
||||
/c2n: GET/.test(txt) ||
|
||||
/localapi:/.test(txt);
|
||||
}
|
||||
|
||||
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
|
||||
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
|
||||
@@ -526,12 +585,39 @@
|
||||
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
|
||||
|
||||
function checkAppRunning() {
|
||||
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(xml) {
|
||||
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
|
||||
return m && m[1] === 'Running';
|
||||
})
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
render(parse(txt));
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
if (result.state !== 'disconnected') {
|
||||
render(result);
|
||||
} else {
|
||||
// Syslog may be empty/rotated - check app status API
|
||||
checkAppRunning().then(function(running) {
|
||||
if (running) {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
@@ -542,10 +628,15 @@
|
||||
setInterval(refresh, 5000);
|
||||
|
||||
// Check for updates from GitHub
|
||||
var latestChecked = false;
|
||||
function checkForUpdate(currentVersion) {
|
||||
if (!currentVersion || latestChecked) return;
|
||||
latestChecked = true;
|
||||
var installedVersion = null;
|
||||
var autoChecked = false;
|
||||
function checkForUpdate(currentVersion, manual) {
|
||||
if (!currentVersion) return;
|
||||
installedVersion = currentVersion;
|
||||
if (!manual && autoChecked) return;
|
||||
if (!manual) autoChecked = true;
|
||||
var btn = document.getElementById('check-update-btn');
|
||||
if (manual && btn) btn.textContent = 'Checking...';
|
||||
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
@@ -555,11 +646,22 @@
|
||||
document.getElementById('update-version').textContent = 'v' + tag;
|
||||
document.getElementById('update-banner').classList.add('visible');
|
||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
||||
if (btn) btn.textContent = 'Update Available';
|
||||
} else {
|
||||
if (manual && btn) btn.textContent = 'Up to date';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {
|
||||
if (manual && btn) btn.textContent = 'Check failed';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('check-update-btn').addEventListener('click', function() {
|
||||
if (installedVersion) checkForUpdate(installedVersion, true);
|
||||
});
|
||||
|
||||
function compareVersions(a, b) {
|
||||
var pa = a.split('.').map(Number);
|
||||
var pb = b.split('.').map(Number);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,9 @@
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Tailscale - Packaged by Mo3he",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.4",
|
||||
"architecture": "aarch64"
|
||||
|
||||
@@ -15,13 +15,14 @@ killall tailscaled 2>/dev/null || true
|
||||
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
||||
"$APP_DIR/lib/tailscaled" \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket="$STATE_DIR/tailscaled.sock" &
|
||||
--socket="$STATE_DIR/tailscaled.sock" \
|
||||
2>&1 | logger -t "Tailscale_VPN" &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes 2>&1 | logger -t "Tailscale_VPN"
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
@@ -359,6 +359,9 @@
|
||||
<div class="info-value" id="ts-version">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right;">
|
||||
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
@@ -421,6 +424,10 @@
|
||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
||||
});
|
||||
|
||||
// Cache helpers - survive syslog rotation
|
||||
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
|
||||
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
|
||||
|
||||
function parse(txt) {
|
||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||
@@ -432,25 +439,77 @@
|
||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||
if (m) tsIP = m[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||
if (nmSelf) tsIP = nmSelf[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||
var node = nodeMatch ? nodeMatch[1] : null;
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
var node = null;
|
||||
if (nodeMatches) {
|
||||
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
|
||||
if (nm) node = nm[1];
|
||||
}
|
||||
if (!node) {
|
||||
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
}
|
||||
|
||||
var loginMatch = txt.match(/active login:\s+(\S+)/);
|
||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
||||
var loginMatches = txt.match(/active login:\s+\S+/g);
|
||||
var tailnet = null;
|
||||
if (loginMatches) {
|
||||
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
|
||||
if (lm) tailnet = lm[1];
|
||||
}
|
||||
if (!tailnet) {
|
||||
// Fallback: extract from periodic netmap lines "u=user@email.com"
|
||||
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
|
||||
if (userMatches) {
|
||||
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
|
||||
if (um) tailnet = um[1];
|
||||
}
|
||||
}
|
||||
|
||||
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/);
|
||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
||||
var version = versionMatch ? versionMatch[1] : null;
|
||||
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||
var version = null;
|
||||
if (versionMatches) {
|
||||
var last = versionMatches[versionMatches.length - 1];
|
||||
var vm = last.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (vm) version = vm[1];
|
||||
}
|
||||
if (!version) {
|
||||
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
|
||||
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
|
||||
if (peersMatches) {
|
||||
var lp = peersMatches[peersMatches.length - 1];
|
||||
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (pm) version = pm[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
node = node || cacheGet('node');
|
||||
tailnet = tailnet || cacheGet('tailnet');
|
||||
version = version || cacheGet('version');
|
||||
|
||||
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
||||
// Detect running even after syslog rotation (daemon active but startup lines gone)
|
||||
if (!isRunning && !latestUrl) {
|
||||
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||
/derp-\d+ connected/.test(txt) ||
|
||||
/c2n: GET/.test(txt) ||
|
||||
/localapi:/.test(txt);
|
||||
}
|
||||
|
||||
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
|
||||
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
|
||||
@@ -526,12 +585,38 @@
|
||||
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
|
||||
|
||||
function checkAppRunning() {
|
||||
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(xml) {
|
||||
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
|
||||
return m && m[1] === 'Running';
|
||||
})
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
render(parse(txt));
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
if (result.state !== 'disconnected') {
|
||||
render(result);
|
||||
} else {
|
||||
checkAppRunning().then(function(running) {
|
||||
if (running) {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
@@ -542,10 +627,15 @@
|
||||
setInterval(refresh, 5000);
|
||||
|
||||
// Check for updates from GitHub
|
||||
var latestChecked = false;
|
||||
function checkForUpdate(currentVersion) {
|
||||
if (!currentVersion || latestChecked) return;
|
||||
latestChecked = true;
|
||||
var installedVersion = null;
|
||||
var autoChecked = false;
|
||||
function checkForUpdate(currentVersion, manual) {
|
||||
if (!currentVersion) return;
|
||||
installedVersion = currentVersion;
|
||||
if (!manual && autoChecked) return;
|
||||
if (!manual) autoChecked = true;
|
||||
var btn = document.getElementById('check-update-btn');
|
||||
if (manual && btn) btn.textContent = 'Checking...';
|
||||
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
@@ -555,11 +645,22 @@
|
||||
document.getElementById('update-version').textContent = 'v' + tag;
|
||||
document.getElementById('update-banner').classList.add('visible');
|
||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
||||
if (btn) btn.textContent = 'Update Available';
|
||||
} else {
|
||||
if (manual && btn) btn.textContent = 'Up to date';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {
|
||||
if (manual && btn) btn.textContent = 'Check failed';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('check-update-btn').addEventListener('click', function() {
|
||||
if (installedVersion) checkForUpdate(installedVersion, true);
|
||||
});
|
||||
|
||||
function compareVersions(a, b) {
|
||||
var pa = a.split('.').map(Number);
|
||||
var pb = b.split('.').map(Number);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,13 +4,13 @@
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Tailscale - Packaged by Mo3he",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"user": {
|
||||
"group": "root",
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.4",
|
||||
"architecture": "aarch64"
|
||||
|
||||
@@ -359,6 +359,9 @@
|
||||
<div class="info-value" id="ts-version">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right;">
|
||||
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
@@ -421,6 +424,10 @@
|
||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
||||
});
|
||||
|
||||
// Cache helpers - survive syslog rotation
|
||||
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
|
||||
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
|
||||
|
||||
function parse(txt) {
|
||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||
@@ -432,25 +439,77 @@
|
||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||
if (m) tsIP = m[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||
if (nmSelf) tsIP = nmSelf[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||
var node = nodeMatch ? nodeMatch[1] : null;
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
var node = null;
|
||||
if (nodeMatches) {
|
||||
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
|
||||
if (nm) node = nm[1];
|
||||
}
|
||||
if (!node) {
|
||||
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
}
|
||||
|
||||
var loginMatch = txt.match(/active login:\s+(\S+)/);
|
||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
||||
var loginMatches = txt.match(/active login:\s+\S+/g);
|
||||
var tailnet = null;
|
||||
if (loginMatches) {
|
||||
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
|
||||
if (lm) tailnet = lm[1];
|
||||
}
|
||||
if (!tailnet) {
|
||||
// Fallback: extract from periodic netmap lines "u=user@email.com"
|
||||
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
|
||||
if (userMatches) {
|
||||
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
|
||||
if (um) tailnet = um[1];
|
||||
}
|
||||
}
|
||||
|
||||
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/);
|
||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
||||
var version = versionMatch ? versionMatch[1] : null;
|
||||
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||
var version = null;
|
||||
if (versionMatches) {
|
||||
var last = versionMatches[versionMatches.length - 1];
|
||||
var vm = last.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (vm) version = vm[1];
|
||||
}
|
||||
if (!version) {
|
||||
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
|
||||
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
|
||||
if (peersMatches) {
|
||||
var lp = peersMatches[peersMatches.length - 1];
|
||||
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (pm) version = pm[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
node = node || cacheGet('node');
|
||||
tailnet = tailnet || cacheGet('tailnet');
|
||||
version = version || cacheGet('version');
|
||||
|
||||
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
||||
// Detect running even after syslog rotation (daemon active but startup lines gone)
|
||||
if (!isRunning && !latestUrl) {
|
||||
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||
/derp-\d+ connected/.test(txt) ||
|
||||
/c2n: GET/.test(txt) ||
|
||||
/localapi:/.test(txt);
|
||||
}
|
||||
|
||||
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
|
||||
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
|
||||
@@ -526,12 +585,38 @@
|
||||
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
|
||||
|
||||
function checkAppRunning() {
|
||||
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(xml) {
|
||||
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
|
||||
return m && m[1] === 'Running';
|
||||
})
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
render(parse(txt));
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
if (result.state !== 'disconnected') {
|
||||
render(result);
|
||||
} else {
|
||||
checkAppRunning().then(function(running) {
|
||||
if (running) {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
@@ -542,10 +627,15 @@
|
||||
setInterval(refresh, 5000);
|
||||
|
||||
// Check for updates from GitHub
|
||||
var latestChecked = false;
|
||||
function checkForUpdate(currentVersion) {
|
||||
if (!currentVersion || latestChecked) return;
|
||||
latestChecked = true;
|
||||
var installedVersion = null;
|
||||
var autoChecked = false;
|
||||
function checkForUpdate(currentVersion, manual) {
|
||||
if (!currentVersion) return;
|
||||
installedVersion = currentVersion;
|
||||
if (!manual && autoChecked) return;
|
||||
if (!manual) autoChecked = true;
|
||||
var btn = document.getElementById('check-update-btn');
|
||||
if (manual && btn) btn.textContent = 'Checking...';
|
||||
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
@@ -555,11 +645,22 @@
|
||||
document.getElementById('update-version').textContent = 'v' + tag;
|
||||
document.getElementById('update-banner').classList.add('visible');
|
||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
||||
if (btn) btn.textContent = 'Update Available';
|
||||
} else {
|
||||
if (manual && btn) btn.textContent = 'Up to date';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {
|
||||
if (manual && btn) btn.textContent = 'Check failed';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('check-update-btn').addEventListener('click', function() {
|
||||
if (installedVersion) checkForUpdate(installedVersion, true);
|
||||
});
|
||||
|
||||
function compareVersions(a, b) {
|
||||
var pa = a.split('.').map(Number);
|
||||
var pb = b.split('.').map(Number);
|
||||
|
||||
@@ -42,7 +42,8 @@ $TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
--socks5-server=localhost:1055 \
|
||||
--tun=userspace-networking &
|
||||
--tun=userspace-networking \
|
||||
2>&1 | logger -t "tailscale_script" &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
# Wait for tailscaled to initialize
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,9 @@
|
||||
"setup": {
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"appName": "serverconfig",
|
||||
"vendor": "Tailscale - Packaged by Mo3he",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.4"
|
||||
},
|
||||
|
||||
@@ -17,13 +17,14 @@ logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket="$STATE_DIR/tailscaled.sock" \
|
||||
--socks5-server=localhost:1055 \
|
||||
--tun=userspace-networking &
|
||||
--tun=userspace-networking \
|
||||
2>&1 | logger -t "Tailscale_VPN" &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up 2>&1 | logger -t "Tailscale_VPN"
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
+115
-14
@@ -359,6 +359,9 @@
|
||||
<div class="info-value" id="ts-version">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right;">
|
||||
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
@@ -421,6 +424,10 @@
|
||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
||||
});
|
||||
|
||||
// Cache helpers - survive syslog rotation
|
||||
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
|
||||
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
|
||||
|
||||
function parse(txt) {
|
||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||
@@ -432,25 +439,77 @@
|
||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||
if (m) tsIP = m[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||
if (nmSelf) tsIP = nmSelf[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||
var node = nodeMatch ? nodeMatch[1] : null;
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
var node = null;
|
||||
if (nodeMatches) {
|
||||
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
|
||||
if (nm) node = nm[1];
|
||||
}
|
||||
if (!node) {
|
||||
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
}
|
||||
|
||||
var loginMatch = txt.match(/active login:\s+(\S+)/);
|
||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
||||
var loginMatches = txt.match(/active login:\s+\S+/g);
|
||||
var tailnet = null;
|
||||
if (loginMatches) {
|
||||
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
|
||||
if (lm) tailnet = lm[1];
|
||||
}
|
||||
if (!tailnet) {
|
||||
// Fallback: extract from periodic netmap lines "u=user@email.com"
|
||||
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
|
||||
if (userMatches) {
|
||||
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
|
||||
if (um) tailnet = um[1];
|
||||
}
|
||||
}
|
||||
|
||||
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/);
|
||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
||||
var version = versionMatch ? versionMatch[1] : null;
|
||||
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||
var version = null;
|
||||
if (versionMatches) {
|
||||
var last = versionMatches[versionMatches.length - 1];
|
||||
var vm = last.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (vm) version = vm[1];
|
||||
}
|
||||
if (!version) {
|
||||
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
|
||||
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
|
||||
if (peersMatches) {
|
||||
var lp = peersMatches[peersMatches.length - 1];
|
||||
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (pm) version = pm[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
node = node || cacheGet('node');
|
||||
tailnet = tailnet || cacheGet('tailnet');
|
||||
version = version || cacheGet('version');
|
||||
|
||||
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
||||
// Detect running even after syslog rotation (daemon active but startup lines gone)
|
||||
if (!isRunning && !latestUrl) {
|
||||
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||
/derp-\d+ connected/.test(txt) ||
|
||||
/c2n: GET/.test(txt) ||
|
||||
/localapi:/.test(txt);
|
||||
}
|
||||
|
||||
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
|
||||
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
|
||||
@@ -526,12 +585,38 @@
|
||||
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
|
||||
|
||||
function checkAppRunning() {
|
||||
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(xml) {
|
||||
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
|
||||
return m && m[1] === 'Running';
|
||||
})
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
render(parse(txt));
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
if (result.state !== 'disconnected') {
|
||||
render(result);
|
||||
} else {
|
||||
checkAppRunning().then(function(running) {
|
||||
if (running) {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
@@ -542,10 +627,15 @@
|
||||
setInterval(refresh, 5000);
|
||||
|
||||
// Check for updates from GitHub
|
||||
var latestChecked = false;
|
||||
function checkForUpdate(currentVersion) {
|
||||
if (!currentVersion || latestChecked) return;
|
||||
latestChecked = true;
|
||||
var installedVersion = null;
|
||||
var autoChecked = false;
|
||||
function checkForUpdate(currentVersion, manual) {
|
||||
if (!currentVersion) return;
|
||||
installedVersion = currentVersion;
|
||||
if (!manual && autoChecked) return;
|
||||
if (!manual) autoChecked = true;
|
||||
var btn = document.getElementById('check-update-btn');
|
||||
if (manual && btn) btn.textContent = 'Checking...';
|
||||
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
@@ -555,11 +645,22 @@
|
||||
document.getElementById('update-version').textContent = 'v' + tag;
|
||||
document.getElementById('update-banner').classList.add('visible');
|
||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
||||
if (btn) btn.textContent = 'Update Available';
|
||||
} else {
|
||||
if (manual && btn) btn.textContent = 'Up to date';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {
|
||||
if (manual && btn) btn.textContent = 'Check failed';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('check-update-btn').addEventListener('click', function() {
|
||||
if (installedVersion) checkForUpdate(installedVersion, true);
|
||||
});
|
||||
|
||||
function compareVersions(a, b) {
|
||||
var pa = a.split('.').map(Number);
|
||||
var pb = b.split('.').map(Number);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,9 @@
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Tailscale - Packaged by Mo3he",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.4",
|
||||
"architecture": "armv7hf"
|
||||
|
||||
@@ -15,13 +15,14 @@ killall tailscaled 2>/dev/null || true
|
||||
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
||||
"$APP_DIR/lib/tailscaled" \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket="$STATE_DIR/tailscaled.sock" &
|
||||
--socket="$STATE_DIR/tailscaled.sock" \
|
||||
2>&1 | logger -t "Tailscale_VPN" &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes
|
||||
"$APP_DIR/lib/tailscale" --socket="$STATE_DIR/tailscaled.sock" up --accept-routes 2>&1 | logger -t "Tailscale_VPN"
|
||||
|
||||
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||
wait $TAILSCALED_PID
|
||||
|
||||
+115
-14
@@ -359,6 +359,9 @@
|
||||
<div class="info-value" id="ts-version">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right;">
|
||||
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
@@ -421,6 +424,10 @@
|
||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
||||
});
|
||||
|
||||
// Cache helpers - survive syslog rotation
|
||||
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
|
||||
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
|
||||
|
||||
function parse(txt) {
|
||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||
@@ -432,25 +439,77 @@
|
||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||
if (m) tsIP = m[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||
if (nmSelf) tsIP = nmSelf[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||
var node = nodeMatch ? nodeMatch[1] : null;
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
var node = null;
|
||||
if (nodeMatches) {
|
||||
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
|
||||
if (nm) node = nm[1];
|
||||
}
|
||||
if (!node) {
|
||||
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
}
|
||||
|
||||
var loginMatch = txt.match(/active login:\s+(\S+)/);
|
||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
||||
var loginMatches = txt.match(/active login:\s+\S+/g);
|
||||
var tailnet = null;
|
||||
if (loginMatches) {
|
||||
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
|
||||
if (lm) tailnet = lm[1];
|
||||
}
|
||||
if (!tailnet) {
|
||||
// Fallback: extract from periodic netmap lines "u=user@email.com"
|
||||
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
|
||||
if (userMatches) {
|
||||
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
|
||||
if (um) tailnet = um[1];
|
||||
}
|
||||
}
|
||||
|
||||
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/);
|
||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
||||
var version = versionMatch ? versionMatch[1] : null;
|
||||
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||
var version = null;
|
||||
if (versionMatches) {
|
||||
var last = versionMatches[versionMatches.length - 1];
|
||||
var vm = last.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (vm) version = vm[1];
|
||||
}
|
||||
if (!version) {
|
||||
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
|
||||
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
|
||||
if (peersMatches) {
|
||||
var lp = peersMatches[peersMatches.length - 1];
|
||||
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (pm) version = pm[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
node = node || cacheGet('node');
|
||||
tailnet = tailnet || cacheGet('tailnet');
|
||||
version = version || cacheGet('version');
|
||||
|
||||
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
||||
// Detect running even after syslog rotation (daemon active but startup lines gone)
|
||||
if (!isRunning && !latestUrl) {
|
||||
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||
/derp-\d+ connected/.test(txt) ||
|
||||
/c2n: GET/.test(txt) ||
|
||||
/localapi:/.test(txt);
|
||||
}
|
||||
|
||||
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
|
||||
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
|
||||
@@ -526,12 +585,38 @@
|
||||
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
|
||||
|
||||
function checkAppRunning() {
|
||||
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(xml) {
|
||||
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
|
||||
return m && m[1] === 'Running';
|
||||
})
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
render(parse(txt));
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
if (result.state !== 'disconnected') {
|
||||
render(result);
|
||||
} else {
|
||||
checkAppRunning().then(function(running) {
|
||||
if (running) {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
@@ -542,10 +627,15 @@
|
||||
setInterval(refresh, 5000);
|
||||
|
||||
// Check for updates from GitHub
|
||||
var latestChecked = false;
|
||||
function checkForUpdate(currentVersion) {
|
||||
if (!currentVersion || latestChecked) return;
|
||||
latestChecked = true;
|
||||
var installedVersion = null;
|
||||
var autoChecked = false;
|
||||
function checkForUpdate(currentVersion, manual) {
|
||||
if (!currentVersion) return;
|
||||
installedVersion = currentVersion;
|
||||
if (!manual && autoChecked) return;
|
||||
if (!manual) autoChecked = true;
|
||||
var btn = document.getElementById('check-update-btn');
|
||||
if (manual && btn) btn.textContent = 'Checking...';
|
||||
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
@@ -555,11 +645,22 @@
|
||||
document.getElementById('update-version').textContent = 'v' + tag;
|
||||
document.getElementById('update-banner').classList.add('visible');
|
||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
||||
if (btn) btn.textContent = 'Update Available';
|
||||
} else {
|
||||
if (manual && btn) btn.textContent = 'Up to date';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {
|
||||
if (manual && btn) btn.textContent = 'Check failed';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('check-update-btn').addEventListener('click', function() {
|
||||
if (installedVersion) checkForUpdate(installedVersion, true);
|
||||
});
|
||||
|
||||
function compareVersions(a, b) {
|
||||
var pa = a.split('.').map(Number);
|
||||
var pb = b.split('.').map(Number);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,13 +4,13 @@
|
||||
"setup": {
|
||||
"appName": "Tailscale_VPN",
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"vendor": "Tailscale - Packaged by Mo3he",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"user": {
|
||||
"group": "root",
|
||||
"username": "root"
|
||||
},
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.4",
|
||||
"architecture": "armv7hf"
|
||||
|
||||
+115
-14
@@ -359,6 +359,9 @@
|
||||
<div class="info-value" id="ts-version">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:14px;text-align:right;">
|
||||
<button id="check-update-btn" class="log-toggle">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
@@ -421,6 +424,10 @@
|
||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
||||
});
|
||||
|
||||
// Cache helpers - survive syslog rotation
|
||||
function cacheSet(k, v) { if (v) try { localStorage.setItem('ts-' + k, v); } catch(e){} }
|
||||
function cacheGet(k) { try { return localStorage.getItem('ts-' + k); } catch(e){ return null; } }
|
||||
|
||||
function parse(txt) {
|
||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||
@@ -432,25 +439,77 @@
|
||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||
if (m) tsIP = m[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||
if (nmSelf) tsIP = nmSelf[1];
|
||||
}
|
||||
if (!tsIP) {
|
||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||
}
|
||||
|
||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
||||
var node = nodeMatch ? nodeMatch[1] : null;
|
||||
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||
var node = null;
|
||||
if (nodeMatches) {
|
||||
var nm = nodeMatches[nodeMatches.length - 1].match(/"([^"]+)"/);
|
||||
if (nm) node = nm[1];
|
||||
}
|
||||
if (!node) {
|
||||
// Extract hostname from Axis syslog format: "2026-01-01T...+00:00 Hostname [ NOTICE ]"
|
||||
var hostLine = txt.match(/\d{4}-\d{2}-\d{2}T[\d:.]+[+-]\d{2}:\d{2}\s+(\S+)\s+\[/);
|
||||
if (hostLine) node = hostLine[1];
|
||||
}
|
||||
|
||||
var loginMatch = txt.match(/active login:\s+(\S+)/);
|
||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
||||
var loginMatches = txt.match(/active login:\s+\S+/g);
|
||||
var tailnet = null;
|
||||
if (loginMatches) {
|
||||
var lm = loginMatches[loginMatches.length - 1].match(/active login:\s+(\S+)/);
|
||||
if (lm) tailnet = lm[1];
|
||||
}
|
||||
if (!tailnet) {
|
||||
// Fallback: extract from periodic netmap lines "u=user@email.com"
|
||||
var userMatches = txt.match(/\bu=([^\s\[,\]]+)/g);
|
||||
if (userMatches) {
|
||||
var um = userMatches[userMatches.length - 1].match(/u=([^\s\[,\]]+)/);
|
||||
if (um) tailnet = um[1];
|
||||
}
|
||||
}
|
||||
|
||||
var versionMatch = txt.match(/Program starting: v(\d+\.\d+\.\d+)/);
|
||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
||||
var version = versionMatch ? versionMatch[1] : null;
|
||||
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||
var version = null;
|
||||
if (versionMatches) {
|
||||
var last = versionMatches[versionMatches.length - 1];
|
||||
var vm = last.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (vm) version = vm[1];
|
||||
}
|
||||
if (!version) {
|
||||
// Fallback: extract from periodic "v1.2.3-tXXX-gYYY peers:" log lines
|
||||
var peersMatches = txt.match(/v(\d+\.\d+\.\d+)-\S+\s+peers:/g);
|
||||
if (peersMatches) {
|
||||
var lp = peersMatches[peersMatches.length - 1];
|
||||
var pm = lp.match(/v(\d+\.\d+\.\d+)/);
|
||||
if (pm) version = pm[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Cache when found, restore from cache when missing
|
||||
cacheSet('ip', tsIP); cacheSet('node', node); cacheSet('tailnet', tailnet); cacheSet('version', version);
|
||||
tsIP = tsIP || cacheGet('ip');
|
||||
node = node || cacheGet('node');
|
||||
tailnet = tailnet || cacheGet('tailnet');
|
||||
version = version || cacheGet('version');
|
||||
|
||||
var stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
||||
// Detect running even after syslog rotation (daemon active but startup lines gone)
|
||||
if (!isRunning && !latestUrl) {
|
||||
isRunning = /health\(warnable=[^)]+\): ok/.test(txt) ||
|
||||
/derp-\d+ connected/.test(txt) ||
|
||||
/c2n: GET/.test(txt) ||
|
||||
/localapi:/.test(txt);
|
||||
}
|
||||
|
||||
if (isRunning) return { state: 'connected', url: null, ip: tsIP, node: node, tailnet: tailnet, version: version };
|
||||
if (latestUrl) return { state: 'connecting', url: latestUrl, ip: null, node: null, tailnet: null, version: version };
|
||||
@@ -526,12 +585,38 @@
|
||||
('0'+now.getHours()).slice(-2) + ':' + ('0'+now.getMinutes()).slice(-2) + ':' + ('0'+now.getSeconds()).slice(-2);
|
||||
}
|
||||
|
||||
var APP_LIST_URL = '/axis-cgi/applications/list.cgi';
|
||||
|
||||
function checkAppRunning() {
|
||||
return fetch(APP_LIST_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(xml) {
|
||||
var m = xml.match(new RegExp('Name="' + APP + '"[^/]*Status="([^"]+)"'));
|
||||
return m && m[1] === 'Running';
|
||||
})
|
||||
.catch(function() { return false; });
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(txt) {
|
||||
render(parse(txt));
|
||||
var result = parse(txt);
|
||||
renderLogs(txt);
|
||||
if (result.state !== 'disconnected') {
|
||||
render(result);
|
||||
} else {
|
||||
checkAppRunning().then(function(running) {
|
||||
if (running) {
|
||||
result.state = 'connected';
|
||||
result.ip = result.ip || cacheGet('ip');
|
||||
result.node = result.node || cacheGet('node');
|
||||
result.tailnet = result.tailnet || cacheGet('tailnet');
|
||||
result.version = result.version || cacheGet('version');
|
||||
}
|
||||
render(result);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||
@@ -542,10 +627,15 @@
|
||||
setInterval(refresh, 5000);
|
||||
|
||||
// Check for updates from GitHub
|
||||
var latestChecked = false;
|
||||
function checkForUpdate(currentVersion) {
|
||||
if (!currentVersion || latestChecked) return;
|
||||
latestChecked = true;
|
||||
var installedVersion = null;
|
||||
var autoChecked = false;
|
||||
function checkForUpdate(currentVersion, manual) {
|
||||
if (!currentVersion) return;
|
||||
installedVersion = currentVersion;
|
||||
if (!manual && autoChecked) return;
|
||||
if (!manual) autoChecked = true;
|
||||
var btn = document.getElementById('check-update-btn');
|
||||
if (manual && btn) btn.textContent = 'Checking...';
|
||||
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
@@ -555,11 +645,22 @@
|
||||
document.getElementById('update-version').textContent = 'v' + tag;
|
||||
document.getElementById('update-banner').classList.add('visible');
|
||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
||||
if (btn) btn.textContent = 'Update Available';
|
||||
} else {
|
||||
if (manual && btn) btn.textContent = 'Up to date';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {
|
||||
if (manual && btn) btn.textContent = 'Check failed';
|
||||
setTimeout(function() { if (btn) btn.textContent = 'Check for Updates'; }, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('check-update-btn').addEventListener('click', function() {
|
||||
if (installedVersion) checkForUpdate(installedVersion, true);
|
||||
});
|
||||
|
||||
function compareVersions(a, b) {
|
||||
var pa = a.split('.').map(Number);
|
||||
var pb = b.split('.').map(Number);
|
||||
|
||||
@@ -42,7 +42,8 @@ $TAILSCALED_PATH \
|
||||
--state="$STATE_DIR/tailscaled.state" \
|
||||
--socket=$SOCKET_PATH \
|
||||
--socks5-server=localhost:1055 \
|
||||
--tun=userspace-networking &
|
||||
--tun=userspace-networking \
|
||||
2>&1 | logger -t "tailscale_script" &
|
||||
TAILSCALED_PID=$!
|
||||
|
||||
# Wait for tailscaled to initialize
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"setup": {
|
||||
"friendlyName": "Tailscale VPN",
|
||||
"appName": "serverconfig",
|
||||
"vendor": "Tailscale - Packaged by Mo3he",
|
||||
"vendor": "Mo3he",
|
||||
"embeddedSdkVersion": "3.0",
|
||||
"vendorUrl": "https://www.tailscale.com",
|
||||
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||
"runMode": "respawn",
|
||||
"version": "1.96.4",
|
||||
"architecture": "armv7hf"
|
||||
|
||||
+48
-6
@@ -358,6 +358,16 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Disclaimer -->
|
||||
<div style="max-width:720px;margin:0 auto;padding:0 1.5rem 1.5rem;">
|
||||
<div style="background:rgba(79,143,247,0.07);border:1px solid rgba(79,143,247,0.18);border-radius:10px;padding:12px 18px;font-size:12.5px;color:var(--muted);line-height:1.6;">
|
||||
<strong style="color:var(--text);">Disclaimer:</strong>
|
||||
This is an independent, community-developed ACAP package and is <strong style="color:var(--text);">not an official Axis Communications product</strong>.
|
||||
It was developed entirely on personal time and is not affiliated with, endorsed by, or supported by Axis Communications AB.
|
||||
Use at your own risk. For official Axis software, visit <a href="https://www.axis.com" target="_blank" rel="noopener">axis.com</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<section id="features" class="features">
|
||||
<div class="feature-card">
|
||||
@@ -403,11 +413,11 @@
|
||||
<h3>Standard (Non-Root)</h3>
|
||||
<p>For Axis OS 12 and newer. Runs in user-space networking mode without root privileges.</p>
|
||||
<div class="arch-buttons">
|
||||
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<a class="arch-btn" data-asset="Tailscale_VPN" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
AARCH64
|
||||
</a>
|
||||
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<a class="arch-btn" data-asset="Tailscale_VPN" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
ARM
|
||||
</a>
|
||||
@@ -419,11 +429,11 @@
|
||||
<h3>Custom Server</h3>
|
||||
<p>Supports custom Tailscale control servers and auth keys. Ideal for self-hosted Headscale setups.</p>
|
||||
<div class="arch-buttons">
|
||||
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<a class="arch-btn" data-asset="custom" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
AARCH64
|
||||
</a>
|
||||
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<a class="arch-btn" data-asset="custom" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
ARM
|
||||
</a>
|
||||
@@ -435,11 +445,11 @@
|
||||
<h3>Root (Full Networking)</h3>
|
||||
<p>For Axis OS older than 12. Requires root access but provides full Tailscale networking features.</p>
|
||||
<div class="arch-buttons">
|
||||
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<a class="arch-btn" data-asset="ROOT" data-arch="aarch64" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
AARCH64
|
||||
</a>
|
||||
<a class="arch-btn" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<a class="arch-btn" data-asset="ROOT" data-arch="armv7hf" href="https://github.com/Mo3he/Axis_Cam_Tailscale/releases/latest" target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
ARM
|
||||
</a>
|
||||
@@ -486,6 +496,7 @@
|
||||
<a href="https://www.axis.com/" target="_blank" rel="noopener">Axis</a>
|
||||
</div>
|
||||
<p>© 2025 Mo3he · MIT License</p>
|
||||
<p style="font-size:0.75rem;opacity:0.5;margin-top:0.5rem;">Tailscale is a product of Tailscale Inc. This is an independent, unofficial package that redistributes Tailscale binaries under the <a href="https://github.com/Mo3he/Axis_Cam_Tailscale/blob/main/LICENSE" target="_blank" rel="noopener" style="color:inherit;">BSD 3-Clause License</a>. Not affiliated with or endorsed by Tailscale Inc. or Axis Communications AB.</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
@@ -526,6 +537,37 @@
|
||||
applyTheme(e.matches ? 'light' : 'dark');
|
||||
}
|
||||
});
|
||||
|
||||
// Rewrite download links to point directly to latest release assets
|
||||
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var assets = data.assets || [];
|
||||
var buttons = document.querySelectorAll('a[data-asset]');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
var btn = buttons[i];
|
||||
var type = btn.getAttribute('data-asset');
|
||||
var arch = btn.getAttribute('data-arch');
|
||||
for (var j = 0; j < assets.length; j++) {
|
||||
var name = assets[j].name.toLowerCase();
|
||||
var archMatch = name.indexOf(arch) !== -1;
|
||||
var typeMatch = false;
|
||||
if (type === 'Tailscale_VPN') {
|
||||
typeMatch = name.indexOf('custom') === -1 && name.indexOf('root') === -1;
|
||||
} else if (type === 'custom') {
|
||||
typeMatch = name.indexOf('custom') !== -1;
|
||||
} else if (type === 'ROOT') {
|
||||
typeMatch = name.indexOf('root') !== -1;
|
||||
}
|
||||
if (archMatch && typeMatch) {
|
||||
btn.href = assets[j].browser_download_url;
|
||||
btn.removeAttribute('target');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user