account: split Wolf card controls
This commit is contained in:
parent
668a01081e
commit
ce7bca1f6f
9
frontend/src/account/accountErrors.js
Normal file
9
frontend/src/account/accountErrors.js
Normal file
@ -0,0 +1,9 @@
|
||||
export function formatActionError(err, fallback) {
|
||||
const message = err?.message || "";
|
||||
if (!message) return fallback;
|
||||
const normalized = message.toLowerCase();
|
||||
if (normalized.includes("ariadne unavailable") || normalized.includes("status 502") || normalized.includes("status 503")) {
|
||||
return "Ariadne is busy. Please try again in a moment.";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
@ -1,12 +1,10 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { auth, authFetch, login } from "@/auth";
|
||||
import { formatActionError } from "./accountErrors";
|
||||
import { useWolfDashboard } from "./useWolfDashboard";
|
||||
/**
|
||||
* Build the Account page dashboard and admin action state.
|
||||
*
|
||||
* WHY: this page coordinates several downstream services plus admin
|
||||
* approval flows, so isolating the orchestration keeps the SFC readable and
|
||||
* gives tests a direct seam for service state behavior.
|
||||
*
|
||||
* @returns {object} reactive service cards, admin state, and event handlers.
|
||||
*/
|
||||
export function useAccountDashboard() {
|
||||
@ -66,32 +64,7 @@ export function useAccountDashboard() {
|
||||
error: "",
|
||||
});
|
||||
|
||||
const wolf = reactive({
|
||||
status: "loading",
|
||||
error: "",
|
||||
loading: false,
|
||||
unlocking: false,
|
||||
pairing: {},
|
||||
actioning: "",
|
||||
manualUnlocking: false,
|
||||
canControlGpu: false,
|
||||
moonlightHost: "moonlight.bstein.dev",
|
||||
sourceIp: "",
|
||||
unlockTtlSeconds: 28800,
|
||||
gpuPriority: "unknown",
|
||||
gameModeStatus: "unknown",
|
||||
gameModeActive: false,
|
||||
selectedGame: "steam",
|
||||
note: "",
|
||||
manualIp: "",
|
||||
manualUser: "",
|
||||
clients: [],
|
||||
pendingPairRequests: [],
|
||||
activeUnlocks: [],
|
||||
sessions: [],
|
||||
apps: [],
|
||||
pinInputs: {},
|
||||
});
|
||||
const { wolf, refreshWolf, unlockWolf, pairWolf, setWolfGameMode, adminUnlockWolfIp } = useWolfDashboard();
|
||||
|
||||
const admin = reactive({
|
||||
enabled: false,
|
||||
@ -105,9 +78,7 @@ export function useAccountDashboard() {
|
||||
selectedFlags: {},
|
||||
});
|
||||
const onboardingUrl = ref("/onboarding");
|
||||
const vaultwardenReady = computed(() =>
|
||||
["ready", "already_present", "active", "grandfathered"].includes(vaultwarden.status),
|
||||
);
|
||||
const vaultwardenReady = computed(() => ["ready", "already_present", "active", "grandfathered"].includes(vaultwarden.status));
|
||||
const vaultwardenDisplayStatus = computed(() => (vaultwardenReady.value ? "ready" : vaultwarden.status));
|
||||
const vaultwardenOrder = computed(() => (vaultwardenReady.value ? 3 : 0));
|
||||
|
||||
@ -218,46 +189,6 @@ export function useAccountDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWolf() {
|
||||
if (!auth.authenticated) {
|
||||
wolf.status = "login required";
|
||||
return;
|
||||
}
|
||||
wolf.error = "";
|
||||
wolf.loading = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/wolf", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
|
||||
wolf.canControlGpu = Boolean(data.can_control_gpu);
|
||||
wolf.moonlightHost = data.moonlight?.host || "moonlight.bstein.dev";
|
||||
wolf.sourceIp = data.moonlight?.source_ip || "";
|
||||
wolf.unlockTtlSeconds = Number(data.moonlight?.unlock_ttl_seconds || 28800);
|
||||
wolf.gpuPriority = data.gpu?.priority || "unknown";
|
||||
wolf.gameModeStatus = data.gpu?.game_mode?.status || "unknown";
|
||||
wolf.gameModeActive = Boolean(data.gpu?.game_mode?.active);
|
||||
wolf.clients = Array.isArray(data.wolf?.clients) ? data.wolf.clients : [];
|
||||
wolf.pendingPairRequests = Array.isArray(data.wolf?.pending_pair_requests) ? data.wolf.pending_pair_requests : [];
|
||||
wolf.sessions = Array.isArray(data.wolf?.sessions) ? data.wolf.sessions : [];
|
||||
wolf.apps = Array.isArray(data.wolf?.apps) ? data.wolf.apps : [];
|
||||
wolf.activeUnlocks = Array.isArray(data.firewall?.active_unlocks) ? data.firewall.active_unlocks : [];
|
||||
for (const req of wolf.pendingPairRequests) {
|
||||
const key = req?.pair_secret || req?.name;
|
||||
if (key && !(key in wolf.pinInputs)) wolf.pinInputs[key] = "";
|
||||
}
|
||||
wolf.status = data.wolf?.api_enabled ? "ready" : "degraded";
|
||||
} catch (err) {
|
||||
wolf.status = "unavailable";
|
||||
wolf.error = formatActionError(err, "Wolf status unavailable");
|
||||
} finally {
|
||||
wolf.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAdminRequests() {
|
||||
if (!auth.authenticated) {
|
||||
admin.enabled = false;
|
||||
@ -339,16 +270,6 @@ export function useAccountDashboard() {
|
||||
return parts.length ? parts.join(" ") : "unknown";
|
||||
}
|
||||
|
||||
function formatActionError(err, fallback) {
|
||||
const message = err?.message || "";
|
||||
if (!message) return fallback;
|
||||
const normalized = message.toLowerCase();
|
||||
if (normalized.includes("ariadne unavailable") || normalized.includes("status 502") || normalized.includes("status 503")) {
|
||||
return "Ariadne is busy. Please try again in a moment.";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
function toggleFlag(username, flag, event) {
|
||||
const checked = Boolean(event?.target?.checked);
|
||||
const selected = Array.isArray(admin.selectedFlags[username]) ? [...admin.selectedFlags[username]] : [];
|
||||
@ -453,90 +374,6 @@ export function useAccountDashboard() {
|
||||
}
|
||||
}
|
||||
|
||||
async function unlockWolf() {
|
||||
wolf.error = "";
|
||||
wolf.unlocking = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/wolf/firewall/unlock", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "Unlock failed");
|
||||
} finally {
|
||||
wolf.unlocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pairWolf(pairSecret) {
|
||||
if (!pairSecret) return;
|
||||
wolf.error = "";
|
||||
wolf.pairing[pairSecret] = true;
|
||||
try {
|
||||
const pin = wolf.pinInputs[pairSecret] || "";
|
||||
const resp = await authFetch("/api/account/wolf/pairing/submit-pin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pair_secret: pairSecret, pin }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
wolf.pinInputs[pairSecret] = "";
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "Pairing failed");
|
||||
} finally {
|
||||
wolf.pairing[pairSecret] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setWolfGameMode(action) {
|
||||
wolf.error = "";
|
||||
wolf.actioning = action;
|
||||
try {
|
||||
const resp = await authFetch(`/api/account/wolf/game-mode/${action}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ game: wolf.selectedGame, note: wolf.note }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "GPU priority update failed");
|
||||
} finally {
|
||||
wolf.actioning = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function adminUnlockWolfIp() {
|
||||
if (!wolf.manualIp) return;
|
||||
wolf.error = "";
|
||||
wolf.manualUnlocking = true;
|
||||
try {
|
||||
const payload = { ip: wolf.manualIp };
|
||||
if (wolf.manualUser) payload.target_user = wolf.manualUser;
|
||||
const resp = await authFetch("/api/account/wolf/admin/firewall/unlock", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
wolf.manualIp = "";
|
||||
wolf.manualUser = "";
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "Manual unlock failed");
|
||||
} finally {
|
||||
wolf.manualUnlocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
// WHY: Safari/private-mode clipboard support varies; @returns whether fallback copy completed.
|
||||
function fallbackCopy(text) {
|
||||
const textarea = document.createElement("textarea");
|
||||
|
||||
163
frontend/src/account/useWolfDashboard.js
Normal file
163
frontend/src/account/useWolfDashboard.js
Normal file
@ -0,0 +1,163 @@
|
||||
import { reactive } from "vue";
|
||||
import { auth, authFetch } from "@/auth";
|
||||
import { formatActionError } from "./accountErrors";
|
||||
|
||||
/**
|
||||
* Build the Wolf card state and actions for the Account page.
|
||||
*
|
||||
* @returns {object} reactive Wolf state plus refresh, unlock, pairing, and admin GPU actions.
|
||||
*/
|
||||
export function useWolfDashboard() {
|
||||
const wolf = reactive({
|
||||
status: "loading",
|
||||
error: "",
|
||||
loading: false,
|
||||
unlocking: false,
|
||||
pairing: {},
|
||||
actioning: "",
|
||||
manualUnlocking: false,
|
||||
canControlGpu: false,
|
||||
moonlightHost: "moonlight.bstein.dev",
|
||||
sourceIp: "",
|
||||
unlockTtlSeconds: 28800,
|
||||
gpuPriority: "unknown",
|
||||
gameModeStatus: "unknown",
|
||||
gameModeActive: false,
|
||||
selectedGame: "steam",
|
||||
note: "",
|
||||
manualIp: "",
|
||||
manualUser: "",
|
||||
clients: [],
|
||||
pendingPairRequests: [],
|
||||
activeUnlocks: [],
|
||||
sessions: [],
|
||||
apps: [],
|
||||
pinInputs: {},
|
||||
});
|
||||
|
||||
async function refreshWolf() {
|
||||
if (!auth.authenticated) {
|
||||
wolf.status = "login required";
|
||||
return;
|
||||
}
|
||||
wolf.error = "";
|
||||
wolf.loading = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/wolf", {
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
|
||||
wolf.canControlGpu = Boolean(data.can_control_gpu);
|
||||
wolf.moonlightHost = data.moonlight?.host || "moonlight.bstein.dev";
|
||||
wolf.sourceIp = data.moonlight?.source_ip || "";
|
||||
wolf.unlockTtlSeconds = Number(data.moonlight?.unlock_ttl_seconds || 28800);
|
||||
wolf.gpuPriority = data.gpu?.priority || "unknown";
|
||||
wolf.gameModeStatus = data.gpu?.game_mode?.status || "unknown";
|
||||
wolf.gameModeActive = Boolean(data.gpu?.game_mode?.active);
|
||||
wolf.clients = Array.isArray(data.wolf?.clients) ? data.wolf.clients : [];
|
||||
wolf.pendingPairRequests = Array.isArray(data.wolf?.pending_pair_requests) ? data.wolf.pending_pair_requests : [];
|
||||
wolf.sessions = Array.isArray(data.wolf?.sessions) ? data.wolf.sessions : [];
|
||||
wolf.apps = Array.isArray(data.wolf?.apps) ? data.wolf.apps : [];
|
||||
wolf.activeUnlocks = Array.isArray(data.firewall?.active_unlocks) ? data.firewall.active_unlocks : [];
|
||||
for (const req of wolf.pendingPairRequests) {
|
||||
const key = req?.pair_secret || req?.name;
|
||||
if (key && !(key in wolf.pinInputs)) wolf.pinInputs[key] = "";
|
||||
}
|
||||
wolf.status = data.wolf?.api_enabled ? "ready" : "degraded";
|
||||
} catch (err) {
|
||||
wolf.status = "unavailable";
|
||||
wolf.error = formatActionError(err, "Wolf status unavailable");
|
||||
} finally {
|
||||
wolf.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function unlockWolf() {
|
||||
wolf.error = "";
|
||||
wolf.unlocking = true;
|
||||
try {
|
||||
const resp = await authFetch("/api/account/wolf/firewall/unlock", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "Unlock failed");
|
||||
} finally {
|
||||
wolf.unlocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pairWolf(pairSecret) {
|
||||
if (!pairSecret) return;
|
||||
wolf.error = "";
|
||||
wolf.pairing[pairSecret] = true;
|
||||
try {
|
||||
const pin = wolf.pinInputs[pairSecret] || "";
|
||||
const resp = await authFetch("/api/account/wolf/pairing/submit-pin", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pair_secret: pairSecret, pin }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
wolf.pinInputs[pairSecret] = "";
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "Pairing failed");
|
||||
} finally {
|
||||
wolf.pairing[pairSecret] = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setWolfGameMode(action) {
|
||||
wolf.error = "";
|
||||
wolf.actioning = action;
|
||||
try {
|
||||
const resp = await authFetch(`/api/account/wolf/game-mode/${action}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ game: wolf.selectedGame, note: wolf.note }),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "GPU priority update failed");
|
||||
} finally {
|
||||
wolf.actioning = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function adminUnlockWolfIp() {
|
||||
if (!wolf.manualIp) return;
|
||||
wolf.error = "";
|
||||
wolf.manualUnlocking = true;
|
||||
try {
|
||||
const payload = { ip: wolf.manualIp };
|
||||
if (wolf.manualUser) payload.target_user = wolf.manualUser;
|
||||
const resp = await authFetch("/api/account/wolf/admin/firewall/unlock", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data?.error || data?.detail || `status ${resp.status}`);
|
||||
wolf.manualIp = "";
|
||||
wolf.manualUser = "";
|
||||
await refreshWolf();
|
||||
} catch (err) {
|
||||
wolf.error = formatActionError(err, "Manual unlock failed");
|
||||
} finally {
|
||||
wolf.manualUnlocking = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { wolf, refreshWolf, unlockWolf, pairWolf, setWolfGameMode, adminUnlockWolfIp };
|
||||
}
|
||||
115
frontend/src/components/WolfAccountCard.vue
Normal file
115
frontend/src/components/WolfAccountCard.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="card module wolf-card" :style="{ order: 0 }">
|
||||
<div class="module-head">
|
||||
<h2>Wolf</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
wolf.status === 'ready'
|
||||
? 'pill-ok'
|
||||
: wolf.status === 'degraded'
|
||||
? 'pill-warn'
|
||||
: wolf.status === 'unavailable' || wolf.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ wolf.loading ? "loading..." : wolf.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">Moonlight</span>
|
||||
<span class="v mono">{{ wolf.moonlightHost }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Your IP</span>
|
||||
<span class="v mono">{{ wolf.sourceIp || "unknown" }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">GPU priority</span>
|
||||
<span class="v mono">{{ wolf.gpuPriority }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Firewall unlocks</span>
|
||||
<span class="v mono">{{ wolf.activeUnlocks.length }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Paired devices</span>
|
||||
<span class="v mono">{{ wolf.clients.map((client) => client.name).join(", ") || "none" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="wolf.unlocking" @click="$emit('unlock')">
|
||||
{{ wolf.unlocking ? "Unlocking..." : "Unlock Moonlight" }}
|
||||
</button>
|
||||
<button class="copy mono" type="button" :disabled="wolf.loading" @click="$emit('refresh')">refresh</button>
|
||||
</div>
|
||||
|
||||
<div v-if="wolf.pendingPairRequests.length" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Pairing</div>
|
||||
</div>
|
||||
<div v-for="req in wolf.pendingPairRequests" :key="req.pair_secret || req.name" class="pair-row">
|
||||
<div class="mono pair-name">{{ req.name || "pending device" }}</div>
|
||||
<input
|
||||
v-model="wolf.pinInputs[req.pair_secret]"
|
||||
class="input mono"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
placeholder="PIN"
|
||||
:disabled="wolf.pairing[req.pair_secret]"
|
||||
/>
|
||||
<button class="primary" type="button" :disabled="wolf.pairing[req.pair_secret]" @click="$emit('pair', req.pair_secret)">
|
||||
{{ wolf.pairing[req.pair_secret] ? "Pairing..." : "Pair" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wolf.canControlGpu" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Admin</div>
|
||||
</div>
|
||||
<div class="wolf-controls">
|
||||
<select v-model="wolf.selectedGame" class="input mono">
|
||||
<option value="steam">Steam</option>
|
||||
<option value="arc-raiders">Arc Raiders</option>
|
||||
<option value="satisfactory">Satisfactory</option>
|
||||
<option value="wolf">Wolf</option>
|
||||
</select>
|
||||
<input v-model="wolf.note" class="input mono" type="text" placeholder="note" />
|
||||
<button class="primary" type="button" :disabled="Boolean(wolf.actioning)" @click="$emit('game-mode', 'start')">
|
||||
{{ wolf.actioning === "start" ? "Starting..." : "Prioritize Wolf" }}
|
||||
</button>
|
||||
<button class="copy mono" type="button" :disabled="Boolean(wolf.actioning)" @click="$emit('game-mode', 'stop')">
|
||||
{{ wolf.actioning === "stop" ? "Stopping..." : "Restore AI" }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="wolf-controls manual-unlock">
|
||||
<input v-model="wolf.manualIp" class="input mono" type="text" placeholder="IP address" />
|
||||
<input v-model="wolf.manualUser" class="input mono" type="text" placeholder="user" />
|
||||
<button class="primary" type="button" :disabled="wolf.manualUnlocking" @click="$emit('admin-unlock')">
|
||||
{{ wolf.manualUnlocking ? "Unlocking..." : "Unlock IP" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wolf.sessions.length" class="hint mono">Active sessions: {{ wolf.sessions.length }}</div>
|
||||
<div v-if="wolf.error" class="error-box">
|
||||
<div class="mono">{{ wolf.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
wolf: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(["refresh", "unlock", "pair", "game-mode", "admin-unlock"]);
|
||||
</script>
|
||||
@ -192,120 +192,14 @@
|
||||
</div>
|
||||
|
||||
<div class="account-stack">
|
||||
<div class="card module wolf-card" :style="{ order: 0 }">
|
||||
<div class="module-head">
|
||||
<h2>Wolf</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
wolf.status === 'ready'
|
||||
? 'pill-ok'
|
||||
: wolf.status === 'degraded'
|
||||
? 'pill-warn'
|
||||
: wolf.status === 'unavailable' || wolf.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ wolf.loading ? "loading..." : wolf.status }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">Moonlight</span>
|
||||
<span class="v mono">{{ wolf.moonlightHost }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Your IP</span>
|
||||
<span class="v mono">{{ wolf.sourceIp || "unknown" }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">GPU priority</span>
|
||||
<span class="v mono">{{ wolf.gpuPriority }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Firewall unlocks</span>
|
||||
<span class="v mono">{{ wolf.activeUnlocks.length }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Paired devices</span>
|
||||
<span class="v mono">{{ wolf.clients.map((client) => client.name).join(", ") || "none" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="wolf.unlocking" @click="unlockWolf">
|
||||
{{ wolf.unlocking ? "Unlocking..." : "Unlock Moonlight" }}
|
||||
</button>
|
||||
<button class="copy mono" type="button" :disabled="wolf.loading" @click="refreshWolf">refresh</button>
|
||||
</div>
|
||||
|
||||
<div v-if="wolf.pendingPairRequests.length" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Pairing</div>
|
||||
</div>
|
||||
<div v-for="req in wolf.pendingPairRequests" :key="req.pair_secret || req.name" class="pair-row">
|
||||
<div class="mono pair-name">{{ req.name || "pending device" }}</div>
|
||||
<input
|
||||
v-model="wolf.pinInputs[req.pair_secret]"
|
||||
class="input mono"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
placeholder="PIN"
|
||||
:disabled="wolf.pairing[req.pair_secret]"
|
||||
/>
|
||||
<button
|
||||
class="primary"
|
||||
type="button"
|
||||
:disabled="wolf.pairing[req.pair_secret]"
|
||||
@click="pairWolf(req.pair_secret)"
|
||||
>
|
||||
{{ wolf.pairing[req.pair_secret] ? "Pairing..." : "Pair" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wolf.canControlGpu" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Admin</div>
|
||||
</div>
|
||||
<div class="wolf-controls">
|
||||
<select v-model="wolf.selectedGame" class="input mono">
|
||||
<option value="steam">Steam</option>
|
||||
<option value="arc-raiders">Arc Raiders</option>
|
||||
<option value="satisfactory">Satisfactory</option>
|
||||
<option value="wolf">Wolf</option>
|
||||
</select>
|
||||
<input v-model="wolf.note" class="input mono" type="text" placeholder="note" />
|
||||
<button
|
||||
class="primary"
|
||||
type="button"
|
||||
:disabled="Boolean(wolf.actioning)"
|
||||
@click="setWolfGameMode('start')"
|
||||
>
|
||||
{{ wolf.actioning === "start" ? "Starting..." : "Prioritize Wolf" }}
|
||||
</button>
|
||||
<button class="copy mono" type="button" :disabled="Boolean(wolf.actioning)" @click="setWolfGameMode('stop')">
|
||||
{{ wolf.actioning === "stop" ? "Stopping..." : "Restore AI" }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="wolf-controls manual-unlock">
|
||||
<input v-model="wolf.manualIp" class="input mono" type="text" placeholder="IP address" />
|
||||
<input v-model="wolf.manualUser" class="input mono" type="text" placeholder="user" />
|
||||
<button class="primary" type="button" :disabled="wolf.manualUnlocking" @click="adminUnlockWolfIp">
|
||||
{{ wolf.manualUnlocking ? "Unlocking..." : "Unlock IP" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="wolf.sessions.length" class="hint mono">
|
||||
Active sessions: {{ wolf.sessions.length }}
|
||||
</div>
|
||||
<div v-if="wolf.error" class="error-box">
|
||||
<div class="mono">{{ wolf.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<WolfAccountCard
|
||||
:wolf="wolf"
|
||||
@admin-unlock="adminUnlockWolfIp"
|
||||
@game-mode="setWolfGameMode"
|
||||
@pair="pairWolf"
|
||||
@refresh="refreshWolf"
|
||||
@unlock="unlockWolf"
|
||||
/>
|
||||
|
||||
<div class="card module" :style="{ order: vaultwardenOrder }">
|
||||
<div class="module-head">
|
||||
@ -533,6 +427,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import WolfAccountCard from "../components/WolfAccountCard.vue";
|
||||
import { useAccountDashboard } from "../account/useAccountDashboard";
|
||||
|
||||
const {
|
||||
|
||||
@ -264,6 +264,43 @@ describe("account dashboard", () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("handles Wolf login and action failures", async () => {
|
||||
installFetch((url) => {
|
||||
if (url.includes("/api/account/wolf/firewall/unlock")) return jsonResponse({ error: "unlock failed" }, 500);
|
||||
if (url.includes("/api/account/wolf/pairing/submit-pin")) return jsonResponse({ error: "pair failed" }, 500);
|
||||
if (url.includes("/api/account/wolf/game-mode/start")) return jsonResponse({ error: "mode failed" }, 500);
|
||||
if (url.includes("/api/account/wolf/admin/firewall/unlock")) return jsonResponse({ error: "manual failed" }, 500);
|
||||
if (url.includes("/api/account/wolf")) return jsonResponse({ error: "status 502" }, 502);
|
||||
if (url.includes("/api/account/overview")) return jsonResponse(overview());
|
||||
if (url.includes("/api/admin/access/requests")) return jsonResponse({ requests: [] });
|
||||
if (url.includes("/api/admin/access/flags")) return jsonResponse({ flags: [] });
|
||||
return jsonResponse({});
|
||||
});
|
||||
const { dashboard, wrapper } = mountDashboard();
|
||||
await flushPromises();
|
||||
|
||||
expect(dashboard.wolf.error).toBe("Ariadne is busy. Please try again in a moment.");
|
||||
|
||||
await dashboard.unlockWolf();
|
||||
expect(dashboard.wolf.error).toBe("unlock failed");
|
||||
|
||||
dashboard.wolf.pinInputs.secret = "1234";
|
||||
await dashboard.pairWolf("secret");
|
||||
expect(dashboard.wolf.error).toBe("pair failed");
|
||||
|
||||
await dashboard.setWolfGameMode("start");
|
||||
expect(dashboard.wolf.error).toBe("mode failed");
|
||||
|
||||
dashboard.wolf.manualIp = "5.6.7.8";
|
||||
await dashboard.adminUnlockWolfIp();
|
||||
expect(dashboard.wolf.error).toBe("manual failed");
|
||||
|
||||
auth.authenticated = false;
|
||||
await dashboard.refreshWolf();
|
||||
expect(dashboard.wolf.status).toBe("login required");
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it("handles service action warnings and failures", async () => {
|
||||
const rotateResponses = [
|
||||
{ password: "mail-a", sync_enabled: false },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user