mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
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:
parent
ed8ab44832
commit
b3523f8bdc
5 changed files with 13 additions and 442 deletions
16
CLAUDE.md
16
CLAUDE.md
|
|
@ -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/*)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue