chore: cleanup leftover dirs from ManaCore→Mana rename + document apps/api

Removed:
- apps/manacore/ — three Svelte files were byte-identical duplicates of
  the apps/mana/ versions, leftover from the 2025 rename. Untracked .env
  files in the same dir were also cleared.
- 21 empty apps/*/apps/web-archived/ directories — leftover from the
  unification move, never tracked in git.
- services/it-landing/ — empty directory, picked up by the services/*
  workspace glob for no reason.
- apps/news/apps/server-archived/ — empty.

Fixed:
- scripts/mac-mini/status.sh: COMPOSE_PROJECT_NAME fallback was still
  manacore-monorepo from before the rename.

Documented:
- Root CLAUDE.md now describes apps/api/ (the @mana/api unified backend)
  as a top-level peer to apps/mana/. It was completely missing from the
  trimmed CLAUDE.md, which made the layout look frontend-only.
This commit is contained in:
Till JS 2026-04-08 12:12:02 +02:00
parent ed8ab44832
commit b3523f8bdc
5 changed files with 13 additions and 442 deletions

View file

@ -4,18 +4,26 @@ Guidance for Claude Code when working in this repo.
## Monorepo Overview
pnpm workspace monorepo. The main surface is the **unified web app** at `apps/mana/apps/web` — one SvelteKit build serving 27+ product modules under `mana.how`, sharing one IndexedDB, one auth session, one deployment.
pnpm workspace monorepo with two consolidated tops:
- **`apps/mana/apps/web`** — unified SvelteKit frontend serving 27+ product modules under `mana.how`. One build, one IndexedDB, one auth session, one deployment.
- **`apps/api`** (`@mana/api`) — unified Hono/Bun backend API server. Consolidates per-module compute servers; routes registered under `/api/v1/{module}/*`.
Per-product directories under `apps/{product}/` still exist for landing pages, mobile apps, and product-specific packages, but the active web frontend and API both live in the two consolidated apps above.
- **Package Manager:** pnpm 9.15.0
- **Build System:** Turborepo
- **Node:** 20+
- **Primary doc:** [`apps/mana/CLAUDE.md`](apps/mana/CLAUDE.md) — read this for module structure, data layer, encryption, and routing details.
- **Primary doc:** [`apps/mana/CLAUDE.md`](apps/mana/CLAUDE.md) — module structure, data layer, encryption, routing.
### Repo layout
```
apps/ # Product apps. Most are integrated as modules in apps/mana/apps/web.
# Standalone (own container, not unified): matrix, manavoxel
apps/
├── mana/ # Unified frontend (SvelteKit web + Expo mobile + Astro landing)
├── api/ # Unified backend API (Hono/Bun) — @mana/api
├── {product}/ # Per-product landing pages, mobile apps, packages
│ # Standalone (own container, not unified): matrix, manavoxel
games/ # arcade, voxelava, whopixels, worldream
services/ # Backend services (Hono/Bun, Go, Python) — see list below
packages/ # Shared workspace packages (@mana/*)

View file

@ -1,195 +0,0 @@
<script lang="ts">
import { networkStore } from '$lib/stores/network.svelte';
import { _ } from 'svelte-i18n';
let dismissed = $state(false);
// Reset dismissed state when coming back online
$effect(() => {
if (networkStore.isOnline) {
dismissed = false;
}
});
const isOffline = $derived(!networkStore.isOnline);
const showBanner = $derived(isOffline && !dismissed);
// Only show sync indicators when online (not when offline — pending count stays >0 offline)
const showSyncBadge = $derived(
!isOffline && networkStore.pendingCount > 0 && networkStore.syncStatus !== 'syncing'
);
const showSyncing = $derived(!isOffline && networkStore.syncStatus === 'syncing');
</script>
{#if showBanner}
<div class="offline-banner" role="status" aria-live="polite">
<div class="offline-icon">
<svg width="16" height="16" viewBox="0 0 256 256" fill="currentColor">
<path
d="M213.92,210.62a8,8,0,1,1-11.84,10.76l-39-42.86A83.93,83.93,0,0,0,48,128a84.37,84.37,0,0,0,.55,9.41,8,8,0,0,1-15.92,1.18A99.64,99.64,0,0,1,32,128a99.68,99.68,0,0,1,22.57-63.24L42.08,51.38A8,8,0,1,1,53.92,40.62ZM167.43,128a83.48,83.48,0,0,1-1.25,14.42,8,8,0,0,0,6.52,9.24,8.25,8.25,0,0,0,1.37.12,8,8,0,0,0,7.87-6.64A99.64,99.64,0,0,0,183.43,128a8,8,0,0,0-16,0Zm-32.53-56.88a8,8,0,0,0,5.15-15.13A100.06,100.06,0,0,0,32.63,128a8,8,0,0,0,16,0,84.07,84.07,0,0,1,86.27-83.88ZM231.43,128a8,8,0,0,0-16,0,84,84,0,0,1-14.82,47.64,8,8,0,0,0,13.22,9,100,100,0,0,0,17.6-56.64Zm-103.43,72a12,12,0,1,0,12,12A12,12,0,0,0,128,200Z"
/>
</svg>
</div>
<span class="offline-text"
>{$_('pwa.offline', { default: 'Offline — Änderungen werden lokal gespeichert' })}</span
>
<button
type="button"
class="dismiss-btn"
onclick={() => (dismissed = true)}
aria-label="Schliessen"
>
<svg width="14" height="14" viewBox="0 0 256 256" fill="currentColor">
<path
d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"
/>
</svg>
</button>
</div>
{/if}
{#if showSyncBadge}
<div class="sync-badge" role="status" aria-live="polite">
<span class="sync-dot pending"></span>
<span class="sync-text">
{networkStore.pendingCount}
{$_('pwa.pendingChanges', { default: 'Änderungen warten auf Sync' })}
</span>
</div>
{/if}
{#if showSyncing}
<div class="sync-badge" role="status" aria-live="polite">
<span class="sync-dot syncing"></span>
<span class="sync-text">{$_('pwa.syncing', { default: 'Synchronisiere...' })}</span>
</div>
{/if}
<style>
.offline-banner {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
z-index: 200;
display: flex;
align-items: center;
gap: 8px;
background: rgba(239, 68, 68, 0.92);
color: white;
padding: 8px 16px;
border-radius: 10px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
animation: slideDown 0.3s ease-out;
backdrop-filter: blur(8px);
}
@keyframes slideDown {
from {
transform: translateX(-50%) translateY(-20px);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
.offline-icon {
display: flex;
align-items: center;
flex-shrink: 0;
}
.offline-text {
white-space: nowrap;
}
@media (max-width: 480px) {
.offline-banner {
left: 12px;
right: 12px;
transform: none;
}
.offline-text {
white-space: normal;
font-size: 12px;
}
}
.dismiss-btn {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.15);
border: none;
color: white;
padding: 4px;
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
margin-left: 4px;
}
.dismiss-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.sync-badge {
position: fixed;
top: 12px;
right: 12px;
z-index: 200;
display: flex;
align-items: center;
gap: 6px;
background: hsl(var(--color-card, 0 0% 10%) / 0.92);
color: hsl(var(--color-foreground, 0 0% 90%));
padding: 6px 12px;
border-radius: 8px;
font-size: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeIn 0.2s ease-out;
backdrop-filter: blur(8px);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.sync-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.sync-dot.pending {
background: #f59e0b;
}
.sync-dot.syncing {
background: #6366f1;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.sync-text {
white-space: nowrap;
}
</style>

View file

@ -1,183 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
let showPrompt = $state(false);
let registration = $state<ServiceWorkerRegistration | null>(null);
onMount(() => {
if (!('serviceWorker' in navigator)) return;
function onNewSW(reg: ServiceWorkerRegistration) {
registration = reg;
showPrompt = true;
}
navigator.serviceWorker.ready.then((reg) => {
// Already waiting worker present
if (reg.waiting) {
onNewSW(reg);
return;
}
// Watch for new installs
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
if (!newWorker) return;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
onNewSW(reg);
}
});
});
});
});
let reloading = false;
function handleUpdate() {
if (!registration?.waiting || reloading) return;
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
// Reload once the new SW takes over (once only)
navigator.serviceWorker.addEventListener(
'controllerchange',
() => {
if (reloading) return;
reloading = true;
window.location.reload();
},
{ once: true }
);
}
function handleDismiss() {
showPrompt = false;
}
</script>
{#if showPrompt}
<div class="update-banner" role="alert">
<div class="update-content">
<svg class="update-icon" width="18" height="18" viewBox="0 0 256 256" fill="currentColor">
<path
d="M224,128a96,96,0,1,1-96-96A96.11,96.11,0,0,1,224,128Zm-40-8H136V80a8,8,0,0,0-16,0v48a8,8,0,0,0,8,8h48a8,8,0,0,0,0-16Z"
/>
</svg>
<span class="update-text">
{$_('pwa.updateAvailable', { default: 'Neue Version verfügbar' })}
</span>
</div>
<div class="update-actions">
<button type="button" class="update-btn" onclick={handleUpdate}>
{$_('pwa.updateNow', { default: 'Aktualisieren' })}
</button>
<button type="button" class="dismiss-btn" onclick={handleDismiss}>
{$_('pwa.later', { default: 'Später' })}
</button>
</div>
</div>
{/if}
<style>
.update-banner {
position: fixed;
bottom: 12px;
right: 12px;
z-index: 200;
display: flex;
align-items: center;
gap: 12px;
background: hsl(var(--color-card, 0 0% 10%) / 0.95);
color: hsl(var(--color-foreground, 0 0% 90%));
padding: 12px 16px;
border-radius: 12px;
font-size: 13px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
animation: slideUp 0.3s ease-out;
backdrop-filter: blur(12px);
border: 1px solid hsl(var(--color-foreground, 0 0% 90%) / 0.1);
max-width: 360px;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.update-content {
display: flex;
align-items: center;
gap: 8px;
}
.update-icon {
flex-shrink: 0;
color: #6366f1;
}
.update-text {
white-space: nowrap;
}
@media (max-width: 640px) {
.update-banner {
bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
left: 12px;
right: 12px;
max-width: none;
}
.update-text {
white-space: normal;
font-size: 12px;
}
}
.update-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.update-btn {
background: #6366f1;
border: none;
color: white;
padding: 5px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
.update-btn:hover {
background: #5558e6;
}
.dismiss-btn {
background: hsl(var(--color-foreground, 0 0% 90%) / 0.08);
border: none;
color: hsl(var(--color-foreground, 0 0% 90%) / 0.6);
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.dismiss-btn:hover {
background: hsl(var(--color-foreground, 0 0% 90%) / 0.15);
color: hsl(var(--color-foreground, 0 0% 90%) / 0.8);
}
</style>

View file

@ -1,59 +0,0 @@
/**
* Network status store tracks online/offline state and pending sync changes.
*
* Integrates with the unified sync manager to show sync status
* and pending change counts in the UI.
*/
import type { SyncStatus } from '$lib/data/sync';
let isOnline = $state(typeof navigator !== 'undefined' ? navigator.onLine : true);
let syncStatus = $state<SyncStatus>('idle');
let pendingCount = $state(0);
let cleanup: (() => void) | null = null;
function initialize() {
if (typeof window === 'undefined') return;
if (cleanup) return; // Guard against double-init
const handleOnline = () => (isOnline = true);
const handleOffline = () => (isOnline = false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
cleanup = () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}
function destroy() {
cleanup?.();
cleanup = null;
}
function setSyncStatus(status: SyncStatus) {
syncStatus = status;
}
function setPendingCount(count: number) {
pendingCount = count;
}
export const networkStore = {
get isOnline() {
return isOnline;
},
get syncStatus() {
return syncStatus;
},
get pendingCount() {
return pendingCount;
},
initialize,
destroy,
setSyncStatus,
setPendingCount,
};

View file

@ -78,7 +78,7 @@ if docker info >/dev/null 2>&1; then
# every running container, and report any compose service whose
# container_name is not currently up.
if [ -f "$COMPOSE_FILE" ]; then
DEFINED=$(docker compose -p "${COMPOSE_PROJECT_NAME:-manacore-monorepo}" \
DEFINED=$(docker compose -p "${COMPOSE_PROJECT_NAME:-mana-monorepo}" \
-f "$COMPOSE_FILE" config --format json 2>/dev/null \
| python3 -c '
import sys, json