portal: gate onboarding link; revamp apps

This commit is contained in:
Brad Stein 2026-01-02 10:27:02 -03:00
parent 2c52a23d8f
commit 712676a054
4 changed files with 215 additions and 26 deletions

View File

@ -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))

View File

@ -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">

View File

@ -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>

View File

@ -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;