import { createApp, computed, onMounted, ref } from "vue";
import "gnexus-ui-kit/dist/css/kit.css";
import "gnexus-ui-kit/dist/assets/fonts/phosphor-icons/src/css/icons.css";
import {
GnBadge,
GnButton,
GnDescriptionList,
GnEmptyState,
GnMetricCard,
GnPageHeader,
GnTabs,
GnToolbar,
GnToastProvider
} from "gnexus-ui-kit/vue";
import "./styles.css";
const apiGet = async (path) => {
const response = await fetch(`/api${path}`);
if (!response.ok) {
throw new Error(`${path} returned ${response.status}`);
}
return response.json();
};
const AppScreen = {
components: {
GnBadge,
GnButton,
GnDescriptionList,
GnEmptyState,
GnMetricCard,
GnPageHeader,
GnTabs,
GnToolbar
},
setup() {
const activeTab = ref("docs");
const loading = ref(true);
const error = ref("");
const health = ref(null);
const validation = ref(null);
const freshness = ref(null);
const docs = ref([]);
const inventoryTypes = ref([]);
const selectedInventoryType = ref("");
const inventoryLoading = ref(false);
const inventoryError = ref("");
const inventoryRecords = ref([]);
const selectedInventoryId = ref("");
const changes = ref([]);
const gitStatus = ref(null);
const tabs = [
{ id: "docs", label: "Docs", icon: "ph-files" },
{ id: "inventory", label: "Inventory", icon: "ph-database" },
{ id: "changes", label: "Changes", icon: "ph-git-pull-request" },
{ id: "git", label: "Git", icon: "ph-git-branch" }
];
const validationVariant = computed(() =>
validation.value?.status === "ok" ? "success" : "danger"
);
const freshnessVariant = computed(() =>
freshness.value?.status === "ok" ? "success" : "danger"
);
const overview = computed(() => [
{ key: "repo", term: "Repository", value: health.value?.repo_root || "unknown" },
{ key: "validation", term: "Validation", value: validation.value?.status || "unknown" },
{ key: "freshness", term: "Freshness", value: freshness.value?.status || "unknown" }
]);
const selectedInventoryRecord = computed(() => {
if (!selectedInventoryId.value) {
return null;
}
return inventoryRecords.value.find((item) => item?.id === selectedInventoryId.value) || null;
});
const selectedInventoryRecordJson = computed(() => {
if (!selectedInventoryRecord.value) {
return "";
}
return JSON.stringify(selectedInventoryRecord.value, null, 2);
});
const inventoryRecordTitle = (item) => item?.name || item?.fqdn || item?.id || "unknown";
const loadInventoryType = async (type) => {
selectedInventoryType.value = type;
selectedInventoryId.value = "";
inventoryLoading.value = true;
inventoryError.value = "";
try {
const records = await apiGet(`/inventory/${type}`);
inventoryRecords.value = Array.isArray(records) ? records : [];
selectedInventoryId.value = inventoryRecords.value[0]?.id || "";
} catch (caught) {
inventoryRecords.value = [];
inventoryError.value =
caught instanceof Error ? caught.message : `Failed to load inventory ${type}`;
} finally {
inventoryLoading.value = false;
}
};
const selectInventoryRecord = (item) => {
selectedInventoryId.value = item?.id || "";
};
const load = async () => {
loading.value = true;
error.value = "";
try {
const [
healthData,
validationData,
freshnessData,
docsData,
inventoryData,
changesData,
gitData
] = await Promise.all([
apiGet("/health"),
apiGet("/validate"),
apiGet("/health/freshness"),
apiGet("/docs"),
apiGet("/inventory"),
apiGet("/changes"),
apiGet("/git/status")
]);
health.value = healthData;
validation.value = validationData;
freshness.value = freshnessData;
docs.value = docsData;
inventoryTypes.value = inventoryData;
changes.value = changesData;
gitStatus.value = gitData;
if (!selectedInventoryType.value && inventoryData.length) {
await loadInventoryType(inventoryData[0]);
}
} catch (caught) {
error.value = caught instanceof Error ? caught.message : "Failed to load API data";
} finally {
loading.value = false;
}
};
onMounted(load);
return {
activeTab,
changes,
docs,
error,
freshness,
freshnessVariant,
gitStatus,
inventoryError,
inventoryLoading,
inventoryRecordTitle,
inventoryRecords,
inventoryTypes,
load,
loadInventoryType,
loading,
overview,
selectedInventoryId,
selectedInventoryRecord,
selectedInventoryRecordJson,
selectedInventoryType,
selectInventoryRecord,
tabs,
validation,
validationVariant
};
},
template: `
<main class="gnb-shell">
<GnPageHeader
title="Gnexus Book"
subtitle="Infrastructure documentation maintenance console"
kicker="Knowledge Base"
accent
>
<template #actions>
<GnButton variant="secondary" icon="ph-arrow-clockwise" @click="load">
Refresh
</GnButton>
</template>
</GnPageHeader>
<section class="gnb-status">
<GnMetricCard label="Documents" :value="String(docs.length)" icon="ph-files" meta="Markdown pages" />
<GnMetricCard label="Inventory" :value="String(inventoryTypes.length)" icon="ph-database" meta="YAML types" />
<GnMetricCard label="Changes" :value="String(changes.length)" icon="ph-git-pull-request" meta="Pending records" />
</section>
<section class="section">
<GnToolbar title="System Status" :meta="loading ? 'Loading' : 'Live API'">
<template #actions>
<GnBadge :variant="validationVariant">validation {{ validation?.status || 'unknown' }}</GnBadge>
<GnBadge :variant="freshnessVariant">freshness {{ freshness?.status || 'unknown' }}</GnBadge>
</template>
</GnToolbar>
<GnDescriptionList :items="overview" />
<div v-if="error" class="gnb-error">{{ error }}</div>
</section>
<section class="section">
<GnTabs v-model="activeTab" :items="tabs">
<template #docs>
<div v-if="docs.length" class="gnb-list">
<article v-for="doc in docs" :key="doc.path" class="gnb-row">
<strong>{{ doc.title }}</strong>
<span>{{ doc.path }}</span>
</article>
</div>
<GnEmptyState v-else title="No documents loaded" text="The backend did not return document records." />
</template>
<template #inventory>
<div v-if="inventoryTypes.length" class="gnb-inventory">
<aside class="gnb-list">
<button
v-for="type in inventoryTypes"
:key="type"
type="button"
class="gnb-row gnb-row-button"
:class="{ 'is-active': type === selectedInventoryType }"
@click="loadInventoryType(type)"
>
<strong>{{ type }}</strong>
<span>/inventory/{{ type }}</span>
</button>
</aside>
<section class="gnb-detail">
<GnToolbar
:title="selectedInventoryType || 'Inventory'"
:meta="inventoryLoading ? 'Loading' : String(inventoryRecords.length) + ' records'"
/>
<div v-if="inventoryError" class="gnb-error">{{ inventoryError }}</div>
<div v-else-if="inventoryRecords.length" class="gnb-detail-grid">
<div class="gnb-list">
<button
v-for="item in inventoryRecords"
:key="item.id || inventoryRecordTitle(item)"
type="button"
class="gnb-row gnb-row-button"
:class="{ 'is-active': item.id === selectedInventoryId }"
@click="selectInventoryRecord(item)"
>
<strong>{{ inventoryRecordTitle(item) }}</strong>
<span>{{ item.id || 'no-id' }}</span>
</button>
</div>
<pre class="gnb-json">{{ selectedInventoryRecordJson }}</pre>
</div>
<GnEmptyState v-else title="No records" text="This inventory type is currently empty." />
</section>
</div>
<GnEmptyState v-else title="No inventory loaded" text="The backend did not return inventory types." />
</template>
<template #changes>
<div v-if="changes.length" class="gnb-list">
<article v-for="change in changes" :key="change.id" class="gnb-row">
<strong>{{ change.summary }}</strong>
<span>{{ change.status }} · {{ change.kind }} · {{ change.target }}</span>
</article>
</div>
<GnEmptyState v-else title="No pending changes" text="Pending changes will appear here after agents propose updates." />
</template>
<template #git>
<div v-if="gitStatus?.entries?.length" class="gnb-list">
<article v-for="entry in gitStatus.entries" :key="entry.path" class="gnb-row">
<strong>{{ entry.status }}</strong>
<span>{{ entry.path }}</span>
</article>
</div>
<GnEmptyState v-else title="Working tree clean" text="No tracked or untracked changes are currently reported." />
</template>
</GnTabs>
</section>
</main>
`
};
const App = {
components: {
AppScreen,
GnToastProvider
},
template: `
<GnToastProvider>
<AppScreen />
</GnToastProvider>
`
};
createApp(App).mount("#app");