mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-29 10:32:10 +08:00
Galaxy Pair Locally
This commit is contained in:
@@ -4,6 +4,7 @@ import { hideSidebar } from "/assets/js/utils.js"
|
||||
import { DeviceSettings } from "/assets/components/tools/device_settings.js"
|
||||
import { DoorControl } from "/assets/components/tools/doors.js"
|
||||
import { ErrorLogs } from "/assets/components/tools/error_logs.js"
|
||||
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
|
||||
import { Home } from "/assets/components/home/home.js"
|
||||
import { NavDestination } from "/assets/components/navigation/navigation_destination.js"
|
||||
import { NavKeys } from "/assets/components/navigation/navigation_keys.js"
|
||||
@@ -34,6 +35,7 @@ function Root() {
|
||||
createRoute("device_settings", "/device_settings", DeviceSettings),
|
||||
createRoute("doors", "/lock_or_unlock_doors", DoorControl),
|
||||
createRoute("errorLogs", "/manage_error_logs", ErrorLogs),
|
||||
createRoute("galaxy", "/galaxy", GalaxyPairing),
|
||||
createRoute("navdestination", "/set_navigation_destination", NavDestination),
|
||||
createRoute("navkeys", "/manage_navigation_keys", NavKeys),
|
||||
createRoute("root", "/", Home),
|
||||
|
||||
@@ -18,6 +18,7 @@ const MenuItems = {
|
||||
{ name: "Device Settings", link: "/device_settings", icon: "bi-sliders" },
|
||||
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
|
||||
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
|
||||
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
|
||||
{ name: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
|
||||
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
|
||||
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/* ――― Galaxy Pairing Widget ――― */
|
||||
.galaxy-wrapper {
|
||||
max-width: var(--width-xxxl);
|
||||
padding: var(--padding-base) var(--padding-lg) var(--padding-xxl);
|
||||
}
|
||||
|
||||
.galaxy-loading {
|
||||
color: var(--text-muted);
|
||||
padding: var(--padding-xl);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.galaxy-widget {
|
||||
align-items: center;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-lg);
|
||||
max-width: var(--width-lg);
|
||||
padding: var(--padding-xl);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.galaxy-widget:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
/* ――― Status Badge ――― */
|
||||
.galaxy-status-badge {
|
||||
align-items: center;
|
||||
border-radius: 2rem;
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
gap: var(--gap-xs);
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
|
||||
.galaxy-paired {
|
||||
background-color: rgba(94, 200, 200, 0.15);
|
||||
color: var(--success-fg);
|
||||
}
|
||||
|
||||
.galaxy-unpaired {
|
||||
background-color: rgba(224, 85, 119, 0.15);
|
||||
color: var(--danger-fg);
|
||||
}
|
||||
|
||||
/* ――― Text ――― */
|
||||
.galaxy-text {
|
||||
color: var(--text-color);
|
||||
line-height: var(--line-height-base);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ――― URL Link ――― */
|
||||
.galaxy-url {
|
||||
color: var(--main-fg);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
transition: color var(--transition-fast);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.galaxy-url:hover {
|
||||
color: var(--accent-hover-bg);
|
||||
}
|
||||
|
||||
/* ――― Input Group ――― */
|
||||
.galaxy-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.galaxy-input {
|
||||
background-color: var(--input-bg);
|
||||
border: var(--border-style-input);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--font-size-base);
|
||||
outline: none;
|
||||
padding: var(--padding-sm) var(--padding-base);
|
||||
transition: box-shadow var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.galaxy-input:focus {
|
||||
box-shadow: 0 0 0 2px var(--main-fg);
|
||||
}
|
||||
|
||||
/* ――― Buttons ――― */
|
||||
.galaxy-button {
|
||||
background-color: var(--input-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding: var(--padding-sm) var(--padding-base);
|
||||
text-align: center;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.galaxy-button:hover:not(:disabled) {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.galaxy-button:disabled {
|
||||
opacity: var(--disabled-opacity);
|
||||
}
|
||||
|
||||
.galaxy-button-danger {
|
||||
background-color: rgba(224, 85, 119, 0.2);
|
||||
color: var(--danger-fg);
|
||||
}
|
||||
|
||||
.galaxy-button-danger:hover:not(:disabled) {
|
||||
background-color: rgba(224, 85, 119, 0.35);
|
||||
}
|
||||
|
||||
/* ――― Mobile ――― */
|
||||
@media only screen and (max-width: 768px) and (orientation: portrait) {
|
||||
.galaxy-wrapper {
|
||||
padding: var(--padding-sm) var(--padding-base) var(--padding-xl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { html, reactive } from "https://esm.sh/@arrow-js/core"
|
||||
import { isGalaxyTunnel } from "/assets/js/utils.js"
|
||||
import { Modal } from "/assets/components/modal.js"
|
||||
|
||||
const state = reactive({
|
||||
paired: false,
|
||||
url: "",
|
||||
password: "",
|
||||
loading: true,
|
||||
submitting: false,
|
||||
showUnpairModal: false,
|
||||
fetched: false,
|
||||
})
|
||||
|
||||
async function fetchStatus() {
|
||||
state.loading = true
|
||||
try {
|
||||
const res = await fetch("/api/galaxy/status")
|
||||
const data = await res.json()
|
||||
state.paired = data.paired
|
||||
state.url = data.url
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch Galaxy status:", e)
|
||||
}
|
||||
state.loading = false
|
||||
}
|
||||
|
||||
async function pair() {
|
||||
if (state.submitting) return
|
||||
const pw = state.password.trim()
|
||||
if (pw.length < 6) {
|
||||
showSnackbar("Password must be at least 6 characters.")
|
||||
return
|
||||
}
|
||||
|
||||
state.submitting = true
|
||||
try {
|
||||
const res = await fetch("/api/galaxy/pair", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password: pw }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
state.paired = true
|
||||
state.url = data.url
|
||||
state.password = ""
|
||||
// Clear the DOM input value directly (Arrow.js doesn't two-way bind)
|
||||
const input = document.querySelector(".galaxy-input")
|
||||
if (input) input.value = ""
|
||||
showSnackbar(data.message || "Paired!")
|
||||
} else {
|
||||
showSnackbar(data.error || "Pairing failed.")
|
||||
}
|
||||
} catch (e) {
|
||||
showSnackbar("Network error — is the device reachable?")
|
||||
}
|
||||
state.submitting = false
|
||||
}
|
||||
|
||||
async function unpair() {
|
||||
state.showUnpairModal = false
|
||||
state.submitting = true
|
||||
try {
|
||||
const res = await fetch("/api/galaxy/unpair", { method: "POST" })
|
||||
const data = await res.json()
|
||||
if (res.ok) {
|
||||
state.paired = false
|
||||
state.url = ""
|
||||
showSnackbar(data.message || "Unpaired!")
|
||||
} else {
|
||||
showSnackbar(data.error || "Unpairing failed.")
|
||||
}
|
||||
} catch (e) {
|
||||
showSnackbar("Network error — is the device reachable?")
|
||||
}
|
||||
state.submitting = false
|
||||
}
|
||||
|
||||
export function GalaxyPairing() {
|
||||
if (isGalaxyTunnel()) {
|
||||
return html`
|
||||
<div class="tunnel-notice">
|
||||
<div class="tunnel-notice-icon">🛰️</div>
|
||||
<h3 class="tunnel-notice-title">Galaxy Pairing Unavailable via Galaxy</h3>
|
||||
<p class="tunnel-notice-body">Galaxy pairing requires a direct connection.<br>Connect to your device's local network to use this feature.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!state.fetched) {
|
||||
state.fetched = true
|
||||
fetchStatus()
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="galaxy-wrapper">
|
||||
<h2>Galaxy</h2>
|
||||
|
||||
${() => {
|
||||
if (state.loading) {
|
||||
return html`<div class="galaxy-loading">Checking pairing status…</div>`
|
||||
}
|
||||
|
||||
if (state.paired) {
|
||||
return html`
|
||||
<section class="galaxy-widget">
|
||||
<div class="galaxy-status-badge galaxy-paired">
|
||||
<i class="bi bi-check-circle-fill"></i> Paired
|
||||
</div>
|
||||
<p class="galaxy-text">
|
||||
Your device is paired with Galaxy. Access it remotely at:
|
||||
</p>
|
||||
<a class="galaxy-url" href="${state.url}" target="_blank" rel="noopener">
|
||||
${state.url}
|
||||
</a>
|
||||
<button
|
||||
class="galaxy-button galaxy-button-danger"
|
||||
@click="${() => { state.showUnpairModal = true }}"
|
||||
disabled="${() => state.submitting}"
|
||||
>
|
||||
${() => state.submitting ? "Unpairing…" : "Unpair"}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
${() => state.showUnpairModal ? Modal({
|
||||
title: "Confirm Unpair",
|
||||
message: "Are you sure you want to unpair from Galaxy? You will lose remote access until you pair again.",
|
||||
onConfirm: unpair,
|
||||
onCancel: () => { state.showUnpairModal = false },
|
||||
confirmText: "Unpair",
|
||||
}) : ""}
|
||||
`
|
||||
}
|
||||
|
||||
return html`
|
||||
<section class="galaxy-widget">
|
||||
<div class="galaxy-status-badge galaxy-unpaired">
|
||||
<i class="bi bi-x-circle-fill"></i> Not Paired
|
||||
</div>
|
||||
<p class="galaxy-text">
|
||||
Pair your device with Galaxy to access The Pond remotely from anywhere.
|
||||
Enter a password to secure your connection.
|
||||
</p>
|
||||
<div class="galaxy-input-group">
|
||||
<input
|
||||
class="galaxy-input"
|
||||
type="password"
|
||||
placeholder="Password (min 6 characters)"
|
||||
@input="${(e) => { state.password = e.target.value }}"
|
||||
@keydown="${(e) => { if (e.key === 'Enter') pair() }}"
|
||||
/>
|
||||
<button
|
||||
class="galaxy-button"
|
||||
@click="${pair}"
|
||||
disabled="${() => state.submitting || state.password.trim().length < 6}"
|
||||
>
|
||||
${() => state.submitting ? "Pairing…" : "Pair"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
`
|
||||
}}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/device_settings.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||
|
||||
<script type="module" src="/assets/components/router.js"></script>
|
||||
|
||||
@@ -1153,6 +1153,45 @@ def setup(app):
|
||||
},
|
||||
}
|
||||
|
||||
# ── Galaxy pairing (mirrors settings.cc L262-282) ──────────────────
|
||||
GALAXY_DIR = Path("/data/galaxy")
|
||||
GALAXY_AUTH_FILE = GALAXY_DIR / "glxyauth"
|
||||
|
||||
@app.route("/api/galaxy/status", methods=["GET"])
|
||||
def galaxy_status():
|
||||
try:
|
||||
paired = GALAXY_AUTH_FILE.is_file() and len(GALAXY_AUTH_FILE.read_text().strip()) == 64
|
||||
except Exception:
|
||||
paired = False
|
||||
dongle_id = params.get("DongleId", encoding="utf8") or ""
|
||||
return jsonify({
|
||||
"paired": paired,
|
||||
"url": f"https://galaxy.firestar.link/{dongle_id}" if dongle_id else "",
|
||||
})
|
||||
|
||||
@app.route("/api/galaxy/pair", methods=["POST"])
|
||||
def galaxy_pair():
|
||||
data = request.get_json() or {}
|
||||
password = (data.get("password") or "").strip()
|
||||
if len(password) < 6:
|
||||
return jsonify({"error": "Password must be at least 6 characters."}), 400
|
||||
|
||||
pw_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
GALAXY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
GALAXY_AUTH_FILE.write_text(pw_hash)
|
||||
|
||||
dongle_id = params.get("DongleId", encoding="utf8") or ""
|
||||
return jsonify({
|
||||
"message": "Pairing successful!",
|
||||
"url": f"https://galaxy.firestar.link/{dongle_id}" if dongle_id else "",
|
||||
})
|
||||
|
||||
@app.route("/api/galaxy/unpair", methods=["POST"])
|
||||
def galaxy_unpair():
|
||||
if GALAXY_AUTH_FILE.is_file():
|
||||
GALAXY_AUTH_FILE.unlink()
|
||||
return jsonify({"message": "Galaxy unpaired successfully."})
|
||||
|
||||
@app.route("/api/tailscale/installed", methods=["GET"])
|
||||
def tailscale_installed():
|
||||
base = "/data/tailscale"
|
||||
|
||||
Reference in New Issue
Block a user