portal: gate onboarding link; revamp apps
This commit is contained in:
parent
2c52a23d8f
commit
712676a054
@ -39,6 +39,7 @@ def _client_ip() -> str:
|
||||
|
||||
|
||||
ONBOARDING_STEPS: tuple[str, ...] = (
|
||||
"keycloak_password_changed",
|
||||
"vaultwarden_master_password",
|
||||
"element_recovery_key",
|
||||
"element_recovery_key_stored",
|
||||
@ -223,7 +224,7 @@ def register(app) -> None:
|
||||
"status": status,
|
||||
"username": row.get("username") or "",
|
||||
}
|
||||
if status in {"accounts_building", "awaiting_onboarding", "ready"}:
|
||||
if status in {"awaiting_onboarding", "ready"}:
|
||||
response["onboarding_url"] = f"/onboarding?code={code}"
|
||||
if status in {"awaiting_onboarding", "ready"}:
|
||||
completed = sorted(_fetch_completed_onboarding_steps(conn, code))
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
<nav class="links">
|
||||
<RouterLink to="/" class="nav-link">Home</RouterLink>
|
||||
<RouterLink to="/about" class="nav-link">About</RouterLink>
|
||||
<a href="https://cloud.bstein.dev" class="nav-link strong" target="_blank" rel="noreferrer">Cloud</a>
|
||||
|
||||
<template v-if="auth.enabled">
|
||||
<template v-if="auth.authenticated">
|
||||
|
||||
@ -4,34 +4,129 @@
|
||||
<div>
|
||||
<p class="eyebrow">Atlas</p>
|
||||
<h1>Apps</h1>
|
||||
<p class="lede">Quick links to the lab services. Some apps open in a new tab for security reasons.</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="pill mono" href="https://cloud.bstein.dev" target="_blank" rel="noreferrer">Open Nextcloud</a>
|
||||
<a class="pill mono" href="https://sso.bstein.dev" target="_blank" rel="noreferrer">Open Keycloak</a>
|
||||
<p class="lede">
|
||||
Service shortcuts for Atlas. Nextcloud is the hub, but everything is available directly too.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<section v-for="category in categories" :key="category.title" class="card category">
|
||||
<div class="section-head">
|
||||
<h2>Service Grid</h2>
|
||||
<span class="pill mono">apps + pipelines + observability</span>
|
||||
<div>
|
||||
<h2>{{ category.title }}</h2>
|
||||
<p class="muted">{{ category.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tiles">
|
||||
<a
|
||||
v-for="app in category.apps"
|
||||
:key="app.name"
|
||||
class="tile"
|
||||
:href="app.url"
|
||||
:target="app.target"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div class="tile-title">{{ app.name }}</div>
|
||||
<div class="tile-desc">{{ app.description }}</div>
|
||||
</a>
|
||||
</div>
|
||||
<ServiceGrid :services="displayServices" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import ServiceGrid from "../components/ServiceGrid.vue";
|
||||
import { fallbackServices } from "../data/sample.js";
|
||||
|
||||
const props = defineProps({
|
||||
serviceData: Object,
|
||||
});
|
||||
|
||||
const displayServices = computed(() => props.serviceData?.services || fallbackServices().services);
|
||||
const categories = [
|
||||
{
|
||||
title: "Cloud",
|
||||
description: "Files, photos, mail, calendars, and documents — the primary hub for Atlas users.",
|
||||
apps: [
|
||||
{
|
||||
name: "Nextcloud",
|
||||
url: "https://cloud.bstein.dev",
|
||||
target: "_blank",
|
||||
description: "Your personal cloud storage and productivity suite.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Security",
|
||||
description: "Modern password security and secrets tooling.",
|
||||
apps: [
|
||||
{
|
||||
name: "Vaultwarden",
|
||||
url: "https://vault.bstein.dev",
|
||||
target: "_blank",
|
||||
description: "Password manager (Bitwarden-compatible).",
|
||||
},
|
||||
{
|
||||
name: "Vault",
|
||||
url: "https://secret.bstein.dev",
|
||||
target: "_blank",
|
||||
description: "Secrets management for infrastructure and apps.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Communications",
|
||||
description: "Chat and meetings.",
|
||||
apps: [
|
||||
{
|
||||
name: "AI Chat",
|
||||
url: "/ai/chat",
|
||||
target: "_self",
|
||||
description: "Chat with Atlas AI (GPU-accelerated).",
|
||||
},
|
||||
{
|
||||
name: "Meet",
|
||||
url: "https://meet.bstein.dev",
|
||||
target: "_blank",
|
||||
description: "Video meetings (Jitsi).",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Streaming",
|
||||
description: "Family media streaming and upload workflows.",
|
||||
apps: [
|
||||
{
|
||||
name: "Jellyfin",
|
||||
url: "https://stream.bstein.dev",
|
||||
target: "_blank",
|
||||
description: "Stream videos to desktop, mobile, and TV.",
|
||||
},
|
||||
{
|
||||
name: "Pegasus",
|
||||
url: "https://pegasus.bstein.dev",
|
||||
target: "_blank",
|
||||
description: "Mobile-friendly upload/publish into Jellyfin.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Dev",
|
||||
description: "Source control, CI, registry, GitOps, and observability.",
|
||||
apps: [
|
||||
{ name: "Gitea", url: "https://scm.bstein.dev", target: "_blank", description: "Git hosting and collaboration." },
|
||||
{ name: "Jenkins", url: "https://ci.bstein.dev", target: "_blank", description: "CI pipelines and automation." },
|
||||
{ name: "Harbor", url: "https://registry.bstein.dev", target: "_blank", description: "Artifact registry." },
|
||||
{ name: "GitOps", url: "https://cd.bstein.dev", target: "_blank", description: "GitOps UI for Flux." },
|
||||
{ name: "Grafana", url: "https://metrics.bstein.dev", target: "_blank", description: "Dashboards and monitoring." },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Crypto",
|
||||
description: "Local infrastructure for crypto workloads.",
|
||||
apps: [
|
||||
{
|
||||
name: "Monero Node",
|
||||
url: "https://monero.bstein.dev",
|
||||
target: "_blank",
|
||||
description: "Faster sync using the Atlas Monero node.",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -49,6 +144,58 @@ const displayServices = computed(() => props.serviceData?.services || fallbackSe
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.category {
|
||||
padding: 18px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin: 6px 0 0;
|
||||
color: var(--text-muted);
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: 14px 14px 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
transition: border-color 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.tile:hover {
|
||||
border-color: rgba(120, 180, 255, 0.35);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.tile-title {
|
||||
font-weight: 750;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.tile-desc {
|
||||
margin-top: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
@ -67,10 +214,4 @@ h1 {
|
||||
color: var(--text-muted);
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -23,6 +23,13 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="requestUsername" class="status-meta">
|
||||
<div class="meta-row">
|
||||
<span class="label mono">Username</span>
|
||||
<span class="value mono">{{ requestUsername }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="status === 'pending'" class="steps">
|
||||
<h3>Awaiting approval</h3>
|
||||
<p class="muted">An Atlas admin has to approve this request before an account can be provisioned.</p>
|
||||
@ -53,6 +60,22 @@
|
||||
</div>
|
||||
|
||||
<ul class="checklist">
|
||||
<li class="check-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isStepDone('keycloak_password_changed')"
|
||||
:disabled="!auth.authenticated || loading"
|
||||
@change="toggleStep('keycloak_password_changed', $event)"
|
||||
/>
|
||||
<span>Change your Keycloak password</span>
|
||||
</label>
|
||||
<p class="muted">
|
||||
Set a strong account password in Keycloak. Use the
|
||||
<a href="https://sso.bstein.dev/realms/atlas/account" target="_blank" rel="noreferrer">account console</a>.
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="check-item">
|
||||
<label>
|
||||
<input
|
||||
@ -127,6 +150,7 @@ import { auth, authFetch, login } from "../auth";
|
||||
const route = useRoute();
|
||||
|
||||
const requestCode = ref("");
|
||||
const requestUsername = ref("");
|
||||
const status = ref("");
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
@ -170,6 +194,7 @@ async function check() {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
||||
status.value = data.status || "unknown";
|
||||
requestUsername.value = data.username || "";
|
||||
onboarding.value = data.onboarding || { required_steps: [], completed_steps: [] };
|
||||
} catch (err) {
|
||||
error.value = err.message || "Failed to check status";
|
||||
@ -264,6 +289,29 @@ h1 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.status-meta {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.meta-row .label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.meta-row .value {
|
||||
color: var(--text-strong);
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user