<template>
<div class="setup-page">
<div class="setup-card">
<div class="setup-brand">
<img src="/logo-cube-square.svg" alt="Smart Home" class="brand-logo" />
<h1 class="brand-title">Smart Home</h1>
<p class="brand-subtitle text-muted">Mobile App Setup</p>
</div>
<div class="setup-divider text-muted" />
<p class="setup-hint text-muted">
Enter the address of your Smart Home server. Example:
<code class="text-info">https://shserv.home</code> or
<code class="text-info">http://192.168.1.10</code>
</p>
<div class="setup-form">
<GnInput
v-model="serverUrl"
label="Server URL"
placeholder="https://your-server.com"
:error="error"
@keyup.enter="handleSave"
/>
<GnButton
variant="primary"
size="lg"
class="setup-btn"
:loading="testing"
@click="handleSave"
>
<template #icon>
<i class="ph ph-check" />
</template>
{{ testing ? "Checking connection…" : "Save and continue" }}
</GnButton>
</div>
<p v-if="testError" class="setup-error text-danger">
<i class="ph ph-warning-circle" /> {{ testError }}
</p>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import { GnButton, GnInput } from "gnexus-ui-kit/vue";
import { setServerUrl, isNativeApp } from "../../../api/server-config";
const router = useRouter();
const serverUrl = ref("");
const error = ref("");
const testing = ref(false);
const testError = ref("");
async function handleSave() {
error.value = "";
testError.value = "";
const raw = serverUrl.value.trim();
if (!raw) {
error.value = "Server URL is required";
return;
}
// Basic URL validation
let url = raw;
if (!/^https?:\/\//i.test(url)) {
url = "http://" + url;
}
try {
new URL(url);
} catch {
error.value = "Invalid URL format";
return;
}
testing.value = true;
try {
// Test connectivity: try to reach the server's /about or /api/v1/about
const testUrls = [`${url}/api/v1/about`, `${url}/about`];
let reachable = false;
for (const testUrl of testUrls) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
const res = await fetch(testUrl, {
method: "GET",
signal: controller.signal,
});
clearTimeout(timeout);
if (res.ok || res.status === 401) {
// 401 is fine — means server is alive but wants auth
reachable = true;
break;
}
} catch {
// try next
}
}
if (!reachable) {
testError.value = "Could not connect to server. Please check the address.";
testing.value = false;
return;
}
await setServerUrl(url);
// Force a page reload so that the API client picks up the new base URL
// and auth init runs against the correct server
if (isNativeApp()) {
window.location.reload();
} else {
router.replace("/login");
}
} catch {
testError.value = "Connection check failed. Please try again.";
testing.value = false;
}
}
</script>
<style scoped>
.setup-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
.setup-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
max-width: 360px;
width: 100%;
padding: 2rem;
text-align: center;
}
.setup-brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.brand-logo {
width: 120px;
height: 120px;
}
.brand-title {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
}
.brand-subtitle {
margin: 0;
font-size: 0.875rem;
}
.setup-divider {
width: 48px;
height: 2px;
background: currentColor;
border-radius: 1px;
}
.setup-hint {
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
}
.setup-form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.setup-btn {
width: 100%;
}
.setup-error {
margin: 0;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.375rem;
}
</style>