feat(infra): add api.mana.how route + Prometheus scrape targets for Go services

- Cloudflare Tunnel: api.mana.how → localhost:3060 (Go API Gateway)
- Prometheus: scrape targets for mana-api-gateway:3060 and mana-matrix-bot:4000

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-27 21:27:04 +01:00
parent c81527c57c
commit a31ccc6c62
11 changed files with 434 additions and 95 deletions

View file

@ -543,6 +543,8 @@ Logged in: App → IndexedDB → UI → SyncEngine → mana-sync (Go) → Postg
| Mukke | songs, playlists, playlistSongs, projects, markers | Done |
| Context | spaces, documents | Done |
| Photos | albums, albumItems, favorites, tags, photoTags | Done |
| SkilltTree | skills, activities, achievements | Done |
| CityCorners | locations, favorites | Done |
### Dev Commands (Local-First Stack)

View file

@ -33,6 +33,7 @@
"dependencies": {
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/local-store": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-error-tracking": "workspace:*",
"@manacore/shared-feedback-service": "workspace:*",

View file

@ -0,0 +1,49 @@
/**
* Guest seed data for the CityCorners app.
*
* Provides iconic Konstanz locations for the onboarding experience.
*/
import type { LocalLocation } from './local-store';
export const guestLocations: LocalLocation[] = [
{
id: 'loc-muenster',
name: 'Konstanzer Münster',
category: 'sight',
description:
'Das Münster Unserer Lieben Frau ist die ehemalige Bischofskirche des Bistums Konstanz und Wahrzeichen der Stadt.',
address: 'Münsterplatz 1, 78462 Konstanz',
latitude: 47.6603,
longitude: 9.1752,
},
{
id: 'loc-imperia',
name: 'Imperia',
category: 'sight',
description:
'Die 9 Meter hohe Statue im Hafen von Konstanz dreht sich einmal in 4 Minuten um ihre Achse.',
address: 'Hafen, 78462 Konstanz',
latitude: 47.6596,
longitude: 9.1789,
},
{
id: 'loc-insel',
name: 'Mainau Blumeninsel',
category: 'park',
description:
'Die Blumeninsel Mainau im Bodensee ist bekannt für ihre Gärten, das Schmetterlingshaus und das Barockschloss.',
address: 'Mainau 1, 78465 Konstanz',
latitude: 47.7051,
longitude: 9.1919,
},
{
id: 'loc-strandbad',
name: 'Strandbad Horn',
category: 'beach',
description: 'Beliebtes Freibad am Bodensee mit Sandstrand und Blick auf die Alpen.',
address: 'Eichhornstraße 100, 78464 Konstanz',
latitude: 47.6753,
longitude: 9.2001,
},
];

View file

@ -0,0 +1,63 @@
/**
* CityCorners Local-First Data Layer
*
* Locations and favorites stored locally for offline browsing.
* Location lookup and web search remain server-side.
*/
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
import { guestLocations } from './guest-seed';
// ─── Types ──────────────────────────────────────────────────
export interface LocalLocation extends BaseRecord {
name: string;
category:
| 'sight'
| 'restaurant'
| 'shop'
| 'museum'
| 'cafe'
| 'bar'
| 'park'
| 'beach'
| 'hotel'
| 'event_venue'
| 'viewpoint';
description?: string | null;
address?: string | null;
latitude?: number | null;
longitude?: number | null;
imageUrl?: string | null;
timeline?: Array<{ year: number; event: string }> | null;
}
export interface LocalFavorite extends BaseRecord {
locationId: string;
}
// ─── Store ──────────────────────────────────────────────────
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
export const citycornersStore = createLocalStore({
appId: 'citycorners',
collections: [
{
name: 'locations',
indexes: ['category', 'name'],
guestSeed: guestLocations,
},
{
name: 'favorites',
indexes: ['locationId'],
},
],
sync: {
serverUrl: SYNC_SERVER_URL,
},
});
// Typed collection accessors
export const locationCollection = citycornersStore.collection<LocalLocation>('locations');
export const favoriteCollection = citycornersStore.collection<LocalFavorite>('favorites');

View file

@ -9,6 +9,9 @@
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { favoritesStore } from '$lib/stores/favorites.svelte';
import { AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { citycornersStore } from '$lib/data/local-store';
import { THEME_DEFINITIONS, DEFAULT_THEME_VARIANTS } from '@manacore/shared-theme';
import type { ThemeVariant } from '@manacore/shared-theme';
import { getPillAppItems } from '@manacore/shared-branding';
@ -172,109 +175,126 @@
showNav = !showNav;
}
onMount(async () => {
const savedNav = localStorage.getItem('citycorners-nav-visible');
if (savedNav !== null) showNav = savedNav !== 'false';
let showGuestWelcome = $state(false);
async function handleAuthReady() {
await citycornersStore.initialize();
if (authStore.isAuthenticated) {
citycornersStore.startSync(() => authStore.getValidToken());
await tagStore.fetchTags();
}
});
if (!authStore.isAuthenticated && shouldShowGuestWelcome('citycorners')) {
showGuestWelcome = true;
}
const savedNav = localStorage.getItem('citycorners-nav-visible');
if (savedNav !== null) showNav = savedNav !== 'false';
}
</script>
<div class="layout-container">
{#if showNav}
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="CityCorners"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#2563eb"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
themesHref="/themes"
helpHref="/help"
profileHref="/profile"
/>
{/if}
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Quick Search Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
placeholder={$_('search.placeholder')}
emptyText={$_('search.noResults')}
searchingText={$_('search.searching')}
locale={$locale || 'de'}
appIcon="mappin"
bottomOffset={inputBarBottomOffset}
hasFabRight={true}
/>
<button
class="pillnav-fab"
onclick={handleNavToggle}
title={showNav ? $_('nav.hideNav') : $_('nav.showNav')}
>
{#if !showNav}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
{:else}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
<div class="layout-container">
{#if showNav}
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="CityCorners"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#2563eb"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
themesHref="/themes"
helpHref="/help"
profileHref="/profile"
/>
{/if}
</button>
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}
</div>
</main>
</div>
<!-- TagStrip (above PillNav, toggled via Tags pill) -->
{#if isTagStripVisible}
<TagStrip
tags={tagStore.tags.map((t) => ({
id: t.id,
name: t.name,
color: t.color || '#3b82f6',
}))}
selectedIds={[]}
onToggle={() => {}}
onClear={() => {}}
managementHref="/tags"
loading={tagStore.loading}
/>
{/if}
<!-- Quick Search Bar -->
<QuickInputBar
onSearch={handleSearch}
onSelect={handleSelect}
placeholder={$_('search.placeholder')}
emptyText={$_('search.noResults')}
searchingText={$_('search.searching')}
locale={$locale || 'de'}
appIcon="mappin"
bottomOffset={inputBarBottomOffset}
hasFabRight={true}
/>
<button
class="pillnav-fab"
onclick={handleNavToggle}
title={showNav ? $_('nav.hideNav') : $_('nav.showNav')}
>
{#if !showNav}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
{:else}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" class="fab-icon">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
{/if}
</button>
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}
</div>
</main>
</div>
<GuestWelcomeModal
appId="citycorners"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => goto('/login')}
onRegister={() => goto('/register')}
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
/>
</AuthGate>
<style>
.layout-container {

View file

@ -43,6 +43,7 @@
"@manacore/shared-help-types": "workspace:*",
"@manacore/shared-help-ui": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/local-store": "workspace:*",
"@manacore/shared-app-onboarding": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",

View file

@ -0,0 +1,51 @@
/**
* Guest seed data for the SkilltTree app.
*
* Provides a demo skill with an activity to showcase the leveling system.
*/
import type { LocalSkill, LocalActivity } from './local-store';
const DEMO_SKILL_ID = 'demo-coding';
export const guestSkills: LocalSkill[] = [
{
id: DEMO_SKILL_ID,
name: 'Programmieren',
description: 'Software-Entwicklung und Coding-Skills',
branch: 'intellect',
icon: '💻',
currentXp: 150,
totalXp: 150,
level: 1,
},
{
id: 'demo-fitness',
name: 'Fitness',
description: 'Körperliche Fitness und Training',
branch: 'body',
icon: '💪',
currentXp: 50,
totalXp: 50,
level: 0,
},
];
export const guestActivities: LocalActivity[] = [
{
id: 'activity-1',
skillId: DEMO_SKILL_ID,
xpEarned: 100,
description: 'TypeScript-Projekt aufgesetzt',
duration: 60,
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
},
{
id: 'activity-2',
skillId: DEMO_SKILL_ID,
xpEarned: 50,
description: 'Unit Tests geschrieben',
duration: 30,
timestamp: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
},
];

View file

@ -0,0 +1,71 @@
/**
* SkilltTree Local-First Data Layer (via @manacore/local-store)
*
* Adds unified sync support alongside the existing idb-based IndexedDB storage.
* Skills, activities, and achievements are synced to the server when authenticated.
*/
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
import { guestSkills, guestActivities } from './guest-seed';
// ─── Types ──────────────────────────────────────────────────
export interface LocalSkill extends BaseRecord {
name: string;
description: string;
branch: 'intellect' | 'body' | 'creativity' | 'social' | 'practical' | 'mindset' | 'custom';
parentId?: string | null;
icon: string;
color?: string | null;
currentXp: number;
totalXp: number;
level: number;
}
export interface LocalActivity extends BaseRecord {
skillId: string;
xpEarned: number;
description: string;
duration?: number | null;
timestamp: string;
}
export interface LocalAchievement extends BaseRecord {
key: string;
name: string;
description: string;
icon: string;
unlockedAt: string;
}
// ─── Store ──────────────────────────────────────────────────
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
export const skilltreeStore = createLocalStore({
appId: 'skilltree',
collections: [
{
name: 'skills',
indexes: ['branch', 'parentId', 'level'],
guestSeed: guestSkills,
},
{
name: 'activities',
indexes: ['skillId', 'timestamp'],
guestSeed: guestActivities,
},
{
name: 'achievements',
indexes: ['key', 'unlockedAt'],
},
],
sync: {
serverUrl: SYNC_SERVER_URL,
},
});
// Typed collection accessors
export const skillCollection = skilltreeStore.collection<LocalSkill>('skills');
export const activityCollection = skilltreeStore.collection<LocalActivity>('activities');
export const achievementCollection = skilltreeStore.collection<LocalAchievement>('achievements');

View file

@ -8,10 +8,17 @@
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { skilltreeOnboarding } from '$lib/stores/app-onboarding.svelte';
import { SessionExpiredBanner, AuthGate } from '@manacore/shared-auth-ui';
import { skilltreeStore } from '$lib/data/local-store';
let { children } = $props();
async function handleAuthReady() {
// Initialize unified local-store (IndexedDB + sync)
await skilltreeStore.initialize();
if (authStore.isAuthenticated) {
skilltreeStore.startSync(() => authStore.getValidToken());
}
// Initialize existing idb-based stores
await skillStore.initialize();
await achievementStore.initialize();
}

View file

@ -14,6 +14,10 @@ ingress:
- hostname: auth.mana.how
service: http://localhost:3001
# API Gateway (Go)
- hostname: api.mana.how
service: http://localhost:3060
# Chat App
- hostname: chat.mana.how
service: http://localhost:5010
@ -158,5 +162,17 @@ ingress:
- hostname: docs.mana.how
service: http://localhost:4400
# GPU Server (Windows PC, LAN: 192.168.178.11)
- hostname: gpu-llm.mana.how
service: http://192.168.178.11:3025
- hostname: gpu-stt.mana.how
service: http://192.168.178.11:3020
- hostname: gpu-tts.mana.how
service: http://192.168.178.11:3022
- hostname: gpu-img.mana.how
service: http://192.168.178.11:3023
- hostname: gpu-ollama.mana.how
service: http://192.168.178.11:11434
# Catch-all
- service: http_status:404

View file

@ -186,6 +186,64 @@ scrape_configs:
metrics_path: '/_synapse/metrics'
scrape_interval: 30s
# ============================================
# GPU Server (Windows PC, LAN: 192.168.178.11)
# ============================================
# GPU: LLM Gateway
- job_name: 'gpu-llm'
static_configs:
- targets: ['192.168.178.11:3025']
labels:
instance: 'gpu-server'
metrics_path: '/metrics'
scrape_interval: 15s
# GPU: Speech-to-Text (WhisperX)
- job_name: 'gpu-stt'
static_configs:
- targets: ['192.168.178.11:3020']
labels:
instance: 'gpu-server'
metrics_path: '/health'
scrape_interval: 30s
# GPU: Text-to-Speech
- job_name: 'gpu-tts'
static_configs:
- targets: ['192.168.178.11:3022']
labels:
instance: 'gpu-server'
metrics_path: '/health'
scrape_interval: 30s
# GPU: Image Generation (FLUX.2)
- job_name: 'gpu-image-gen'
static_configs:
- targets: ['192.168.178.11:3023']
labels:
instance: 'gpu-server'
metrics_path: '/health'
scrape_interval: 30s
# ============================================
# Go Infrastructure Services
# ============================================
# API Gateway (Go)
- job_name: 'mana-api-gateway'
static_configs:
- targets: ['mana-api-gateway:3060']
metrics_path: '/metrics'
scrape_interval: 15s
# Matrix Bot (Go) — consolidated 21 bots
- job_name: 'mana-matrix-bot'
static_configs:
- targets: ['mana-matrix-bot:4000']
metrics_path: '/metrics'
scrape_interval: 30s
# ============================================
# Pushgateway (deploy metrics, batch jobs)
# ============================================