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:
parent
8792a884cd
commit
442a755a51
27 changed files with 116 additions and 116 deletions
|
|
@ -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=
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
· {{ draft.amount }} {{ draft.currency || 'sats' }}
|
· {{ draft.amount }} {{ draft.currency || 'sats' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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...')
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ const messages: LocaleMessages = {
|
||||||
language: 'Language',
|
language: 'Language',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
castle: {
|
libra: {
|
||||||
nav: {
|
nav: {
|
||||||
record: 'Record',
|
record: 'Record',
|
||||||
transactions: 'Transactions',
|
transactions: 'Transactions',
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ const messages: LocaleMessages = {
|
||||||
language: 'Idioma',
|
language: 'Idioma',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
castle: {
|
libra: {
|
||||||
nav: {
|
nav: {
|
||||||
record: 'Registrar',
|
record: 'Registrar',
|
||||||
transactions: 'Transacciones',
|
transactions: 'Transacciones',
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ const messages: LocaleMessages = {
|
||||||
language: 'Langue',
|
language: 'Langue',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
castle: {
|
libra: {
|
||||||
nav: {
|
nav: {
|
||||||
record: 'Saisir',
|
record: 'Saisir',
|
||||||
transactions: 'Transactions',
|
transactions: 'Transactions',
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
||||||
Loading…
Add table
Add a link
Reference in a new issue