Admin dashboard customization

How to add, remove, and customize tabs and action cards in the GENIE.AI admin dashboard — for deployers and frontend contributors modifying AdminDashboard.vue.

Overview

The GENIE.AI admin dashboard is a single-file Vue 3 component, AdminDashboard.vue (4465 lines, Options API) that renders a left sidebar, four stat tiles, and a tabbed content area. The eight tabs cover everything an operator needs to run the platform: System Health, Knowledge Hierarchy, Document Management, Database, Logs, Query Inspector, User Management, and Security.

This page is the how-to for customizing the dashboard. It walks through the wiring model — sidebar nav-links, the adminTabs computed property, the DsTabs component, and the activeTab === '<tab-name>' content gates — then gives step-by-step recipes for adding a tab, removing a tab, and adding a new action card (the Backup / Optimize style). It also covers per-institution access control (which goes through Keycloak, not the dashboard) and the local-dev rebuild loop.

For a reference of what each existing tab does (data sources, failure modes, sidebar layout), see Frontend → Admin Dashboard. This page is about changing the dashboard; that page is about using it.

How tabs are wired

The dashboard has three layers that must stay in sync when you add or remove a tab:

The sidebar is a flat list of <a class="nav-link"> elements, grouped under four section headers (Dashboard, Content Management, System, Settings). Each nav-link sets activeTab on click and toggles the active class when its value matches the current tab. The tab identifiers in the sidebar are: overview, hierarchy, documents, database, logs, queryInspector, users, security.

<!-- AdminDashboard.vue, sidebar nav-link pattern (lines 10-21) -->
<li class="nav-item">
  <a
    href="#"
    :class="['nav-link', { active: activeTab === 'overview' }]"
    @click.prevent="setActiveTab('overview')"
  >
    <i>📊</i>
    <span>{{ translate('admin.overview', 'Overview') }}</span>
  </a>
</li>

2. The tabs data array and adminTabs computed

The data array tabs (defined at line 1623 of AdminDashboard.vue) is the single source of truth for tab identifiers and English fallback labels. The adminTabs computed property (line 1760) maps that array through the i18n helper so the tab strip uses the locale-resolved label and exposes the tab id as value:

// AdminDashboard.vue
data() {
  return {
    // ...
    activeTab: 'overview',
    tabs: [
      { id: 'overview', label: 'System Health' },
      { id: 'hierarchy', label: 'Knowledge Hierarchy' },
      { id: 'documents', label: 'Document Management' },
      { id: 'database', label: 'Database' },
      { id: 'logs', label: 'Logs' },
      { id: 'queryInspector', label: 'Query Inspector' },
      { id: 'security', label: 'Security' },
      { id: 'users', label: 'Users' }
    ],
    // ...
  };
},
computed: {
  // AdminDashboard.vue line 1760
  adminTabs() {
    return this.tabs.map((t) => ({
      label: this.translate(`admin.tabs.${t.id}`, t.label),
      value: t.id
    }));
  },
  // ...
}

The DsTabs component receives this computed:

<!-- AdminDashboard.vue line 172 -->
<DsTabs
  :tabs="adminTabs"
  :model-value="activeTab"
  @update:model-value="setActiveTab"
>

3. Per-tab content blocks

Below DsTabs, the dashboard renders all eight tab bodies inside one container; each body uses v-if="activeTab === '<tab-id>'" so only the active tab paints. For example, the System Health body gates on overview (line 174), the Knowledge Hierarchy body on hierarchy (line 221), the Document Management body on documents (line 389), the Database body on database (line 573), the Logs body on logs (line 614), the Query Inspector body on queryInspector (line 810/819), the User Management body on users, and the Security body on security.

4. setActiveTab and loadDataForTab

When the user clicks a sidebar link or a tab, setActiveTab(tabId) (line 2041) sets this.activeTab, clears the hierarchy-form dirty flag, and then delegates per-tab data loading to loadDataForTab(tabId) (line 2075):

// AdminDashboard.vue line 2075
loadDataForTab(tabId) {
  if (tabId === 'database') this.loadDatabaseStats();
  else if (tabId === 'logs') { this.loadLogsSummary(); this.loadLogs(); }
  else if (tabId === 'security') this.loadSecurityMetrics();
  else if (tabId === 'users') this.loadUserStats();
  else if (tabId === 'documents') this.loadDocuments();
  else if (tabId === 'hierarchy' && this.knowledgeHierarchy.length === 0) {
    this.loadKnowledgeHierarchy();
  }
}

If you add a new tab, register its data-loading call here — otherwise the tab renders empty until the user does a hard refresh.

Why this layering matters

The sidebar drives activeTab directly; DsTabs is a controlled component that follows activeTab via v-model:model-value. They MUST use the same tab identifiers, otherwise the tab strip and the sidebar will desync (the tab content disappears when you click a sidebar item, or vice versa). Adding a tab identifier to the sidebar without adding the matching entry to the tabs array will hide the tab strip entry but keep the sidebar item clickable, which is a common bug.

Adding a new tab

This recipe adds a tab called monitoring that shows a placeholder tile. Repeat the same pattern for any new tab.

Pick a section (Dashboard / Content Management / System / Settings) and add the nav-link. Stay consistent with the emoji + label pattern:

<!-- AdminDashboard.vue, in the <ul class="nav-items"> of an existing section -->
<li class="nav-item">
  <a
    href="#"
    :class="['nav-link', { active: activeTab === 'monitoring' }]"
    @click.prevent="setActiveTab('monitoring')"
  >
    <i>📈</i>
    <span>{{ translate('admin.monitoring', 'Monitoring') }}</span>
  </a>
</li>

Step 2 — register the tab in the tabs array

Append the entry to the data array (line 1623) so adminTabs exposes the tab to DsTabs. The English label here is the fallback used when the i18n key is missing:

tabs: [
  // ... existing tabs ...
  { id: 'monitoring', label: 'Monitoring' }
]

Step 3 — add a content block

Inside the <DsTabs> container (around line 173) add a new sibling gated on activeTab === 'monitoring'. Use the DS primitives (DsCard, DsButton, DsStateDisplay) — do not introduce ad-hoc styling.

<div v-if="activeTab === 'monitoring'" class="dashboard-card" style="grid-column: span 2">
  <div class="card-header">
    <div class="card-title">
      {{ translate('admin.monitoring.title', 'Live Monitoring') }}
    </div>
  </div>
  <div class="monitoring-body">
    <!-- your content here -->
  </div>
</div>

Step 4 — register the data loader

If the tab needs to fetch data on activation, add a branch to loadDataForTab(tabId) (line 2075):

loadDataForTab(tabId) {
  // ... existing branches ...
  else if (tabId === 'monitoring') this.loadMonitoringData();
}

You also need to call the loader in the watch.activeTab block (line 1870) if you want it to run on hot-reload, and in mounted() / loadInitialData() if the tab should be ready before the user clicks it.

Step 5 — translate the labels

Add entries to all 14 locale files under components/gov-chat-frontend/src/i18n/locales/:

// en.js (and every other locale) — admin block
admin: {
  // ... existing keys ...
  monitoring: 'Monitoring',
  tabs: {
    // ... existing tab keys ...
    monitoring: 'Monitoring'
  }
}

The adminTabs computed uses the key admin.tabs.<id> for the tab strip label, and the sidebar uses admin.<id> (or admin.<section>.<key> for nested groups like admin.tabs.security). Add both.

English (en.js) is the source of truth. Translations for non-English locales fall back to the English label until you provide them. See Contribute → i18n guide for the locale parity workflow and which locales are production-required.

Step 6 — lint, format, and rebuild

cd components/gov-chat-frontend
npm run lint
npm run format:check
npm run test

For local dev with hot reload see Local development workflow below.

Removing a tab

Reversing the recipe: remove (in order) the data loader branch in loadDataForTab, the content block inside DsTabs, the entry in the tabs array, and the sidebar nav-link. Then delete the i18n keys from all 14 locale files.

