Rename castle-app to libra-app

Match the upstream LNbits extension rebrand (Castle Accounting → Libra).
Renames the standalone PWA build artifacts and all references:

- castle.html → libra.html
- vite.castle.config.ts → vite.libra.config.ts (PWA name "Libra —
  Team Accounting", short_name "Libra", manifest id libra-accounting)
- npm scripts: build:castle/dev:castle/preview:castle → build:libra
  etc; dev:all and build:demo chains updated; dist-castle → dist-libra
- Hub tile: Lucide icon Castle → Scale (the scales/balance metaphor),
  label "Castle" → "Libra", env var VITE_HUB_CASTLE_URL → VITE_HUB_LIBRA_URL
- ExpensesAPI: /castle/api/v1/* → /libra/api/v1/* (matches the renamed
  LNbits extension's URL prefix)
- Feature flags VITE_CASTLE_INCOME_ENABLED/VITE_CASTLE_BUDGETS_ENABLED →
  VITE_LIBRA_*
- i18n: top-level "castle" namespace → "libra" across en/es/fr; all
  t('castle.*') usages updated
- localStorage key castle-expense-drafts → libra-expense-drafts
- nginx.conf.example: /castle/ routes + castle.<domain> redirect → libra
- Comments and identifiers: castleOwesUser → libraOwesUser, castle.api
  references in docs

Source dir src/accounting-app/ stays as-is (already feature-named, not
brand-named).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-05 10:44:04 +02:00
commit 442a755a51
27 changed files with 116 additions and 116 deletions

View file

@ -58,7 +58,7 @@ VITE_MARKET_NADDR=naddr1qqjxgdp4vv6rydej943n2dny956rwwf4943xzwfc95ekyd3evenrsvrr
# In LOCAL DEV with `npm run dev:all` use the per-app dev ports (defined # In LOCAL DEV with `npm run dev:all` use the per-app dev ports (defined
# in the vite configs): # in the vite configs):
# VITE_HUB_ACTIVITIES_URL=http://localhost:5181 # VITE_HUB_ACTIVITIES_URL=http://localhost:5181
# VITE_HUB_CASTLE_URL=http://localhost:5180 # VITE_HUB_LIBRA_URL=http://localhost:5180
# VITE_HUB_WALLET_URL=http://localhost:5182 # VITE_HUB_WALLET_URL=http://localhost:5182
# VITE_HUB_CHAT_URL=http://localhost:5183 # VITE_HUB_CHAT_URL=http://localhost:5183
# VITE_HUB_FORUM_URL=http://localhost:5184 # VITE_HUB_FORUM_URL=http://localhost:5184
@ -67,7 +67,7 @@ VITE_MARKET_NADDR=naddr1qqjxgdp4vv6rydej943n2dny956rwwf4943xzwfc95ekyd3evenrsvrr
# #
# In PATH-MODE production (recommended for demo) — note the trailing slash: # In PATH-MODE production (recommended for demo) — note the trailing slash:
# VITE_HUB_ACTIVITIES_URL=https://demo.example.com/activities/ # VITE_HUB_ACTIVITIES_URL=https://demo.example.com/activities/
# VITE_HUB_CASTLE_URL=https://demo.example.com/castle/ # VITE_HUB_LIBRA_URL=https://demo.example.com/libra/
# VITE_HUB_WALLET_URL=https://demo.example.com/wallet/ # VITE_HUB_WALLET_URL=https://demo.example.com/wallet/
# VITE_HUB_CHAT_URL=https://demo.example.com/chat/ # VITE_HUB_CHAT_URL=https://demo.example.com/chat/
# VITE_HUB_FORUM_URL=https://demo.example.com/forum/ # VITE_HUB_FORUM_URL=https://demo.example.com/forum/
@ -76,11 +76,11 @@ VITE_MARKET_NADDR=naddr1qqjxgdp4vv6rydej943n2dny956rwwf4943xzwfc95ekyd3evenrsvrr
# #
# In SUBDOMAIN-MODE production: # In SUBDOMAIN-MODE production:
# VITE_HUB_ACTIVITIES_URL=https://sortir.example.com # VITE_HUB_ACTIVITIES_URL=https://sortir.example.com
# VITE_HUB_CASTLE_URL=https://castle.example.com # VITE_HUB_LIBRA_URL=https://libra.example.com
# ...etc # ...etc
# ─────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────
VITE_HUB_ACTIVITIES_URL= VITE_HUB_ACTIVITIES_URL=
VITE_HUB_CASTLE_URL= VITE_HUB_LIBRA_URL=
VITE_HUB_WALLET_URL= VITE_HUB_WALLET_URL=
VITE_HUB_CHAT_URL= VITE_HUB_CHAT_URL=
VITE_HUB_FORUM_URL= VITE_HUB_FORUM_URL=

View file

@ -9,8 +9,8 @@
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180"> <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF"> <link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<title>Castle — Accounting</title> <title>Libra — Accounting</title>
<meta name="apple-mobile-web-app-title" content="Castle"> <meta name="apple-mobile-web-app-title" content="Libra">
<meta name="description" content="Team accounting and expense management"> <meta name="description" content="Team accounting and expense management">
</head> </head>
<body> <body>

View file

@ -21,7 +21,7 @@ http {
# demo.<domain>.<com>/chat/ — chat standalone # demo.<domain>.<com>/chat/ — chat standalone
# demo.<domain>.<com>/forum/ — forum standalone # demo.<domain>.<com>/forum/ — forum standalone
# demo.<domain>.<com>/tasks/ — tasks standalone # demo.<domain>.<com>/tasks/ — tasks standalone
# demo.<domain>.<com>/castle/ — castle (accounting) standalone # demo.<domain>.<com>/libra/ — libra (accounting) standalone
# #
# Each standalone is built with VITE_BASE_PATH=/<name>/ so its asset URLs # Each standalone is built with VITE_BASE_PATH=/<name>/ so its asset URLs
# are prefixed correctly. The hub's chakra tiles point at the canonical # are prefixed correctly. The hub's chakra tiles point at the canonical
@ -88,11 +88,11 @@ http {
try_files $uri $uri/ /tasks.html; try_files $uri $uri/ /tasks.html;
} }
# ── Castle (accounting) ────────────────────────────────────────── # ── Libra (accounting) ──────────────────────────────────────────
location = /castle { return 301 /castle/$is_args$args; } location = /libra { return 301 /libra/$is_args$args; }
location /castle/ { location /libra/ {
alias /var/www/aio/dist-castle/; alias /var/www/aio/dist-libra/;
try_files $uri $uri/ /castle.html; try_files $uri $uri/ /libra.html;
} }
# ── Static asset MIME / cache (applies to all bundles) ─────────── # ── Static asset MIME / cache (applies to all bundles) ───────────
@ -142,8 +142,8 @@ http {
} }
server { server {
listen 8080; listen 8080;
server_name castle.demo.<domain>.<com>; server_name libra.demo.<domain>.<com>;
return 301 https://demo.<domain>.<com>/castle/$request_uri; return 301 https://demo.<domain>.<com>/libra/$request_uri;
} }
# ─────────────────────────────────────────────────────────────────────── # ───────────────────────────────────────────────────────────────────────
@ -159,7 +159,7 @@ http {
# server { server_name chat.<domain>; root /var/www/aio/dist-chat; ... } # server { server_name chat.<domain>; root /var/www/aio/dist-chat; ... }
# server { server_name forum.<domain>; root /var/www/aio/dist-forum; ... } # server { server_name forum.<domain>; root /var/www/aio/dist-forum; ... }
# server { server_name tasks.<domain>; root /var/www/aio/dist-tasks; ... } # server { server_name tasks.<domain>; root /var/www/aio/dist-tasks; ... }
# server { server_name castle.<domain>; root /var/www/aio/dist-castle; ... } # server { server_name libra.<domain>; root /var/www/aio/dist-libra; ... }
# #
# Each block uses `location / { try_files $uri $uri/ /<name>.html; }`. # Each block uses `location / { try_files $uri $uri/ /<name>.html; }`.
# In subdomain mode, build each standalone WITHOUT VITE_BASE_PATH (the # In subdomain mode, build each standalone WITHOUT VITE_BASE_PATH (the

View file

@ -12,9 +12,9 @@
"dev:activities": "vite --host --config vite.activities.config.ts", "dev:activities": "vite --host --config vite.activities.config.ts",
"build:activities": "vue-tsc -b && vite build --config vite.activities.config.ts", "build:activities": "vue-tsc -b && vite build --config vite.activities.config.ts",
"preview:activities": "vite preview --host --config vite.activities.config.ts", "preview:activities": "vite preview --host --config vite.activities.config.ts",
"dev:castle": "vite --host --config vite.castle.config.ts", "dev:libra": "vite --host --config vite.libra.config.ts",
"build:castle": "vue-tsc -b && vite build --config vite.castle.config.ts", "build:libra": "vue-tsc -b && vite build --config vite.libra.config.ts",
"preview:castle": "vite preview --host --config vite.castle.config.ts", "preview:libra": "vite preview --host --config vite.libra.config.ts",
"dev:wallet": "vite --host --config vite.wallet.config.ts", "dev:wallet": "vite --host --config vite.wallet.config.ts",
"build:wallet": "vue-tsc -b && vite build --config vite.wallet.config.ts", "build:wallet": "vue-tsc -b && vite build --config vite.wallet.config.ts",
"preview:wallet": "vite preview --host --config vite.wallet.config.ts", "preview:wallet": "vite preview --host --config vite.wallet.config.ts",
@ -30,8 +30,8 @@
"dev:forum": "vite --host --config vite.forum.config.ts", "dev:forum": "vite --host --config vite.forum.config.ts",
"build:forum": "vue-tsc -b && vite build --config vite.forum.config.ts", "build:forum": "vue-tsc -b && vite build --config vite.forum.config.ts",
"preview:forum": "vite preview --host --config vite.forum.config.ts", "preview:forum": "vite preview --host --config vite.forum.config.ts",
"dev:all": "concurrently -n hub,castle,sortir,wallet,chat,forum,market,tasks -c blue,magenta,cyan,yellow,green,blue,red,gray \"npm:dev\" \"npm:dev:castle\" \"npm:dev:activities\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\"", "dev:all": "concurrently -n hub,libra,sortir,wallet,chat,forum,market,tasks -c blue,magenta,cyan,yellow,green,blue,red,gray \"npm:dev\" \"npm:dev:libra\" \"npm:dev:activities\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\"",
"build:demo": "npm run build && VITE_BASE_PATH=/sortir/ npm run build:activities && VITE_BASE_PATH=/castle/ npm run build:castle && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks", "build:demo": "npm run build && VITE_BASE_PATH=/sortir/ npm run build:activities && VITE_BASE_PATH=/libra/ npm run build:libra && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks",
"electron:dev": "concurrently \"vite --host\" \"electron-forge start\"", "electron:dev": "concurrently \"vite --host\" \"electron-forge start\"",
"electron:build": "vue-tsc -b && vite build && electron-builder", "electron:build": "vue-tsc -b && vite build && electron-builder",
"electron:package": "electron-builder", "electron:package": "electron-builder",

View file

@ -23,11 +23,11 @@ const showLoginDialog = ref(false)
// Bottom navigation tabs // Bottom navigation tabs
const bottomTabs = computed(() => [ const bottomTabs = computed(() => [
{ name: t('castle.nav.record'), icon: PlusCircle, path: '/record' }, { name: t('libra.nav.record'), icon: PlusCircle, path: '/record' },
{ name: t('castle.nav.transactions'), icon: List, path: '/expenses/transactions' }, { name: t('libra.nav.transactions'), icon: List, path: '/expenses/transactions' },
{ name: t('castle.nav.balance'), icon: Scale, path: '/balance' }, { name: t('libra.nav.balance'), icon: Scale, path: '/balance' },
{ name: t('castle.nav.wallet'), icon: Wallet, path: '/wallet' }, { name: t('libra.nav.wallet'), icon: Wallet, path: '/wallet' },
{ name: t('castle.nav.settings'), icon: Settings, path: '/settings' }, { name: t('libra.nav.settings'), icon: Settings, path: '/settings' },
]) ])
const isLoginPage = computed(() => route.path === '/login') const isLoginPage = computed(() => route.path === '/login')

View file

@ -1,7 +1,7 @@
import type { AppConfig } from '@/core/types' import type { AppConfig } from '@/core/types'
/** /**
* Standalone Castle accounting app configuration. * Standalone Libra accounting app configuration.
* Only enables base + expenses + wallet modules. * Only enables base + expenses + wallet modules.
*/ */
export const appConfig: AppConfig = { export const appConfig: AppConfig = {

View file

@ -18,13 +18,13 @@ import { installStrictAuthGuard, markAuthReady, catchAllRoute } from '@/lib/rout
import { acceptTokenFromUrl } from '@/lib/url-token' import { acceptTokenFromUrl } from '@/lib/url-token'
/** /**
* Initialize the standalone Castle accounting app * Initialize the standalone Libra accounting app
*/ */
export async function createAppInstance() { export async function createAppInstance() {
console.log('Starting Castle — Accounting App...') console.log('Starting Libra — Accounting App...')
// Accept token from URL before anything else (cross-subdomain auth relay) // Accept token from URL before anything else (cross-subdomain auth relay)
acceptTokenFromUrl('Castle') acceptTokenFromUrl('Libra')
const app = createApp(App) const app = createApp(App)
@ -75,7 +75,7 @@ export async function createAppInstance() {
] ]
}) })
// Castle has no public view — every non-login route requires auth. // Libra has no public view — every non-login route requires auth.
installStrictAuthGuard(router) installStrictAuthGuard(router)
const pinia = createPinia() const pinia = createPinia()
@ -135,7 +135,7 @@ export async function createAppInstance() {
;(window as any).__container = container ;(window as any).__container = container
} }
console.log('Castle app initialized') console.log('Libra app initialized')
return { app, router } return { app, router }
} }
@ -143,10 +143,10 @@ export async function startApp() {
try { try {
const { app } = await createAppInstance() const { app } = await createAppInstance()
app.mount('#app') app.mount('#app')
console.log('Castle app started!') console.log('Libra app started!')
eventBus.emit('app:started', {}, 'app') eventBus.emit('app:started', {}, 'app')
} catch (error) { } catch (error) {
console.error('Failed to start Castle app:', error) console.error('Failed to start Libra app:', error)
document.getElementById('app')!.innerHTML = ` document.getElementById('app')!.innerHTML = `
<div style="padding: 20px; text-align: center; color: red;"> <div style="padding: 20px; text-align: center; color: red;">
<h1>Failed to Start</h1> <h1>Failed to Start</h1>

View file

@ -30,7 +30,7 @@ export interface ExpenseDraft {
btc_price_snapshot?: BtcPriceSnapshot btc_price_snapshot?: BtcPriceSnapshot
} }
const STORAGE_KEY = 'castle-expense-drafts' const STORAGE_KEY = 'libra-expense-drafts'
/** /**
* Composable for managing expense drafts in localStorage. * Composable for managing expense drafts in localStorage.

View file

@ -14,7 +14,7 @@ registerSW({
}, intervalMS) }, intervalMS)
}, },
onOfflineReady() { onOfflineReady() {
console.log('Castle app ready to work offline') console.log('Libra app ready to work offline')
} }
}) })

View file

@ -28,10 +28,10 @@ function handleClose() {
<DialogHeader> <DialogHeader>
<DialogTitle class="flex items-center gap-2"> <DialogTitle class="flex items-center gap-2">
<TrendingUp class="h-5 w-5 text-green-600 dark:text-green-400" /> <TrendingUp class="h-5 w-5 text-green-600 dark:text-green-400" />
<span>{{ t('castle.income.title') }}</span> <span>{{ t('libra.income.title') }}</span>
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{{ t('castle.income.description') }} {{ t('libra.income.description') }}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -41,7 +41,7 @@ function handleClose() {
<Info class="h-8 w-8 text-muted-foreground" /> <Info class="h-8 w-8 text-muted-foreground" />
</div> </div>
<p class="text-sm text-muted-foreground text-center max-w-xs"> <p class="text-sm text-muted-foreground text-center max-w-xs">
{{ t('castle.income.notAvailable') }} {{ t('libra.income.notAvailable') }}
</p> </p>
<Button variant="outline" @click="handleClose"> <Button variant="outline" @click="handleClose">
Close Close

View file

@ -23,7 +23,7 @@ const isLoading = ref(true)
const isRefreshing = ref(false) const isRefreshing = ref(false)
const walletKey = computed(() => user.value?.wallets?.[0]?.inkey) const walletKey = computed(() => user.value?.wallets?.[0]?.inkey)
const budgetsEnabled = computed(() => import.meta.env.VITE_CASTLE_BUDGETS_ENABLED === 'true') const budgetsEnabled = computed(() => import.meta.env.VITE_LIBRA_BUDGETS_ENABLED === 'true')
const pendingCount = computed(() => pendingTransactions.value.length) const pendingCount = computed(() => pendingTransactions.value.length)
@ -42,8 +42,8 @@ const pendingFiatCurrency = computed(() => {
return tx?.fiat_currency ?? null return tx?.fiat_currency ?? null
}) })
// Castle API: positive = user owes castle, negative = castle owes user // Libra API: positive = user owes libra, negative = libra owes user
const castleOwesUser = computed(() => (balance.value ?? 0) <= 0) const libraOwesUser = computed(() => (balance.value ?? 0) <= 0)
async function loadData() { async function loadData() {
if (!walletKey.value) return if (!walletKey.value) return
@ -96,7 +96,7 @@ function formatFiat(amount: number, currency: string): string {
<div class="container mx-auto px-4 py-6 max-w-lg"> <div class="container mx-auto px-4 py-6 max-w-lg">
<!-- Header with refresh --> <!-- Header with refresh -->
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-foreground">{{ t('castle.balance.title') }}</h1> <h1 class="text-2xl font-bold text-foreground">{{ t('libra.balance.title') }}</h1>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -116,27 +116,27 @@ function formatFiat(amount: number, currency: string): string {
<template v-else> <template v-else>
<!-- Balance Hero --> <!-- Balance Hero -->
<div class="rounded-xl border bg-card p-6 mb-6"> <div class="rounded-xl border bg-card p-6 mb-6">
<p class="text-sm text-muted-foreground mb-1">{{ t('castle.balance.netBalance') }}</p> <p class="text-sm text-muted-foreground mb-1">{{ t('libra.balance.netBalance') }}</p>
<div v-if="balance !== null" class="space-y-1"> <div v-if="balance !== null" class="space-y-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<component <component
:is="castleOwesUser ? ArrowDown : ArrowUp" :is="libraOwesUser ? ArrowDown : ArrowUp"
class="w-5 h-5" class="w-5 h-5"
:class="castleOwesUser ? 'text-green-500' : 'text-red-500'" :class="libraOwesUser ? 'text-green-500' : 'text-red-500'"
/> />
<span class="text-3xl font-bold text-foreground"> <span class="text-3xl font-bold text-foreground">
{{ formatAmount(balance) }} {{ formatAmount(balance) }}
</span> </span>
<span class="text-lg text-muted-foreground">{{ balanceCurrency }}</span> <span class="text-lg text-muted-foreground">{{ balanceCurrency }}</span>
</div> </div>
<p class="text-sm" :class="castleOwesUser ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'"> <p class="text-sm" :class="libraOwesUser ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
{{ castleOwesUser ? t('castle.balance.owedToYou') : t('castle.balance.youOwe') }} {{ libraOwesUser ? t('libra.balance.owedToYou') : t('libra.balance.youOwe') }}
</p> </p>
</div> </div>
<div v-else class="text-muted-foreground"> <div v-else class="text-muted-foreground">
{{ t('castle.balance.noBalance') }} {{ t('libra.balance.noBalance') }}
</div> </div>
</div> </div>
@ -144,14 +144,14 @@ function formatFiat(amount: number, currency: string): string {
<div v-if="pendingCount > 0" class="rounded-xl border bg-card p-5 mb-6"> <div v-if="pendingCount > 0" class="rounded-xl border bg-card p-5 mb-6">
<div class="flex items-center gap-2 mb-3"> <div class="flex items-center gap-2 mb-3">
<Clock class="w-4 h-4 text-orange-500" /> <Clock class="w-4 h-4 text-orange-500" />
<h2 class="text-sm font-medium text-foreground">{{ t('castle.balance.pending') }}</h2> <h2 class="text-sm font-medium text-foreground">{{ t('libra.balance.pending') }}</h2>
<Badge variant="secondary" class="text-xs">{{ pendingCount }}</Badge> <Badge variant="secondary" class="text-xs">{{ pendingCount }}</Badge>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground"> <span class="text-sm text-muted-foreground">
{{ t('castle.balance.pendingAmount', { amount: formatAmount(pendingTotal) + ' ' + balanceCurrency }) }} {{ t('libra.balance.pendingAmount', { amount: formatAmount(pendingTotal) + ' ' + balanceCurrency }) }}
</span> </span>
</div> </div>

View file

@ -16,7 +16,7 @@ const { drafts, hasDrafts, deleteDraft } = useExpenseDrafts()
const showAddExpense = ref(false) const showAddExpense = ref(false)
const showAddIncome = ref(false) const showAddIncome = ref(false)
const incomeEnabled = computed(() => import.meta.env.VITE_CASTLE_INCOME_ENABLED === 'true') const incomeEnabled = computed(() => import.meta.env.VITE_LIBRA_INCOME_ENABLED === 'true')
function handleExpenseSubmitted() { function handleExpenseSubmitted() {
// Could refresh balance or show notification // Could refresh balance or show notification
@ -47,7 +47,7 @@ function draftTimeAgo(isoDate: string) {
<template> <template>
<div class="container mx-auto px-4 py-6 max-w-lg"> <div class="container mx-auto px-4 py-6 max-w-lg">
<h1 class="text-2xl font-bold text-foreground mb-6">{{ t('castle.record.title') }}</h1> <h1 class="text-2xl font-bold text-foreground mb-6">{{ t('libra.record.title') }}</h1>
<!-- Action Cards --> <!-- Action Cards -->
<div class="grid gap-4"> <div class="grid gap-4">
@ -60,8 +60,8 @@ function draftTimeAgo(isoDate: string) {
<DollarSign class="w-6 h-6 text-red-600 dark:text-red-400" /> <DollarSign class="w-6 h-6 text-red-600 dark:text-red-400" />
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<h2 class="text-lg font-semibold text-foreground">{{ t('castle.record.addExpense') }}</h2> <h2 class="text-lg font-semibold text-foreground">{{ t('libra.record.addExpense') }}</h2>
<p class="text-sm text-muted-foreground mt-0.5">{{ t('castle.record.addExpenseDescription') }}</p> <p class="text-sm text-muted-foreground mt-0.5">{{ t('libra.record.addExpenseDescription') }}</p>
</div> </div>
</button> </button>
@ -79,12 +79,12 @@ function draftTimeAgo(isoDate: string) {
</div> </div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2 class="text-lg font-semibold text-foreground">{{ t('castle.record.addIncome') }}</h2> <h2 class="text-lg font-semibold text-foreground">{{ t('libra.record.addIncome') }}</h2>
<Badge v-if="!incomeEnabled" variant="secondary" class="text-xs"> <Badge v-if="!incomeEnabled" variant="secondary" class="text-xs">
{{ t('castle.record.comingSoon') }} {{ t('libra.record.comingSoon') }}
</Badge> </Badge>
</div> </div>
<p class="text-sm text-muted-foreground mt-0.5">{{ t('castle.record.addIncomeDescription') }}</p> <p class="text-sm text-muted-foreground mt-0.5">{{ t('libra.record.addIncomeDescription') }}</p>
</div> </div>
</button> </button>
</div> </div>
@ -92,7 +92,7 @@ function draftTimeAgo(isoDate: string) {
<!-- Info hint when income is disabled --> <!-- Info hint when income is disabled -->
<div v-if="!incomeEnabled" class="mt-4 flex items-start gap-2 p-3 rounded-lg bg-muted/50"> <div v-if="!incomeEnabled" class="mt-4 flex items-start gap-2 p-3 rounded-lg bg-muted/50">
<Info class="w-4 h-4 text-muted-foreground mt-0.5 shrink-0" /> <Info class="w-4 h-4 text-muted-foreground mt-0.5 shrink-0" />
<p class="text-xs text-muted-foreground">{{ t('castle.income.notAvailable') }}</p> <p class="text-xs text-muted-foreground">{{ t('libra.income.notAvailable') }}</p>
</div> </div>
<!-- Drafts Section --> <!-- Drafts Section -->
@ -101,7 +101,7 @@ function draftTimeAgo(isoDate: string) {
<div> <div>
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3"> <h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide mb-3">
{{ t('castle.record.drafts') }} {{ t('libra.record.drafts') }}
<Badge variant="secondary" class="ml-1 text-xs">{{ drafts.length }}</Badge> <Badge variant="secondary" class="ml-1 text-xs">{{ drafts.length }}</Badge>
</h2> </h2>
@ -129,7 +129,7 @@ function draftTimeAgo(isoDate: string) {
{{ draft.description || draft.account?.name || 'Untitled draft' }} {{ draft.description || draft.account?.name || 'Untitled draft' }}
</p> </p>
<div class="flex items-center gap-2 text-xs text-muted-foreground"> <div class="flex items-center gap-2 text-xs text-muted-foreground">
<span>{{ t('castle.record.draftAge', { time: draftTimeAgo(draft.created_at) }) }}</span> <span>{{ t('libra.record.draftAge', { time: draftTimeAgo(draft.created_at) }) }}</span>
<span v-if="draft.amount"> <span v-if="draft.amount">
&middot; {{ draft.amount }} {{ draft.currency || 'sats' }} &middot; {{ draft.amount }} {{ draft.currency || 'sats' }}
</span> </span>

View file

@ -35,27 +35,27 @@ async function handleLogout() {
<template> <template>
<div class="container mx-auto px-4 py-6 max-w-lg"> <div class="container mx-auto px-4 py-6 max-w-lg">
<h1 class="text-2xl font-bold text-foreground mb-6">{{ t('castle.settings.title') }}</h1> <h1 class="text-2xl font-bold text-foreground mb-6">{{ t('libra.settings.title') }}</h1>
<!-- Account --> <!-- Account -->
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('castle.settings.account') }}</h2> <h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('libra.settings.account') }}</h2>
<div v-if="isAuthenticated" class="bg-muted/50 rounded-lg p-4 space-y-3"> <div v-if="isAuthenticated" class="bg-muted/50 rounded-lg p-4 space-y-3">
<p class="text-sm text-foreground font-mono truncate"> <p class="text-sm text-foreground font-mono truncate">
{{ userPubkey }} {{ userPubkey }}
</p> </p>
<Button variant="outline" size="sm" class="w-full gap-2" @click="handleLogout"> <Button variant="outline" size="sm" class="w-full gap-2" @click="handleLogout">
<LogOut class="w-4 h-4" /> <LogOut class="w-4 h-4" />
{{ t('castle.settings.logOut') }} {{ t('libra.settings.logOut') }}
</Button> </Button>
</div> </div>
<div v-else class="bg-muted/50 rounded-lg p-4"> <div v-else class="bg-muted/50 rounded-lg p-4">
<p class="text-sm text-muted-foreground mb-3"> <p class="text-sm text-muted-foreground mb-3">
{{ t('castle.settings.loginPrompt') }} {{ t('libra.settings.loginPrompt') }}
</p> </p>
<Button size="sm" class="w-full gap-2" @click="$router.push('/login')"> <Button size="sm" class="w-full gap-2" @click="$router.push('/login')">
<LogIn class="w-4 h-4" /> <LogIn class="w-4 h-4" />
{{ t('castle.settings.logIn') }} {{ t('libra.settings.logIn') }}
</Button> </Button>
</div> </div>
</div> </div>
@ -64,9 +64,9 @@ async function handleLogout() {
<!-- Appearance --> <!-- Appearance -->
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('castle.settings.appearance') }}</h2> <h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('libra.settings.appearance') }}</h2>
<div class="flex items-center justify-between bg-muted/50 rounded-lg p-4"> <div class="flex items-center justify-between bg-muted/50 rounded-lg p-4">
<span class="text-sm text-foreground">{{ t('castle.settings.theme') }}</span> <span class="text-sm text-foreground">{{ t('libra.settings.theme') }}</span>
<Button variant="outline" size="icon" class="h-8 w-8" @click="toggleTheme"> <Button variant="outline" size="icon" class="h-8 w-8" @click="toggleTheme">
<Sun v-if="theme === 'dark'" class="w-4 h-4" /> <Sun v-if="theme === 'dark'" class="w-4 h-4" />
<Moon v-else class="w-4 h-4" /> <Moon v-else class="w-4 h-4" />
@ -78,7 +78,7 @@ async function handleLogout() {
<!-- Language --> <!-- Language -->
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('castle.settings.language') }}</h2> <h2 class="text-sm font-medium text-muted-foreground uppercase tracking-wide">{{ t('libra.settings.language') }}</h2>
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
v-for="lang in languages" v-for="lang in languages"

View file

@ -4,7 +4,7 @@ import type { AppConfig } from './core/types'
* Minimal AIO hub configuration. * Minimal AIO hub configuration.
* The all-in-one app at app.${domain} ships only the base module * The all-in-one app at app.${domain} ships only the base module
* each feature module (wallet, chat, market, tasks, forum, activities, * each feature module (wallet, chat, market, tasks, forum, activities,
* castle) is now its own standalone PWA at its own subdomain. * libra) is now its own standalone PWA at its own subdomain.
*/ */
export const appConfig: AppConfig = { export const appConfig: AppConfig = {
modules: { modules: {

View file

@ -20,7 +20,7 @@ import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/rou
* *
* The all-in-one app at app.${domain} now ships only the base module * The all-in-one app at app.${domain} now ships only the base module
* plus a chakra icon hub linking out to the standalone module apps * plus a chakra icon hub linking out to the standalone module apps
* (wallet, chat, market, tasks, forum, activities, castle). * (wallet, chat, market, tasks, forum, activities, libra).
*/ */
export async function createAppInstance() { export async function createAppInstance() {
console.log('🚀 Starting AIO hub...') console.log('🚀 Starting AIO hub...')

View file

@ -119,7 +119,7 @@ const messages: LocaleMessages = {
language: 'Language', language: 'Language',
}, },
}, },
castle: { libra: {
nav: { nav: {
record: 'Record', record: 'Record',
transactions: 'Transactions', transactions: 'Transactions',

View file

@ -119,7 +119,7 @@ const messages: LocaleMessages = {
language: 'Idioma', language: 'Idioma',
}, },
}, },
castle: { libra: {
nav: { nav: {
record: 'Registrar', record: 'Registrar',
transactions: 'Transacciones', transactions: 'Transacciones',

View file

@ -119,7 +119,7 @@ const messages: LocaleMessages = {
language: 'Langue', language: 'Langue',
}, },
}, },
castle: { libra: {
nav: { nav: {
record: 'Saisir', record: 'Saisir',
transactions: 'Transactions', transactions: 'Transactions',

View file

@ -94,8 +94,8 @@ export interface LocaleMessages {
language: string language: string
} }
} }
// Castle accounting module // Libra accounting module
castle?: { libra?: {
nav: { nav: {
record: string record: string
transactions: string transactions: string

View file

@ -41,7 +41,7 @@ function isFullyAuthed(auth: AuthLike): boolean {
/** /**
* Strict guard every non-/login route requires auth. * Strict guard every non-/login route requires auth.
* Used by wallet, chat, castle (no public view). * Used by wallet, chat, libra (no public view).
*/ */
export function installStrictAuthGuard(router: Router): void { export function installStrictAuthGuard(router: Router): void {
router.beforeEach(async (to) => { router.beforeEach(async (to) => {

View file

@ -2,7 +2,7 @@
* Expenses Module * Expenses Module
* *
* Provides expense tracking and submission functionality * Provides expense tracking and submission functionality
* integrated with castle LNbits extension. * integrated with libra LNbits extension.
*/ */
import type { App } from 'vue' import type { App } from 'vue'

View file

@ -1,5 +1,5 @@
/** /**
* API service for castle extension expense operations * API service for libra extension expense operations
*/ */
import { BaseService } from '@/core/base/BaseService' import { BaseService } from '@/core/base/BaseService'
@ -48,7 +48,7 @@ export class ExpensesAPI extends BaseService {
} }
/** /**
* Get all accounts from castle * Get all accounts from libra
* *
* @param walletKey - Wallet key for authentication * @param walletKey - Wallet key for authentication
* @param filterByUser - If true, only return accounts the user has permissions for * @param filterByUser - If true, only return accounts the user has permissions for
@ -60,7 +60,7 @@ export class ExpensesAPI extends BaseService {
excludeVirtual: boolean = true excludeVirtual: boolean = true
): Promise<Account[]> { ): Promise<Account[]> {
try { try {
const url = new URL(`${this.baseUrl}/castle/api/v1/accounts`) const url = new URL(`${this.baseUrl}/libra/api/v1/accounts`)
if (filterByUser) { if (filterByUser) {
url.searchParams.set('filter_by_user', 'true') url.searchParams.set('filter_by_user', 'true')
} }
@ -162,11 +162,11 @@ export class ExpensesAPI extends BaseService {
} }
/** /**
* Submit expense entry to castle * Submit expense entry to libra
*/ */
async submitExpense(walletKey: string, request: ExpenseEntryRequest): Promise<ExpenseEntry> { async submitExpense(walletKey: string, request: ExpenseEntryRequest): Promise<ExpenseEntry> {
try { try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/entries/expense`, { const response = await fetch(`${this.baseUrl}/libra/api/v1/entries/expense`, {
method: 'POST', method: 'POST',
headers: this.getHeaders(walletKey), headers: this.getHeaders(walletKey),
body: JSON.stringify(request), body: JSON.stringify(request),
@ -193,7 +193,7 @@ export class ExpensesAPI extends BaseService {
*/ */
async getUserExpenses(walletKey: string): Promise<ExpenseEntry[]> { async getUserExpenses(walletKey: string): Promise<ExpenseEntry[]> {
try { try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/entries/user`, { const response = await fetch(`${this.baseUrl}/libra/api/v1/entries/user`, {
method: 'GET', method: 'GET',
headers: this.getHeaders(walletKey), headers: this.getHeaders(walletKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000) signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
@ -214,11 +214,11 @@ export class ExpensesAPI extends BaseService {
} }
/** /**
* Get user's balance with castle * Get user's balance with libra
*/ */
async getUserBalance(walletKey: string): Promise<{ balance: number; currency: string }> { async getUserBalance(walletKey: string): Promise<{ balance: number; currency: string }> {
try { try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/balance`, { const response = await fetch(`${this.baseUrl}/libra/api/v1/balance`, {
method: 'GET', method: 'GET',
headers: this.getHeaders(walletKey), headers: this.getHeaders(walletKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000) signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
@ -285,7 +285,7 @@ export class ExpensesAPI extends BaseService {
*/ */
async getUserInfo(walletKey: string): Promise<UserInfo> { async getUserInfo(walletKey: string): Promise<UserInfo> {
try { try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/user/info`, { const response = await fetch(`${this.baseUrl}/libra/api/v1/user/info`, {
method: 'GET', method: 'GET',
headers: this.getHeaders(walletKey), headers: this.getHeaders(walletKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000) signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
@ -313,7 +313,7 @@ export class ExpensesAPI extends BaseService {
*/ */
async listPermissions(adminKey: string): Promise<AccountPermission[]> { async listPermissions(adminKey: string): Promise<AccountPermission[]> {
try { try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/permissions`, { const response = await fetch(`${this.baseUrl}/libra/api/v1/permissions`, {
method: 'GET', method: 'GET',
headers: this.getHeaders(adminKey), headers: this.getHeaders(adminKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000) signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
@ -342,7 +342,7 @@ export class ExpensesAPI extends BaseService {
request: GrantPermissionRequest request: GrantPermissionRequest
): Promise<AccountPermission> { ): Promise<AccountPermission> {
try { try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/permissions`, { const response = await fetch(`${this.baseUrl}/libra/api/v1/permissions`, {
method: 'POST', method: 'POST',
headers: this.getHeaders(adminKey), headers: this.getHeaders(adminKey),
body: JSON.stringify(request), body: JSON.stringify(request),
@ -373,7 +373,7 @@ export class ExpensesAPI extends BaseService {
async revokePermission(adminKey: string, permissionId: string): Promise<void> { async revokePermission(adminKey: string, permissionId: string): Promise<void> {
try { try {
const response = await fetch( const response = await fetch(
`${this.baseUrl}/castle/api/v1/permissions/${permissionId}`, `${this.baseUrl}/libra/api/v1/permissions/${permissionId}`,
{ {
method: 'DELETE', method: 'DELETE',
headers: this.getHeaders(adminKey), headers: this.getHeaders(adminKey),
@ -412,7 +412,7 @@ export class ExpensesAPI extends BaseService {
} }
): Promise<TransactionListResponse> { ): Promise<TransactionListResponse> {
try { try {
const url = new URL(`${this.baseUrl}/castle/api/v1/entries/user`) const url = new URL(`${this.baseUrl}/libra/api/v1/entries/user`)
// Add query parameters // Add query parameters
if (options?.limit) url.searchParams.set('limit', String(options.limit)) if (options?.limit) url.searchParams.set('limit', String(options.limit))

View file

@ -3,7 +3,7 @@
*/ */
/** /**
* Account types in the castle double-entry accounting system * Account types in the libra double-entry accounting system
*/ */
export enum AccountType { export enum AccountType {
ASSET = 'asset', ASSET = 'asset',
@ -30,7 +30,7 @@ export interface Account {
/** /**
* Account with user-specific permission metadata * Account with user-specific permission metadata
* (Will be available once castle API implements permissions) * (Will be available once libra API implements permissions)
*/ */
export interface AccountWithPermissions extends Account { export interface AccountWithPermissions extends Account {
user_permissions?: PermissionType[] user_permissions?: PermissionType[]
@ -61,7 +61,7 @@ export interface ExpenseEntryRequest {
} }
/** /**
* Expense entry response from castle API * Expense entry response from libra API
*/ */
export interface ExpenseEntry { export interface ExpenseEntry {
id: string id: string

View file

@ -65,7 +65,7 @@ function handleSearchResults(results: Transaction[]) {
searchResults.value = results searchResults.value = results
} }
// Date range options (matching castle LNbits extension) // Date range options (matching libra LNbits extension)
const dateRangeOptions = [ const dateRangeOptions = [
{ label: '15 days', value: 15 }, { label: '15 days', value: 15 },
{ label: '30 days', value: 30 }, { label: '30 days', value: 30 },

View file

@ -6,7 +6,7 @@ import { useTheme } from '@/components/theme-provider'
import { useLocale } from '@/composables/useLocale' import { useLocale } from '@/composables/useLocale'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { import {
Castle, ListTodo, Newspaper, MessageCircle, Wallet, CalendarDays, Scale, ListTodo, Newspaper, MessageCircle, Wallet, CalendarDays,
Store, UtensilsCrossed, Store, UtensilsCrossed,
User as UserIcon, LogIn, Sun, Moon, Monitor, Globe, Coins, User as UserIcon, LogIn, Sun, Moon, Monitor, Globe, Coins,
} from 'lucide-vue-next' } from 'lucide-vue-next'
@ -48,7 +48,7 @@ const modules: Module[] = [
{ label: 'Chat', chakra: 'Anahata', icon: MessageCircle, bgClass: '', glow: 'rgba(0,200,80,0.5)', envKey: 'VITE_HUB_CHAT_URL', status: 'alpha', authRequired: true }, { label: 'Chat', chakra: 'Anahata', icon: MessageCircle, bgClass: '', glow: 'rgba(0,200,80,0.5)', envKey: 'VITE_HUB_CHAT_URL', status: 'alpha', authRequired: true },
{ label: 'Forum', chakra: 'Vishuddha', icon: Newspaper, bgClass: '', glow: 'rgba(60,120,255,0.5)', envKey: 'VITE_HUB_FORUM_URL', status: 'alpha' }, { label: 'Forum', chakra: 'Vishuddha', icon: Newspaper, bgClass: '', glow: 'rgba(60,120,255,0.5)', envKey: 'VITE_HUB_FORUM_URL', status: 'alpha' },
{ label: 'Tasks', chakra: 'Ajna', icon: ListTodo, bgClass: '', glow: 'rgba(99,80,200,0.5)', envKey: 'VITE_HUB_TASKS_URL', status: 'alpha', authRequired: true }, { label: 'Tasks', chakra: 'Ajna', icon: ListTodo, bgClass: '', glow: 'rgba(99,80,200,0.5)', envKey: 'VITE_HUB_TASKS_URL', status: 'alpha', authRequired: true },
{ label: 'Castle', chakra: 'Sahasrara', icon: Castle, bgClass: '', glow: 'rgba(160,80,220,0.5)', envKey: 'VITE_HUB_CASTLE_URL', status: 'beta', authRequired: true }, { label: 'Libra', chakra: 'Sahasrara', icon: Scale, bgClass: '', glow: 'rgba(160,80,220,0.5)', envKey: 'VITE_HUB_LIBRA_URL', status: 'beta', authRequired: true },
] ]
// Crown at top, root at bottom // Crown at top, root at bottom
const orderedModules = computed(() => [...modules].reverse()) const orderedModules = computed(() => [...modules].reverse())
@ -57,7 +57,7 @@ const token = computed(() => localStorage.getItem('lnbits_access_token') || '')
function hubLink(m: Module): string | null { function hubLink(m: Module): string | null {
if (!m.envKey) return null if (!m.envKey) return null
// Auth-only modules (wallet, chat, castle, tasks) are ghosted when not logged in. // Auth-only modules (wallet, chat, libra, tasks) are ghosted when not logged in.
if (m.authRequired && !isAuthenticated.value) return null if (m.authRequired && !isAuthenticated.value) return null
const url = import.meta.env[m.envKey] as string | undefined const url = import.meta.env[m.envKey] as string | undefined
if (!url) return null if (!url) return null

View file

@ -33,7 +33,7 @@ export default defineConfig(({ mode }) => ({
'**/*.{js,css,html,ico,png,svg}' '**/*.{js,css,html,ico,png,svg}'
], ],
// Don't intercept standalone app paths — they have their own service workers // Don't intercept standalone app paths — they have their own service workers
navigateFallbackDenylist: [/^\/sortir\//, /^\/castle\//, /^\/wallet\//, /^\/chat\//, /^\/market\//, /^\/cart\//, /^\/checkout\//, /^\/tasks\//, /^\/forum\//, /^\/submit\//, /^\/submission\//], navigateFallbackDenylist: [/^\/sortir\//, /^\/libra\//, /^\/wallet\//, /^\/chat\//, /^\/market\//, /^\/cart\//, /^\/checkout\//, /^\/tasks\//, /^\/forum\//, /^\/submit\//, /^\/submission\//],
}, },
includeAssets: [ includeAssets: [
'favicon.ico', 'favicon.ico',

View file

@ -7,15 +7,15 @@ import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'
import { visualizer } from 'rollup-plugin-visualizer' import { visualizer } from 'rollup-plugin-visualizer'
/** /**
* Plugin to rewrite dev server requests to castle.html * Plugin to rewrite dev server requests to libra.html
* (SPA fallback for the standalone Castle accounting app entry point) * (SPA fallback for the standalone Libra accounting app entry point)
*/ */
function castleHtmlPlugin(): Plugin { function libraHtmlPlugin(): Plugin {
return { return {
name: 'castle-html-rewrite', name: 'libra-html-rewrite',
configureServer(server) { configureServer(server) {
server.middlewares.use((req, _res, next) => { server.middlewares.use((req, _res, next) => {
// Rewrite all non-asset requests to castle.html. // Rewrite all non-asset requests to libra.html.
// Strip query before checking for an extension — JWTs (e.g. ?token=...) // Strip query before checking for an extension — JWTs (e.g. ?token=...)
// contain dots and would otherwise get mistaken for an asset request. // contain dots and would otherwise get mistaken for an asset request.
const path = req.url ? req.url.split('?')[0] : '' const path = req.url ? req.url.split('?')[0] : ''
@ -26,7 +26,7 @@ function castleHtmlPlugin(): Plugin {
!req.url.startsWith('/node_modules/') && !req.url.startsWith('/node_modules/') &&
!path.includes('.') !path.includes('.')
) { ) {
req.url = '/castle.html' req.url = '/libra.html'
} }
next() next()
}) })
@ -35,22 +35,22 @@ function castleHtmlPlugin(): Plugin {
} }
/** /**
* Vite config for the standalone Castle accounting app. * Vite config for the standalone Libra accounting app.
* *
* Set VITE_BASE_PATH to deploy under a path prefix: * Set VITE_BASE_PATH to deploy under a path prefix:
* VITE_BASE_PATH=/castle/ app.ariege.io/castle/ (shared auth) * VITE_BASE_PATH=/libra/ app.ariege.io/libra/ (shared auth)
* (default: /) castle.ariege.io (standalone subdomain) * (default: /) libra.ariege.io (standalone subdomain)
*/ */
export default defineConfig(({ mode }) => ({ export default defineConfig(({ mode }) => ({
base: process.env.VITE_BASE_PATH || '/', base: process.env.VITE_BASE_PATH || '/',
// Per-app dep cache so concurrent dev servers don't race on .vite/deps // Per-app dep cache so concurrent dev servers don't race on .vite/deps
cacheDir: 'node_modules/.vite-castle', cacheDir: 'node_modules/.vite-libra',
server: { server: {
port: 5180, port: 5180,
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
castleHtmlPlugin(), libraHtmlPlugin(),
vue(), vue(),
tailwindcss(), tailwindcss(),
VitePWA({ VitePWA({
@ -60,7 +60,7 @@ export default defineConfig(({ mode }) => ({
}, },
workbox: { workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}'], globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
navigateFallback: 'castle.html', navigateFallback: 'libra.html',
navigateFallbackAllowlist: [ navigateFallbackAllowlist: [
new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`),
], ],
@ -75,8 +75,8 @@ export default defineConfig(({ mode }) => ({
'icon-maskable-512.png', 'icon-maskable-512.png',
], ],
manifest: { manifest: {
name: 'Castle — Team Accounting', name: 'Libra — Team Accounting',
short_name: 'Castle', short_name: 'Libra',
description: 'Team accounting and expense management', description: 'Team accounting and expense management',
theme_color: '#1f2937', theme_color: '#1f2937',
background_color: '#ffffff', background_color: '#ffffff',
@ -84,7 +84,7 @@ export default defineConfig(({ mode }) => ({
orientation: 'portrait-primary', orientation: 'portrait-primary',
start_url: process.env.VITE_BASE_PATH || '/', start_url: process.env.VITE_BASE_PATH || '/',
scope: process.env.VITE_BASE_PATH || '/', scope: process.env.VITE_BASE_PATH || '/',
id: 'castle-accounting', id: 'libra-accounting',
categories: ['finance', 'business', 'productivity'], categories: ['finance', 'business', 'productivity'],
lang: 'en', lang: 'en',
icons: [ icons: [
@ -103,7 +103,7 @@ export default defineConfig(({ mode }) => ({
mode === 'analyze' && mode === 'analyze' &&
visualizer({ visualizer({
open: true, open: true,
filename: 'dist-castle/stats.html', filename: 'dist-libra/stats.html',
gzipSize: true, gzipSize: true,
brotliSize: true, brotliSize: true,
}), }),
@ -120,9 +120,9 @@ export default defineConfig(({ mode }) => ({
}, },
}, },
build: { build: {
outDir: 'dist-castle', outDir: 'dist-libra',
rollupOptions: { rollupOptions: {
input: 'castle.html', input: 'libra.html',
output: { output: {
manualChunks: { manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'], 'vue-vendor': ['vue', 'vue-router', 'pinia'],