fix blank page, icons, add logging and EVM balance chart
- Fix blank page: set viewsRoot, base:'', history.replaceState in index.html - Fix chain icons: add dist/icons→views/mainview/icons copy rule - Add file-based logger (src/bun/lib/logger.ts) with logToFile RPC - Add frontend error overlay + global error listeners in index.svelte - Add EVM balance chart with time range filters (EvmDashboard.svelte) - Replace console.log with file logger throughout backend - Improve get-env.ts to read version.json for channel detection
This commit is contained in:
@@ -24,3 +24,4 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.router
|
.router
|
||||||
|
artifacts
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default {
|
|||||||
copy: {
|
copy: {
|
||||||
'dist/index.html': 'views/mainview/index.html',
|
'dist/index.html': 'views/mainview/index.html',
|
||||||
'dist/assets': 'views/mainview/assets',
|
'dist/assets': 'views/mainview/assets',
|
||||||
|
'dist/icons': 'views/mainview/icons',
|
||||||
'drizzle': 'drizzle',
|
'drizzle': 'drizzle',
|
||||||
'src/bun/native/biometric-helper': 'native/biometric-helper',
|
'src/bun/native/biometric-helper': 'native/biometric-helper',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>sv-router Starter</title>
|
<title>sv-router Starter</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" style="display: contents"></div>
|
<div id="app" style="display: contents"></div>
|
||||||
|
<script>
|
||||||
|
if (window.location.pathname === '/index.html') {
|
||||||
|
history.replaceState(null, '', '/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
||||||
import { db } from './lib/db'
|
import { db } from './lib/db'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { existsSync } from 'fs'
|
||||||
import { getENV } from './lib/get-env'
|
import { getENV } from './lib/get-env'
|
||||||
import { createEvmHandlers } from './rpc/evm'
|
import { createEvmHandlers } from './rpc/evm'
|
||||||
import { createMoneroHandlers } from './rpc/monero'
|
import { createMoneroHandlers } from './rpc/monero'
|
||||||
@@ -17,15 +18,22 @@ import { canPromptTouchID, promptTouchID } from './lib/biometric'
|
|||||||
import { sqlite } from './lib/sqlite'
|
import { sqlite } from './lib/sqlite'
|
||||||
import { evmWallets } from './lib/db/schema'
|
import { evmWallets } from './lib/db/schema'
|
||||||
import type { RPC } from '../lib/rpc-schema'
|
import type { RPC } from '../lib/rpc-schema'
|
||||||
|
import { log, logError } from './lib/logger'
|
||||||
|
|
||||||
const env = getENV()
|
const env = getENV()
|
||||||
|
|
||||||
console.log(env)
|
log(`env: ${JSON.stringify(env)}`)
|
||||||
|
|
||||||
if (env.env === 'prod') {
|
if (env.env === 'stable') {
|
||||||
migrate(db, {
|
try {
|
||||||
migrationsFolder: join(PATHS.RESOURCES_FOLDER, 'app', 'drizzle'),
|
log('running migrations...')
|
||||||
})
|
migrate(db, {
|
||||||
|
migrationsFolder: join(PATHS.RESOURCES_FOLDER, 'app', 'drizzle'),
|
||||||
|
})
|
||||||
|
log('migrations complete')
|
||||||
|
} catch (e) {
|
||||||
|
logError('migration failed', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEV_SERVER_PORT = 5173
|
const DEV_SERVER_PORT = 5173
|
||||||
@@ -33,19 +41,17 @@ const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
|||||||
|
|
||||||
async function getMainViewUrl(): Promise<string> {
|
async function getMainViewUrl(): Promise<string> {
|
||||||
const channel = await Updater.localInfo.channel()
|
const channel = await Updater.localInfo.channel()
|
||||||
|
log(`channel from Updater: ${channel}`)
|
||||||
if (channel === 'dev') {
|
if (channel === 'dev') {
|
||||||
try {
|
try {
|
||||||
await fetch(DEV_SERVER_URL, { method: 'HEAD' })
|
await fetch(DEV_SERVER_URL, { method: 'HEAD' })
|
||||||
console.log(
|
log(`HMR enabled: Using Vite dev server at ${DEV_SERVER_URL}`)
|
||||||
`HMR enabled: Using Vite dev server at ${DEV_SERVER_URL}`,
|
|
||||||
)
|
|
||||||
return DEV_SERVER_URL
|
return DEV_SERVER_URL
|
||||||
} catch {
|
} catch {
|
||||||
console.log(
|
log("Vite dev server not running, falling back to bundled view")
|
||||||
"Vite dev server not running. Run 'bun run dev:hmr' for HMR support.",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log('using bundled view')
|
||||||
return 'views://mainview/index.html'
|
return 'views://mainview/index.html'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +82,12 @@ ApplicationMenu.setApplicationMenu([
|
|||||||
|
|
||||||
const url = await getMainViewUrl()
|
const url = await getMainViewUrl()
|
||||||
|
|
||||||
|
log(`view URL: ${url}`)
|
||||||
|
const viewsRoot = join(PATHS.RESOURCES_FOLDER, 'app', 'views')
|
||||||
|
log(`viewsRoot: ${viewsRoot}`)
|
||||||
|
log(`viewsRoot exists: ${existsSync(viewsRoot)}`)
|
||||||
|
log(`index.html exists: ${existsSync(join(viewsRoot, 'mainview', 'index.html'))}`)
|
||||||
|
|
||||||
const moneroState = {
|
const moneroState = {
|
||||||
manager: null as MoneroWalletState | null,
|
manager: null as MoneroWalletState | null,
|
||||||
downloading: false,
|
downloading: false,
|
||||||
@@ -159,14 +171,19 @@ rpc.setRequestHandler({
|
|||||||
margin: 1,
|
margin: 1,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
logToFile: async ({ message }) => {
|
||||||
|
log(message)
|
||||||
|
},
|
||||||
...createEvmHandlers(rpc, syncState),
|
...createEvmHandlers(rpc, syncState),
|
||||||
...createMoneroHandlers(moneroState),
|
...createMoneroHandlers(moneroState),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
log('creating BrowserWindow...')
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
title: 'Koins',
|
title: 'Koins',
|
||||||
url,
|
url,
|
||||||
rpc,
|
rpc,
|
||||||
|
viewsRoot: join(PATHS.RESOURCES_FOLDER, 'app', 'views'),
|
||||||
frame: {
|
frame: {
|
||||||
width: 900,
|
width: 900,
|
||||||
height: 700,
|
height: 700,
|
||||||
@@ -174,5 +191,6 @@ const win = new BrowserWindow({
|
|||||||
y: 200,
|
y: 200,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
log('BrowserWindow created')
|
||||||
|
|
||||||
console.log('Koins app started!')
|
log('Koins app started!')
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
const env = (Bun.env.ELECTROBUN_ENV as string) || 'dev'
|
import { PATHS } from 'electrobun/bun'
|
||||||
|
import { readFileSync, existsSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
console.log('ENV', env)
|
function detectEnv(): { env: string; appName: string; dbFilename: string } {
|
||||||
|
const channel = (Bun.env.ELECTROBUN_ENV as string) || ''
|
||||||
export const getENV = () => {
|
if (channel === 'stable') {
|
||||||
let appName
|
return { env: 'stable', appName: 'koins', dbFilename: 'koins.db' }
|
||||||
let dbFilename
|
|
||||||
if (env === 'stable') {
|
|
||||||
appName = 'koins'
|
|
||||||
dbFilename = 'koins.db'
|
|
||||||
} else {
|
|
||||||
appName = 'koins-dev'
|
|
||||||
dbFilename = 'koins-dev.db'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
try {
|
||||||
env,
|
const p = join(PATHS.RESOURCES_FOLDER, 'version.json')
|
||||||
appName,
|
if (existsSync(p)) {
|
||||||
dbFilename,
|
const { channel: c } = JSON.parse(readFileSync(p, 'utf-8'))
|
||||||
}
|
if (c === 'stable') {
|
||||||
|
return { env: 'stable', appName: 'koins', dbFilename: 'koins.db' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return { env: 'dev', appName: 'koins-dev', dbFilename: 'koins-dev.db' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const env = detectEnv()
|
||||||
|
|
||||||
|
console.log('ENV', env.env)
|
||||||
|
|
||||||
|
export const getENV = () => env
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { appendFileSync, mkdirSync, existsSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import envPaths from 'env-paths'
|
||||||
|
import { getENV } from './get-env'
|
||||||
|
|
||||||
|
const { appName } = getENV()
|
||||||
|
const { data } = envPaths(appName, { suffix: '' })
|
||||||
|
const logDir = join(data, 'logs')
|
||||||
|
mkdirSync(logDir, { recursive: true })
|
||||||
|
|
||||||
|
const date = () => {
|
||||||
|
const d = new Date()
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = join(logDir, `${appName}-${date()}.log`)
|
||||||
|
|
||||||
|
export const log = (msg: string) => {
|
||||||
|
const line = `[${new Date().toISOString()}] ${msg}\n`
|
||||||
|
try {
|
||||||
|
appendFileSync(file, line, 'utf-8')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logError = (msg: string, err?: unknown) => {
|
||||||
|
const detail = err instanceof Error ? err.stack || err.message : String(err ?? '')
|
||||||
|
log(`[ERROR] ${msg}${detail ? ' — ' + detail : ''}`)
|
||||||
|
}
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
} from '$lib/states/evm-wallet.svelte.js'
|
} from '$lib/states/evm-wallet.svelte.js'
|
||||||
import { Button } from '$lib/components/ui/button/index.js'
|
import { Button } from '$lib/components/ui/button/index.js'
|
||||||
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
import { Textarea } from '$lib/components/ui/textarea/index.js'
|
||||||
import { generateMnemonic } from 'viem/accounts'
|
import { generateMnemonic } from 'viem/accounts'
|
||||||
import { english } from 'viem/accounts'
|
import { english } from 'viem/accounts'
|
||||||
import { navigate } from 'sv-router/generated'
|
import { navigate } from 'sv-router/generated'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -30,6 +30,12 @@ import { navigate } from 'sv-router/generated'
|
|||||||
import * as Dialog from '$lib/components/ui/dialog/index.js'
|
import * as Dialog from '$lib/components/ui/dialog/index.js'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import Loader from './loader.svelte'
|
import Loader from './loader.svelte'
|
||||||
|
import { LineChart } from 'layerchart'
|
||||||
|
import { scaleUtc } from 'd3-scale'
|
||||||
|
import { curveLinear } from 'd3-shape'
|
||||||
|
import * as Chart from '$lib/components/ui/chart/index.js'
|
||||||
|
import { formatEther, parseEther } from 'viem'
|
||||||
|
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
||||||
|
|
||||||
let { networkId }: { networkId: NetworkId } = $props()
|
let { networkId }: { networkId: NetworkId } = $props()
|
||||||
|
|
||||||
@@ -161,6 +167,57 @@ import { navigate } from 'sv-router/generated'
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isNative(tx: TxEntry): boolean {
|
||||||
|
return !tx.contractAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
const balanceHistory = $derived.by(() => {
|
||||||
|
const native = w.transactions.filter(isNative)
|
||||||
|
const sorted = [...native].reverse()
|
||||||
|
if (sorted.length === 0) return []
|
||||||
|
let cum = 0n
|
||||||
|
const addr = w.address.toLowerCase()
|
||||||
|
const points: { date: Date; balance: number }[] = []
|
||||||
|
for (const tx of sorted) {
|
||||||
|
console.log(tx.value)
|
||||||
|
const val = parseEther(tx.value)
|
||||||
|
if (tx.from.toLowerCase() === addr) cum -= val
|
||||||
|
else cum += val
|
||||||
|
points.push({
|
||||||
|
date: new Date(Number(tx.timeStamp) * 1000),
|
||||||
|
balance: Number(formatEther(cum)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const cur = Number(w.balance)
|
||||||
|
const offset = cur - Number(formatEther(cum))
|
||||||
|
for (const p of points) p.balance += offset
|
||||||
|
return points
|
||||||
|
})
|
||||||
|
|
||||||
|
$inspect(balanceHistory)
|
||||||
|
|
||||||
|
let selectedRange = $state<'1D' | '1W' | '1M' | '1Y' | 'ALL'>('ALL')
|
||||||
|
|
||||||
|
const rangeCutoff = $derived(
|
||||||
|
selectedRange === 'ALL'
|
||||||
|
? 0
|
||||||
|
: Date.now() -
|
||||||
|
(
|
||||||
|
{
|
||||||
|
'1D': 86400000,
|
||||||
|
'1W': 604800000,
|
||||||
|
'1M': 2592000000,
|
||||||
|
'1Y': 31536000000,
|
||||||
|
} as const
|
||||||
|
)[selectedRange],
|
||||||
|
)
|
||||||
|
|
||||||
|
const chartData = $derived(
|
||||||
|
selectedRange === 'ALL'
|
||||||
|
? balanceHistory
|
||||||
|
: balanceHistory.filter((p) => p.date.getTime() >= rangeCutoff),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !w.ready}
|
{#if !w.ready}
|
||||||
@@ -437,12 +494,23 @@ import { navigate } from 'sv-router/generated'
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if gasPrice}
|
{#if gasPrice}
|
||||||
{@const gasNative = Number(gasPrice) * 21000 / 1e9}
|
{@const gasNative = (Number(gasPrice) * 21000) / 1e9}
|
||||||
<p class="font-mono text-xs text-muted-foreground mt-1">
|
<p class="font-mono text-xs text-muted-foreground mt-1">
|
||||||
Gas: {Number(gasPrice).toFixed(1)} Gwei
|
Gas: {Number(gasPrice).toFixed(1)} Gwei ({gasNative.toLocaleString(
|
||||||
({gasNative.toLocaleString(undefined, { minimumFractionDigits: 6, maximumFractionDigits: 6 })} {w.symbol})
|
undefined,
|
||||||
|
{
|
||||||
|
minimumFractionDigits: 6,
|
||||||
|
maximumFractionDigits: 6,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
{w.symbol})
|
||||||
{#if nativePrice}
|
{#if nativePrice}
|
||||||
${(gasNative * Number(nativePrice.value)).toLocaleString(undefined, { minimumFractionDigits: 4, maximumFractionDigits: 4 })}
|
${(
|
||||||
|
gasNative * Number(nativePrice.value)
|
||||||
|
).toLocaleString(undefined, {
|
||||||
|
minimumFractionDigits: 4,
|
||||||
|
maximumFractionDigits: 4,
|
||||||
|
})}
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -519,6 +587,88 @@ import { navigate } from 'sv-router/generated'
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{#if balanceHistory.length > 1}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<CardTitle>Balance</CardTitle>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
{#each ['1D', '1W', '1M', '1Y', 'ALL'] as const as range}
|
||||||
|
<button
|
||||||
|
onclick={() => (selectedRange = range)}
|
||||||
|
class="rounded px-1.5 py-0.5 text-xs font-medium transition-colors {selectedRange ===
|
||||||
|
range
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'}">
|
||||||
|
{range}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if chartData.length > 1}
|
||||||
|
<Chart.Container
|
||||||
|
config={{
|
||||||
|
balance: { label: w.symbol, color: 'var(--chart-1)' },
|
||||||
|
}}>
|
||||||
|
<LineChart
|
||||||
|
data={chartData}
|
||||||
|
x="date"
|
||||||
|
xScale={scaleUtc()}
|
||||||
|
axis="x"
|
||||||
|
points
|
||||||
|
series={[
|
||||||
|
{
|
||||||
|
key: 'balance',
|
||||||
|
label: w.symbol,
|
||||||
|
color: 'var(--chart-1)',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
yDomain={[
|
||||||
|
chartData.reduce((a, b) =>
|
||||||
|
a.balance < b.balance ? a : b,
|
||||||
|
).balance,
|
||||||
|
chartData.reduce((a, b) =>
|
||||||
|
a.balance > b.balance ? a : b,
|
||||||
|
).balance,
|
||||||
|
]}
|
||||||
|
props={{
|
||||||
|
spline: {
|
||||||
|
curve: curveLinear,
|
||||||
|
motion: 'tween',
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
yAxis: {},
|
||||||
|
xAxis: {
|
||||||
|
format: (v: Date) =>
|
||||||
|
v.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
highlight: { points: { r: 4 } },
|
||||||
|
}}>
|
||||||
|
{#snippet tooltip()}
|
||||||
|
<Chart.Tooltip
|
||||||
|
labelFormatter={(value: Date) => {
|
||||||
|
return value.toLocaleDateString()
|
||||||
|
}}
|
||||||
|
valueFormatter={(value: Number) => {
|
||||||
|
return Number(value).toFixed(6)
|
||||||
|
}} />
|
||||||
|
{/snippet}
|
||||||
|
</LineChart>
|
||||||
|
</Chart.Container>
|
||||||
|
{:else}
|
||||||
|
<p class="text-center text-sm text-muted-foreground">
|
||||||
|
No transactions in this time range.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Transactions</CardTitle>
|
<CardTitle>Transactions</CardTitle>
|
||||||
|
|||||||
@@ -23,3 +23,14 @@ const rpc = Electroview.defineRPC<RPC>({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export const electrobun = new Electroview({ rpc })
|
export const electrobun = new Electroview({ rpc })
|
||||||
|
|
||||||
|
export async function frontendLog(message: string) {
|
||||||
|
try {
|
||||||
|
await electrobun.rpc?.request.logToFile({ message: `[FE] ${message}` })
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function frontendLogError(message: string, err?: unknown) {
|
||||||
|
const detail = err instanceof Error ? err.stack || err.message : String(err ?? '')
|
||||||
|
await frontendLog(`[ERROR] ${message} — ${detail}`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export type GeneralRpcRequests = {
|
|||||||
setSecret: { params: { service: string; name: string; value: string }; response: void }
|
setSecret: { params: { service: string; name: string; value: string }; response: void }
|
||||||
openExternal: { params: { url: string }; response: void }
|
openExternal: { params: { url: string }; response: void }
|
||||||
generateQrCode: { params: { text: string; size?: number }; response: string }
|
generateQrCode: { params: { text: string; size?: number }; response: string }
|
||||||
|
logToFile: { params: { message: string }; response: void }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EvmRpcRequests = {
|
export type EvmRpcRequests = {
|
||||||
|
|||||||
@@ -11,15 +11,50 @@
|
|||||||
} from '$lib/components/ui/card/index.js'
|
} from '$lib/components/ui/card/index.js'
|
||||||
import { navigate } from 'sv-router/generated'
|
import { navigate } from 'sv-router/generated'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
import { frontendLog, frontendLogError } from '$lib/electrobun.js'
|
||||||
|
|
||||||
const w = wallet
|
const w = wallet
|
||||||
|
|
||||||
|
let feError = $state<string | null>(null)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
w.init()
|
frontendLog('index.svelte mounted')
|
||||||
moneroWallet.init()
|
w.init().catch((e) => frontendLogError('w.init() failed', e))
|
||||||
|
moneroWallet.init().catch((e) => frontendLogError('moneroWallet.init() failed', e))
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const handler = (event: ErrorEvent) => {
|
||||||
|
const msg = event.error?.stack || event.error?.message || event.message || 'unknown error'
|
||||||
|
feError = msg
|
||||||
|
frontendLogError('uncaught error', msg)
|
||||||
|
}
|
||||||
|
const rejectHandler = (event: PromiseRejectionEvent) => {
|
||||||
|
const msg = event.reason?.stack || event.reason?.message || String(event.reason || 'unknown')
|
||||||
|
feError = msg
|
||||||
|
frontendLogError('unhandled rejection', msg)
|
||||||
|
}
|
||||||
|
window.addEventListener('error', handler)
|
||||||
|
window.addEventListener('unhandledrejection', rejectHandler)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('error', handler)
|
||||||
|
window.removeEventListener('unhandledrejection', rejectHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if feError}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-background/95 p-8">
|
||||||
|
<div class="max-w-lg rounded-lg border border-destructive/50 bg-destructive/10 p-6">
|
||||||
|
<p class="font-bold text-destructive text-sm mb-2">Frontend Error</p>
|
||||||
|
<pre class="whitespace-pre-wrap font-mono text-xs text-destructive/80 max-h-96 overflow-y-auto">{feError}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="mx-auto mt-24 max-w-sm">
|
<div class="mx-auto mt-24 max-w-sm">
|
||||||
{#if !w.ready}
|
{#if !w.ready}
|
||||||
<p class="text-center text-muted-foreground text-sm">
|
<p class="text-center text-muted-foreground text-sm">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { defineConfig } from 'vite'
|
|||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
base: '',
|
||||||
plugins: [tailwindcss(), svelte(), router()],
|
plugins: [tailwindcss(), svelte(), router()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user