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/tailscale tailscale_bins/tailscale_arm64
|
||||||
mv tailscale_bins/tailscaled tailscale_bins/tailscaled_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
|
- name: Build all folders
|
||||||
if: env.build_needed == 'true'
|
if: env.build_needed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -143,7 +159,7 @@ jobs:
|
|||||||
# Clean up
|
# Clean up
|
||||||
rm -rf build tailscale_bins *.tgz
|
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
|
- name: Commit updates to main
|
||||||
if: env.build_needed == 'true'
|
if: env.build_needed == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -158,7 +174,7 @@ jobs:
|
|||||||
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Mo3he/Axis_Cam_Tailscale.git main
|
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/Mo3he/Axis_Cam_Tailscale.git main
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 6. Create GitHub Release with all new .eap files
|
# 7. Create GitHub Release with all new .eap files
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
if: env.build_needed == 'true'
|
if: env.build_needed == 'true'
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Tailscale ACAP for Axis Cameras
|
# 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.
|
This repository provides an **ACAP package** that installs the [Tailscale VPN client](https://tailscale.com/) on Axis cameras.
|
||||||
|
|
||||||
- ✅ Secure remote access to 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)
|
- ✅ Works on **Axis OS 12+** (non-root version)
|
||||||
- ✅ Based on **WireGuard VPN** technology
|
- ✅ Based on **WireGuard VPN** technology
|
||||||
|
|
||||||
🌐 **[Visit the Homepage](https://mo3he.github.io/Axis_Cam_Tailscale/)**
|
|
||||||
|
|
||||||
[](https://github.com/Mo3he/Axis_Cam_Tailscale/releases)
|
[](https://github.com/Mo3he/Axis_Cam_Tailscale/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||
[](https://github.com/sponsors/Mo3he)
|
[](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
|
## 📑 Table of Contents
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
|
|||||||
--state="$STATE_DIR/tailscaled.state" \
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
--socket="$STATE_DIR/tailscaled.sock" \
|
--socket="$STATE_DIR/tailscaled.sock" \
|
||||||
--socks5-server=localhost:1055 \
|
--socks5-server=localhost:1055 \
|
||||||
--tun=userspace-networking &
|
--tun=userspace-networking \
|
||||||
|
2>&1 | logger -t "Tailscale_VPN" &
|
||||||
TAILSCALED_PID=$!
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
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"
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
wait $TAILSCALED_PID
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
+116
-14
@@ -359,6 +359,9 @@
|
|||||||
<div class="info-value" id="ts-version">-</div>
|
<div class="info-value" id="ts-version">-</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
@@ -421,6 +424,10 @@
|
|||||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
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) {
|
function parse(txt) {
|
||||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
@@ -432,25 +439,77 @@
|
|||||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||||
if (m) tsIP = m[1];
|
if (m) tsIP = m[1];
|
||||||
}
|
}
|
||||||
|
if (!tsIP) {
|
||||||
|
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||||
|
if (nmSelf) tsIP = nmSelf[1];
|
||||||
|
}
|
||||||
if (!tsIP) {
|
if (!tsIP) {
|
||||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
var node = nodeMatch ? nodeMatch[1] : null;
|
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 loginMatches = txt.match(/active login:\s+\S+/g);
|
||||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
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+)/);
|
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
var version = null;
|
||||||
var version = versionMatch ? versionMatch[1] : 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 stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
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 (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 };
|
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);
|
('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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.text(); })
|
.then(function(r) { return r.text(); })
|
||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
render(parse(txt));
|
var result = parse(txt);
|
||||||
renderLogs(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() {
|
.catch(function() {
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||||
@@ -542,10 +628,15 @@
|
|||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
|
|
||||||
// Check for updates from GitHub
|
// Check for updates from GitHub
|
||||||
var latestChecked = false;
|
var installedVersion = null;
|
||||||
function checkForUpdate(currentVersion) {
|
var autoChecked = false;
|
||||||
if (!currentVersion || latestChecked) return;
|
function checkForUpdate(currentVersion, manual) {
|
||||||
latestChecked = true;
|
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')
|
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -555,11 +646,22 @@
|
|||||||
document.getElementById('update-version').textContent = 'v' + tag;
|
document.getElementById('update-version').textContent = 'v' + tag;
|
||||||
document.getElementById('update-banner').classList.add('visible');
|
document.getElementById('update-banner').classList.add('visible');
|
||||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
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) {
|
function compareVersions(a, b) {
|
||||||
var pa = a.split('.').map(Number);
|
var pa = a.split('.').map(Number);
|
||||||
var pb = b.split('.').map(Number);
|
var pb = b.split('.').map(Number);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,9 @@
|
|||||||
"setup": {
|
"setup": {
|
||||||
"appName": "Tailscale_VPN",
|
"appName": "Tailscale_VPN",
|
||||||
"friendlyName": "Tailscale VPN",
|
"friendlyName": "Tailscale VPN",
|
||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.96.4",
|
"version": "1.96.4",
|
||||||
"architecture": "aarch64"
|
"architecture": "aarch64"
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ killall tailscaled 2>/dev/null || true
|
|||||||
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
||||||
"$APP_DIR/lib/tailscaled" \
|
"$APP_DIR/lib/tailscaled" \
|
||||||
--state="$STATE_DIR/tailscaled.state" \
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
--socket="$STATE_DIR/tailscaled.sock" &
|
--socket="$STATE_DIR/tailscaled.sock" \
|
||||||
|
2>&1 | logger -t "Tailscale_VPN" &
|
||||||
TAILSCALED_PID=$!
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
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"
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
wait $TAILSCALED_PID
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
@@ -359,6 +359,9 @@
|
|||||||
<div class="info-value" id="ts-version">-</div>
|
<div class="info-value" id="ts-version">-</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
@@ -421,6 +424,10 @@
|
|||||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
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) {
|
function parse(txt) {
|
||||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
@@ -432,25 +439,77 @@
|
|||||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||||
if (m) tsIP = m[1];
|
if (m) tsIP = m[1];
|
||||||
}
|
}
|
||||||
|
if (!tsIP) {
|
||||||
|
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||||
|
if (nmSelf) tsIP = nmSelf[1];
|
||||||
|
}
|
||||||
if (!tsIP) {
|
if (!tsIP) {
|
||||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
var node = nodeMatch ? nodeMatch[1] : null;
|
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 loginMatches = txt.match(/active login:\s+\S+/g);
|
||||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
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+)/);
|
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
var version = null;
|
||||||
var version = versionMatch ? versionMatch[1] : 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 stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
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 (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 };
|
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);
|
('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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.text(); })
|
.then(function(r) { return r.text(); })
|
||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
render(parse(txt));
|
var result = parse(txt);
|
||||||
renderLogs(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() {
|
.catch(function() {
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||||
@@ -542,10 +627,15 @@
|
|||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
|
|
||||||
// Check for updates from GitHub
|
// Check for updates from GitHub
|
||||||
var latestChecked = false;
|
var installedVersion = null;
|
||||||
function checkForUpdate(currentVersion) {
|
var autoChecked = false;
|
||||||
if (!currentVersion || latestChecked) return;
|
function checkForUpdate(currentVersion, manual) {
|
||||||
latestChecked = true;
|
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')
|
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -555,11 +645,22 @@
|
|||||||
document.getElementById('update-version').textContent = 'v' + tag;
|
document.getElementById('update-version').textContent = 'v' + tag;
|
||||||
document.getElementById('update-banner').classList.add('visible');
|
document.getElementById('update-banner').classList.add('visible');
|
||||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
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) {
|
function compareVersions(a, b) {
|
||||||
var pa = a.split('.').map(Number);
|
var pa = a.split('.').map(Number);
|
||||||
var pb = b.split('.').map(Number);
|
var pb = b.split('.').map(Number);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -4,13 +4,13 @@
|
|||||||
"setup": {
|
"setup": {
|
||||||
"appName": "Tailscale_VPN",
|
"appName": "Tailscale_VPN",
|
||||||
"friendlyName": "Tailscale VPN",
|
"friendlyName": "Tailscale VPN",
|
||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"user": {
|
"user": {
|
||||||
"group": "root",
|
"group": "root",
|
||||||
"username": "root"
|
"username": "root"
|
||||||
},
|
},
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.96.4",
|
"version": "1.96.4",
|
||||||
"architecture": "aarch64"
|
"architecture": "aarch64"
|
||||||
|
|||||||
@@ -359,6 +359,9 @@
|
|||||||
<div class="info-value" id="ts-version">-</div>
|
<div class="info-value" id="ts-version">-</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
@@ -421,6 +424,10 @@
|
|||||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
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) {
|
function parse(txt) {
|
||||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
@@ -432,25 +439,77 @@
|
|||||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||||
if (m) tsIP = m[1];
|
if (m) tsIP = m[1];
|
||||||
}
|
}
|
||||||
|
if (!tsIP) {
|
||||||
|
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||||
|
if (nmSelf) tsIP = nmSelf[1];
|
||||||
|
}
|
||||||
if (!tsIP) {
|
if (!tsIP) {
|
||||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
var node = nodeMatch ? nodeMatch[1] : null;
|
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 loginMatches = txt.match(/active login:\s+\S+/g);
|
||||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
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+)/);
|
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
var version = null;
|
||||||
var version = versionMatch ? versionMatch[1] : 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 stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
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 (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 };
|
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);
|
('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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.text(); })
|
.then(function(r) { return r.text(); })
|
||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
render(parse(txt));
|
var result = parse(txt);
|
||||||
renderLogs(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() {
|
.catch(function() {
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||||
@@ -542,10 +627,15 @@
|
|||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
|
|
||||||
// Check for updates from GitHub
|
// Check for updates from GitHub
|
||||||
var latestChecked = false;
|
var installedVersion = null;
|
||||||
function checkForUpdate(currentVersion) {
|
var autoChecked = false;
|
||||||
if (!currentVersion || latestChecked) return;
|
function checkForUpdate(currentVersion, manual) {
|
||||||
latestChecked = true;
|
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')
|
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -555,11 +645,22 @@
|
|||||||
document.getElementById('update-version').textContent = 'v' + tag;
|
document.getElementById('update-version').textContent = 'v' + tag;
|
||||||
document.getElementById('update-banner').classList.add('visible');
|
document.getElementById('update-banner').classList.add('visible');
|
||||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
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) {
|
function compareVersions(a, b) {
|
||||||
var pa = a.split('.').map(Number);
|
var pa = a.split('.').map(Number);
|
||||||
var pb = b.split('.').map(Number);
|
var pb = b.split('.').map(Number);
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ $TAILSCALED_PATH \
|
|||||||
--state="$STATE_DIR/tailscaled.state" \
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
--socket=$SOCKET_PATH \
|
--socket=$SOCKET_PATH \
|
||||||
--socks5-server=localhost:1055 \
|
--socks5-server=localhost:1055 \
|
||||||
--tun=userspace-networking &
|
--tun=userspace-networking \
|
||||||
|
2>&1 | logger -t "tailscale_script" &
|
||||||
TAILSCALED_PID=$!
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
# Wait for tailscaled to initialize
|
# Wait for tailscaled to initialize
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,9 @@
|
|||||||
"setup": {
|
"setup": {
|
||||||
"friendlyName": "Tailscale VPN",
|
"friendlyName": "Tailscale VPN",
|
||||||
"appName": "serverconfig",
|
"appName": "serverconfig",
|
||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.96.4"
|
"version": "1.96.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,13 +17,14 @@ logger -t "Tailscale_VPN" "Starting tailscaled daemon (userspace networking)"
|
|||||||
--state="$STATE_DIR/tailscaled.state" \
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
--socket="$STATE_DIR/tailscaled.sock" \
|
--socket="$STATE_DIR/tailscaled.sock" \
|
||||||
--socks5-server=localhost:1055 \
|
--socks5-server=localhost:1055 \
|
||||||
--tun=userspace-networking &
|
--tun=userspace-networking \
|
||||||
|
2>&1 | logger -t "Tailscale_VPN" &
|
||||||
TAILSCALED_PID=$!
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
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"
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
wait $TAILSCALED_PID
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
+115
-14
@@ -359,6 +359,9 @@
|
|||||||
<div class="info-value" id="ts-version">-</div>
|
<div class="info-value" id="ts-version">-</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
@@ -421,6 +424,10 @@
|
|||||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
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) {
|
function parse(txt) {
|
||||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
@@ -432,25 +439,77 @@
|
|||||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||||
if (m) tsIP = m[1];
|
if (m) tsIP = m[1];
|
||||||
}
|
}
|
||||||
|
if (!tsIP) {
|
||||||
|
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||||
|
if (nmSelf) tsIP = nmSelf[1];
|
||||||
|
}
|
||||||
if (!tsIP) {
|
if (!tsIP) {
|
||||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
var node = nodeMatch ? nodeMatch[1] : null;
|
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 loginMatches = txt.match(/active login:\s+\S+/g);
|
||||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
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+)/);
|
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
var version = null;
|
||||||
var version = versionMatch ? versionMatch[1] : 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 stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
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 (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 };
|
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);
|
('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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.text(); })
|
.then(function(r) { return r.text(); })
|
||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
render(parse(txt));
|
var result = parse(txt);
|
||||||
renderLogs(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() {
|
.catch(function() {
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||||
@@ -542,10 +627,15 @@
|
|||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
|
|
||||||
// Check for updates from GitHub
|
// Check for updates from GitHub
|
||||||
var latestChecked = false;
|
var installedVersion = null;
|
||||||
function checkForUpdate(currentVersion) {
|
var autoChecked = false;
|
||||||
if (!currentVersion || latestChecked) return;
|
function checkForUpdate(currentVersion, manual) {
|
||||||
latestChecked = true;
|
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')
|
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -555,11 +645,22 @@
|
|||||||
document.getElementById('update-version').textContent = 'v' + tag;
|
document.getElementById('update-version').textContent = 'v' + tag;
|
||||||
document.getElementById('update-banner').classList.add('visible');
|
document.getElementById('update-banner').classList.add('visible');
|
||||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
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) {
|
function compareVersions(a, b) {
|
||||||
var pa = a.split('.').map(Number);
|
var pa = a.split('.').map(Number);
|
||||||
var pb = b.split('.').map(Number);
|
var pb = b.split('.').map(Number);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -4,9 +4,9 @@
|
|||||||
"setup": {
|
"setup": {
|
||||||
"appName": "Tailscale_VPN",
|
"appName": "Tailscale_VPN",
|
||||||
"friendlyName": "Tailscale VPN",
|
"friendlyName": "Tailscale VPN",
|
||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.96.4",
|
"version": "1.96.4",
|
||||||
"architecture": "armv7hf"
|
"architecture": "armv7hf"
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ killall tailscaled 2>/dev/null || true
|
|||||||
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
logger -t "Tailscale_VPN" "Starting tailscaled daemon"
|
||||||
"$APP_DIR/lib/tailscaled" \
|
"$APP_DIR/lib/tailscaled" \
|
||||||
--state="$STATE_DIR/tailscaled.state" \
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
--socket="$STATE_DIR/tailscaled.sock" &
|
--socket="$STATE_DIR/tailscaled.sock" \
|
||||||
|
2>&1 | logger -t "Tailscale_VPN" &
|
||||||
TAILSCALED_PID=$!
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
logger -t "Tailscale_VPN" "Connecting to Tailscale network (scroll to bottom for auth URL if prompted)"
|
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"
|
logger -t "Tailscale_VPN" "Tailscale VPN is running"
|
||||||
wait $TAILSCALED_PID
|
wait $TAILSCALED_PID
|
||||||
|
|||||||
+115
-14
@@ -359,6 +359,9 @@
|
|||||||
<div class="info-value" id="ts-version">-</div>
|
<div class="info-value" id="ts-version">-</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
@@ -421,6 +424,10 @@
|
|||||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
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) {
|
function parse(txt) {
|
||||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
@@ -432,25 +439,77 @@
|
|||||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||||
if (m) tsIP = m[1];
|
if (m) tsIP = m[1];
|
||||||
}
|
}
|
||||||
|
if (!tsIP) {
|
||||||
|
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||||
|
if (nmSelf) tsIP = nmSelf[1];
|
||||||
|
}
|
||||||
if (!tsIP) {
|
if (!tsIP) {
|
||||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
var node = nodeMatch ? nodeMatch[1] : null;
|
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 loginMatches = txt.match(/active login:\s+\S+/g);
|
||||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
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+)/);
|
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
var version = null;
|
||||||
var version = versionMatch ? versionMatch[1] : 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 stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
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 (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 };
|
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);
|
('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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.text(); })
|
.then(function(r) { return r.text(); })
|
||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
render(parse(txt));
|
var result = parse(txt);
|
||||||
renderLogs(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() {
|
.catch(function() {
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||||
@@ -542,10 +627,15 @@
|
|||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
|
|
||||||
// Check for updates from GitHub
|
// Check for updates from GitHub
|
||||||
var latestChecked = false;
|
var installedVersion = null;
|
||||||
function checkForUpdate(currentVersion) {
|
var autoChecked = false;
|
||||||
if (!currentVersion || latestChecked) return;
|
function checkForUpdate(currentVersion, manual) {
|
||||||
latestChecked = true;
|
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')
|
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -555,11 +645,22 @@
|
|||||||
document.getElementById('update-version').textContent = 'v' + tag;
|
document.getElementById('update-version').textContent = 'v' + tag;
|
||||||
document.getElementById('update-banner').classList.add('visible');
|
document.getElementById('update-banner').classList.add('visible');
|
||||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
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) {
|
function compareVersions(a, b) {
|
||||||
var pa = a.split('.').map(Number);
|
var pa = a.split('.').map(Number);
|
||||||
var pb = b.split('.').map(Number);
|
var pb = b.split('.').map(Number);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -4,13 +4,13 @@
|
|||||||
"setup": {
|
"setup": {
|
||||||
"appName": "Tailscale_VPN",
|
"appName": "Tailscale_VPN",
|
||||||
"friendlyName": "Tailscale VPN",
|
"friendlyName": "Tailscale VPN",
|
||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"user": {
|
"user": {
|
||||||
"group": "root",
|
"group": "root",
|
||||||
"username": "root"
|
"username": "root"
|
||||||
},
|
},
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.96.4",
|
"version": "1.96.4",
|
||||||
"architecture": "armv7hf"
|
"architecture": "armv7hf"
|
||||||
|
|||||||
+115
-14
@@ -359,6 +359,9 @@
|
|||||||
<div class="info-value" id="ts-version">-</div>
|
<div class="info-value" id="ts-version">-</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
@@ -421,6 +424,10 @@
|
|||||||
autoScroll = logBox.scrollHeight - logBox.scrollTop - logBox.clientHeight < 40;
|
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) {
|
function parse(txt) {
|
||||||
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
var allUrls = txt.match(/https:\/\/login\.tailscale\.com\/[^\s<"\t]+/g) || [];
|
||||||
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
var latestUrl = allUrls.length ? allUrls[allUrls.length - 1] : null;
|
||||||
@@ -432,25 +439,77 @@
|
|||||||
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
var m = last.match(/http:\/\/(100\.[\d.]+):/);
|
||||||
if (m) tsIP = m[1];
|
if (m) tsIP = m[1];
|
||||||
}
|
}
|
||||||
|
if (!tsIP) {
|
||||||
|
var nmSelf = txt.match(/netmap: self:[^\n]*\[(100\.[\d.]+)\//);
|
||||||
|
if (nmSelf) tsIP = nmSelf[1];
|
||||||
|
}
|
||||||
if (!tsIP) {
|
if (!tsIP) {
|
||||||
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
var allIPs = txt.match(/\b100\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g) || [];
|
||||||
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
tsIP = allIPs.length ? allIPs[allIPs.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeMatch = txt.match(/popBrowserAuthNow\("([^"]+)"\)/);
|
var nodeMatches = txt.match(/popBrowserAuthNow\("([^"]+)"\)/g);
|
||||||
if (!nodeMatch) nodeMatch = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/);
|
if (!nodeMatches) nodeMatches = txt.match(/StartLoginInteractiveAs\("([^"]+)"\)/g);
|
||||||
var node = nodeMatch ? nodeMatch[1] : null;
|
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 loginMatches = txt.match(/active login:\s+\S+/g);
|
||||||
var tailnet = loginMatch ? loginMatch[1] : null;
|
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+)/);
|
var versionMatches = txt.match(/Program starting: v(\d+\.\d+\.\d+)/g);
|
||||||
if (!versionMatch) versionMatch = txt.match(/tailscaled[\s_](\d+\.\d+\.\d+)/);
|
var version = null;
|
||||||
var version = versionMatch ? versionMatch[1] : 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 stateLines = txt.match(/Switching ipn state [^\n]+/g) || [];
|
||||||
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
var lastState = stateLines.length ? stateLines[stateLines.length - 1] : '';
|
||||||
var isRunning = /-> Running/.test(lastState) || /Tailscale VPN is running/.test(txt);
|
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 (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 };
|
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);
|
('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() {
|
function refresh() {
|
||||||
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
fetch(LOG_URL, { cache: 'no-store', credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.text(); })
|
.then(function(r) { return r.text(); })
|
||||||
.then(function(txt) {
|
.then(function(txt) {
|
||||||
render(parse(txt));
|
var result = parse(txt);
|
||||||
renderLogs(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() {
|
.catch(function() {
|
||||||
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
document.getElementById('status-text').textContent = 'Unable to fetch logs';
|
||||||
@@ -542,10 +627,15 @@
|
|||||||
setInterval(refresh, 5000);
|
setInterval(refresh, 5000);
|
||||||
|
|
||||||
// Check for updates from GitHub
|
// Check for updates from GitHub
|
||||||
var latestChecked = false;
|
var installedVersion = null;
|
||||||
function checkForUpdate(currentVersion) {
|
var autoChecked = false;
|
||||||
if (!currentVersion || latestChecked) return;
|
function checkForUpdate(currentVersion, manual) {
|
||||||
latestChecked = true;
|
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')
|
fetch('https://api.github.com/repos/Mo3he/Axis_Cam_Tailscale/releases/latest')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
@@ -555,11 +645,22 @@
|
|||||||
document.getElementById('update-version').textContent = 'v' + tag;
|
document.getElementById('update-version').textContent = 'v' + tag;
|
||||||
document.getElementById('update-banner').classList.add('visible');
|
document.getElementById('update-banner').classList.add('visible');
|
||||||
document.getElementById('ts-version').textContent = currentVersion + ' (outdated)';
|
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) {
|
function compareVersions(a, b) {
|
||||||
var pa = a.split('.').map(Number);
|
var pa = a.split('.').map(Number);
|
||||||
var pb = b.split('.').map(Number);
|
var pb = b.split('.').map(Number);
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ $TAILSCALED_PATH \
|
|||||||
--state="$STATE_DIR/tailscaled.state" \
|
--state="$STATE_DIR/tailscaled.state" \
|
||||||
--socket=$SOCKET_PATH \
|
--socket=$SOCKET_PATH \
|
||||||
--socks5-server=localhost:1055 \
|
--socks5-server=localhost:1055 \
|
||||||
--tun=userspace-networking &
|
--tun=userspace-networking \
|
||||||
|
2>&1 | logger -t "tailscale_script" &
|
||||||
TAILSCALED_PID=$!
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
# Wait for tailscaled to initialize
|
# Wait for tailscaled to initialize
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"setup": {
|
"setup": {
|
||||||
"friendlyName": "Tailscale VPN",
|
"friendlyName": "Tailscale VPN",
|
||||||
"appName": "serverconfig",
|
"appName": "serverconfig",
|
||||||
"vendor": "Tailscale - Packaged by Mo3he",
|
"vendor": "Mo3he",
|
||||||
"embeddedSdkVersion": "3.0",
|
"embeddedSdkVersion": "3.0",
|
||||||
"vendorUrl": "https://www.tailscale.com",
|
"vendorUrl": "https://github.com/Mo3he/Axis_Cam_Tailscale",
|
||||||
"runMode": "respawn",
|
"runMode": "respawn",
|
||||||
"version": "1.96.4",
|
"version": "1.96.4",
|
||||||
"architecture": "armv7hf"
|
"architecture": "armv7hf"
|
||||||
|
|||||||
+48
-6
@@ -358,6 +358,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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 -->
|
<!-- Features -->
|
||||||
<section id="features" class="features">
|
<section id="features" class="features">
|
||||||
<div class="feature-card">
|
<div class="feature-card">
|
||||||
@@ -403,11 +413,11 @@
|
|||||||
<h3>Standard (Non-Root)</h3>
|
<h3>Standard (Non-Root)</h3>
|
||||||
<p>For Axis OS 12 and newer. Runs in user-space networking mode without root privileges.</p>
|
<p>For Axis OS 12 and newer. Runs in user-space networking mode without root privileges.</p>
|
||||||
<div class="arch-buttons">
|
<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>
|
<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
|
AARCH64
|
||||||
</a>
|
</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>
|
<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
|
ARM
|
||||||
</a>
|
</a>
|
||||||
@@ -419,11 +429,11 @@
|
|||||||
<h3>Custom Server</h3>
|
<h3>Custom Server</h3>
|
||||||
<p>Supports custom Tailscale control servers and auth keys. Ideal for self-hosted Headscale setups.</p>
|
<p>Supports custom Tailscale control servers and auth keys. Ideal for self-hosted Headscale setups.</p>
|
||||||
<div class="arch-buttons">
|
<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>
|
<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
|
AARCH64
|
||||||
</a>
|
</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>
|
<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
|
ARM
|
||||||
</a>
|
</a>
|
||||||
@@ -435,11 +445,11 @@
|
|||||||
<h3>Root (Full Networking)</h3>
|
<h3>Root (Full Networking)</h3>
|
||||||
<p>For Axis OS older than 12. Requires root access but provides full Tailscale networking features.</p>
|
<p>For Axis OS older than 12. Requires root access but provides full Tailscale networking features.</p>
|
||||||
<div class="arch-buttons">
|
<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>
|
<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
|
AARCH64
|
||||||
</a>
|
</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>
|
<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
|
ARM
|
||||||
</a>
|
</a>
|
||||||
@@ -486,6 +496,7 @@
|
|||||||
<a href="https://www.axis.com/" target="_blank" rel="noopener">Axis</a>
|
<a href="https://www.axis.com/" target="_blank" rel="noopener">Axis</a>
|
||||||
</div>
|
</div>
|
||||||
<p>© 2025 Mo3he · MIT License</p>
|
<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>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -526,6 +537,37 @@
|
|||||||
applyTheme(e.matches ? 'light' : 'dark');
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user