If the tab has a backend dependency (for example the Query Inspector tab calls /api/admin/queries/inspect/* endpoints), audit call sites of those endpoints first — a tab removal can leave dead backend routes. Search the backend in components/gov-chat-backend/routes/admin-routes.js for the route prefix and decide whether to retire the route or keep it (some integrators hit the backend directly via curl).

Per-institution customization

GENIE.AI does not ship per-institution tab visibility. The dashboard renders all eight tabs for every authenticated user who reaches /admin. There is no v-if="userHasRole('admin')" on the tab containers, and the router guard (router.js lines 37-42) only checks requiresAuth: true — no role gate at the route level either:

// components/gov-chat-frontend/src/router.js line 37-42
{
  path: '/admin',
  name: 'Admin',
  component: () => import('@/components/AdminDashboard.vue'),
  meta: { requiresAuth: true, showSidebar: false }
}

Access control is therefore a Keycloak problem, not a dashboard problem. Two practical approaches, depending on what your institution needs:

NeedMechanismWhere to configure it
Hide the entire admin surface from non-admin usersAdd a Keycloak role (e.g. admin) to the user; gate the /admin route with a router.beforeEach check on realm_access.rolesEdit router.js and add the role assertion to the /admin route guard
Show all tabs but block backend writes (read-only operators)Wrap each backend POST/PUT/DELETE in requireAdmin in routes/admin-routes.js, leave GET endpoints accessible to functional-adminEdit components/gov-chat-backend/routes/admin-routes.js
Customize which tabs render for which roleAdd a v-if gate on each sidebar nav-link and tab content block, keyed on currentUser.roles.includes('...')Edit AdminDashboard.vue template + add currentUser data field

For the full role / group / client model and how to assign roles in the Keycloak admin console, see Keycloak Admin Guide → Roles and groups.

Any role check you add inside AdminDashboard.vue is client-side only. A motivated user can still hit the backend REST endpoints directly with a stolen JWT. Always enforce authorization on the backend — the dashboard gate is for UX, not security.

Local development workflow

You have two ways to see your changes: hot-reload the frontend dev server (quick iteration) or rebuild the production image (what operators actually run).

# From the repo root
cd components/gov-chat-frontend
npm install          # first time only
npm run serve        # vue-cli-service serve, default :8090 (overridable via .env NGINX_FRONTEND_PORT)

The dev server picks up AdminDashboard.vue edits on save. Browse to http://localhost:8090/admin, log in as a Keycloak admin user, and verify your new tab renders, the sidebar nav-link highlights, and the tab strip switches correctly. Browser DevTools → Vue DevTools lets you inspect activeTab and adminTabs live.

Rebuild the production image (for the docker compose stack)

If you are running the full stack via docker compose up -d, the frontend container is built from components/gov-chat-frontend/Dockerfile. After editing AdminDashboard.vue you must rebuild:

# From the repo root
docker compose build frontend
docker compose up -d frontend

If you also touched the backend (e.g. added a new admin route), rebuild that service too:

docker compose build backend
docker compose up -d backend

For a full clean rebuild of the dependency stack:

docker compose down -v
docker compose up -d --build

See Get started → Quickstart for developers for the full local stack bring-up, and Get started → Quickstart for deployers for the production-style swarm bring-up.

Adding a new dashboard card

Action cards (the small clickable tiles on the Database tab, e.g. Backup and Optimize at lines 582-597) follow a tighter pattern than tabs. They live inside an existing tab and call a method on click — there is no i18n key for the tab id and no entry in the tabs array.

Step 1 — add the card markup

Inside the relevant tab body, after the existing db-action-card siblings:

<!-- AdminDashboard.vue, inside the database tab body -->
<div class="db-action-card" @click="reindexDatabase">
  <h4>{{ translate('admin.dbActions.reindex', 'Reindex') }}</h4>
  <p>{{ translate('admin.dbActions.reindexDesc', 'Rebuild search indexes') }}</p>
</div>

Keep the structure shallow: a <h4> for the title and a <p> for the description. Use the existing admin.dbActions.* i18n namespace so all action cards stay translatable together.

Step 2 — wire the click handler

Add a method in the methods: block that delegates to executeOperation:

// AdminDashboard.vue methods
async reindexDatabase() {
  this.executeOperation('reindexDatabase', async () => {
    await adminService.reindexDatabase();
  });
}

executeOperation is the existing helper that toggles the operation-result dialog and surfaces errors — reuse it instead of writing your own try/catch. Look at backupDatabase (line 2374) and optimizeDatabase for the canonical pattern.

Step 3 — translate the labels

Add reindex and reindexDesc to the admin.dbActions block of every locale file under components/gov-chat-frontend/src/i18n/locales/. The English entries are the source of truth.

Step 4 — test

cd components/gov-chat-frontend
npm run test

The Dashboard test suite (src/__tests__/components/AdminDashboard.test.js if present, or the broader component suite) will catch typos in the method name and unmapped i18n keys via snapshot tests.

Troubleshooting

SymptomLikely causeFix
New tab does not appear in the tab stripEntry missing from the tabs arrayAdd { id: '<your-id>', label: '<English fallback>' } to the array at line 1623
Sidebar nav-link works but tab content is blankContent block not added or v-if predicate mismatchConfirm the content block uses v-if="activeTab === '<your-id>'" with the exact same id as the nav-link and the tabs entry
Clicking the new sidebar item throws Cannot read property of nullloadDataForTab not registered for the new tabAdd a branch in loadDataForTab (line 2075)
Tab label shows the raw i18n key, e.g. admin.tabs.monitoringMissing locale entryAdd the admin.tabs.<id> key to en.js (and other locales); the translate() helper falls back to the English label in the tabs array, not the key
Tab strip and sidebar desync (one highlights, the other does not)Identifier mismatch between sidebar, tabs array, and v-if blocksGrep the file for the tab id (grep -n monitoring AdminDashboard.vue); all three locations must use the identical string
Edit hot-reloads but changes do not appear in the browserBrowser cacheHard reload (Cmd+Shift+R / Ctrl+Shift+R) or clear the Vue DevTools component state
Frontend image rebuild failsOut-of-date package-lock.jsonRun npm install in components/gov-chat-frontend then re-run docker compose build frontend
Backend endpoint added but /admin tab returns 404Container not rebuilt after route file changedocker compose build backend && docker compose up -d backend
Lint fails on the new templateVue style or i18n key violationcd components/gov-chat-frontend && npm run lint:fix — common issues: hardcoded user-facing strings (use translate()), DS primitive not used (use DsButton instead of <button>)
Vue warns “Duplicate keys detected”Two tabs in the tabs array share the same idDedup the ids