feat: Lint fixes, add forgejo workflow and e2e tests
Some checks failed
/ check-changes (push) Successful in 7s
/ build (push) Failing after 1m18s

This commit is contained in:
Djuri 2025-05-03 18:45:32 +02:00
parent af2f593fb8
commit 5917713b0d
Signed by: djuri
GPG key ID: 61B9B2DDE5AA3AC1
39 changed files with 1666 additions and 1506 deletions

View file

@ -0,0 +1,132 @@
on:
push:
branches:
- main
pull_request:
jobs:
check-changes:
runs-on: docker
outputs:
all_changed_and_modified_files_count: ${{ steps.changed-files.outputs.all_changed_and_modified_files_count }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get changed files count
id: changed-files
uses: tj-actions/changed-files@v45
with:
files_ignore: 'doc/**,README.md,Dockerfile,.*'
files_ignore_separator: ','
- name: Print changed files count
run: >
echo "Changed files count: ${{
steps.changed-files.outputs.all_changed_and_modified_files_count }}"
build:
needs: check-changes
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:js-22.04
if: ${{ needs.check-changes.outputs.all_changed_and_modified_files_count >= 1 }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
token: ${{ secrets.GH_TOKEN }}
node-version: lts/*
cache: yarn
cache-dependency-path: '**/yarn.lock'
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/node_modules
~/.cache/ms-playwright
key: ${{ runner.os }}-pio-playwright-${{ hashFiles('**/yarn.lock') }}
- name: Get current date
id: dateAndTime
run: echo "dateAndTime=$(date +'%Y-%m-%d-%H:%M')" >> $GITHUB_OUTPUT
- name: Install mklittlefs
run: >
git clone https://github.com/earlephilhower/mklittlefs.git /tmp/mklittlefs &&
cd /tmp/mklittlefs &&
git submodule update --init &&
make dist
- name: Install yarn
run: pnpm
- name: Run linter
run: pnpm lint
- name: Run vitest tests
run: pnpm vitest run
- name: Install Playwright Browsers
if: steps.cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Build WebUI
run: yarn build
# The following steps only run on push to main
- name: Get current block
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
id: getBlockHeight
run: echo "blockHeight=$(curl -s https://mempool.space/api/blocks/tip/height)" >> $GITHUB_OUTPUT
- name: Write block height to file
env:
BLOCK_HEIGHT: ${{ steps.getBlockHeight.outputs.blockHeight }}
run: mkdir -p output && echo "$BLOCK_HEIGHT" > output/version.txt
- name: gzip build for LittleFS
run: find dist -type f ! -name ".*" -exec sh -c 'mkdir -p "build_gz/$(dirname "${1#dist/}")" && gzip -k "$1" -c > "build_gz/${1#dist/}".gz' _ {} \;
- name: Write git rev to file
run: echo "$GITHUB_SHA" > build_gz/fs_hash.txt && echo "$GITHUB_SHA" > output/commit.txt
- name: Check GZipped directory size
run: |
# Set the threshold size in bytes
THRESHOLD=410000
# Calculate the total size of files in the directory
DIRECTORY_SIZE=$(du -b -s build_gz | awk '{print $1}')
# Fail the workflow if the size exceeds the threshold
if [ "$DIRECTORY_SIZE" -gt "$THRESHOLD" ]; then
echo "Directory size exceeds the threshold of $THRESHOLD bytes"
exit 1
else
echo "Directory size is within the threshold $DIRECTORY_SIZE"
fi
- name: Create tarball
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: tar czf webui.tgz --strip-components=1 dist
- name: Build LittleFS
run: |
set -e
/tmp/mklittlefs/mklittlefs -c build_gz -s 410000 output/littlefs.bin
- name: Upload artifacts
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
path: |
webui.tgz
output/littlefs.bin
- name: Create release
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: https://code.forgejo.org/actions/forgejo-release@v2.6.0
with:
url: 'https://git.btclock.dev/'
repo: '${{ github.repository }}'
direction: upload
tag: ${{ steps.getBlockHeight.outputs.blockHeight }}
sha: '${{ github.sha }}'
release-dir: output
token: ${{ secrets.TOKEN }}
override: false
verbose: false
release-notes-assistant: false

View file

@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => { test('index page has expected columns control, status, settings', async ({ page }) => {
await page.goto('/'); await page.goto('/');
await expect(page.locator('h1')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Control' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible();
}); });

View file

@ -20,7 +20,9 @@ export default ts.config(
languageOptions: { languageOptions: {
globals: { ...globals.browser, ...globals.node } globals: { ...globals.browser, ...globals.node }
}, },
rules: { 'no-undef': 'off' } rules: {
'no-undef': 'off'
}
}, },
{ {
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
@ -32,5 +34,10 @@ export default ts.config(
svelteConfig svelteConfig
} }
} }
},
{
rules: {
'svelte/no-at-html-tags': 'off'
}
} }
); );

View file

@ -1,6 +1,5 @@
@import 'tailwindcss'; @import 'tailwindcss';
@plugin "daisyui" { @plugin "daisyui" {
} }
:root { :root {
@ -13,7 +12,8 @@ html {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
html, body { html,
body {
@apply h-full; @apply h-full;
} }

View file

@ -2,11 +2,10 @@
<html lang="%paraglide.lang%"> <html lang="%paraglide.lang%">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents" class="h-full">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View file

@ -1,4 +1,3 @@
import { PUBLIC_BASE_URL } from '$env/static/public';
import { baseUrl } from './env'; import { baseUrl } from './env';
/** /**
@ -56,14 +55,14 @@ export const generateRandomColor = () => {
*/ */
export const setActiveScreen = async (screenId: string) => { export const setActiveScreen = async (screenId: string) => {
return fetch(`${baseUrl}/api/show/screen/${screenId}`); return fetch(`${baseUrl}/api/show/screen/${screenId}`);
} };
/** /**
* Sets the active currency * Sets the active currency
*/ */
export const setActiveCurrency = async (currency: string) => { export const setActiveCurrency = async (currency: string) => {
return fetch(`${baseUrl}/api/show/currency/${currency}`); return fetch(`${baseUrl}/api/show/currency/${currency}`);
} };
/** /**
* Turns on the frontlight * Turns on the frontlight

View file

@ -60,10 +60,6 @@
// $: if (containerWidth > 0) { // $: if (containerWidth > 0) {
// containerHeight = containerWidth * deviceRatio; // containerHeight = containerWidth * deviceRatio;
// } // }
const fontSizeSingle = '4.5rem';
const fontSizeMedium = '2.0rem';
const fontSizeSplit = '1.0rem';
</script> </script>
<div <div
@ -190,7 +186,6 @@
font-family: var(--font-family); font-family: var(--font-family);
} }
.vertical-desc .split-text { .vertical-desc .split-text {
transform: rotate(270deg); transform: rotate(270deg);
height: 50%; height: 50%;

View file

@ -1,10 +1,5 @@
<script lang="ts"> <script lang="ts">
let { let { currency, active = false, onClick, ...restProps } = $props();
currency,
active = false,
onClick,
...restProps
} = $props();
</script> </script>
<button <button

View file

@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
let { let {
value = $bindable(''), value = $bindable(''),
label = "", label = '',
placeholder = "", placeholder = '',
id = "", id = '',
type = "text", type = 'text',
...restProps ...restProps
} = $props(); } = $props();
</script> </script>
@ -15,12 +15,5 @@
<span class="label-text">{label}</span> <span class="label-text">{label}</span>
</label> </label>
{/if} {/if}
<input <input {type} {placeholder} {id} class="input input-bordered w-full" bind:value {...restProps} />
type={type}
{placeholder}
{id}
class="input input-bordered w-full"
bind:value
{...restProps}
/>
</div> </div>

View file

@ -1,21 +1,10 @@
<script lang="ts"> <script lang="ts">
let { let { checked = $bindable(false), label = '', id = '', ...restProps } = $props();
checked = $bindable(false),
label = "",
id = "",
...restProps
} = $props();
</script> </script>
<label class="flex items-center justify-between gap-2 cursor-pointer"> <label class="flex cursor-pointer items-center justify-between gap-2">
{#if label} {#if label}
<span class="label-text text-xs">{label}</span> <span class="label-text text-xs">{label}</span>
{/if} {/if}
<input <input type="checkbox" class="toggle toggle-primary toggle-xs" {id} bind:checked {...restProps} />
type="checkbox"
class="toggle toggle-primary toggle-xs"
{id}
bind:checked
{...restProps}
/>
</label> </label>

View file

@ -1,12 +1,8 @@
<script lang="ts"> <script lang="ts">
let { let { title, open = $bindable(false), ...restProps } = $props();
title,
open = $bindable(false),
...restProps
} = $props();
</script> </script>
<div class="collapse collapse-arrow bg-base-200 rounded-lg mb-2" {...restProps}> <div class="collapse-arrow bg-base-200 collapse mb-2 rounded-lg" {...restProps}>
<input type="checkbox" bind:checked={open} /> <input type="checkbox" bind:checked={open} />
<div class="collapse-title text-lg font-medium"> <div class="collapse-title text-lg font-medium">
{title} {title}

View file

@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
let { let { ...restProps } = $props();
...restProps
} = $props();
import { setLocale, getLocale } from '$lib/paraglide/runtime'; import { setLocale, getLocale } from '$lib/paraglide/runtime';
import { locales } from '$lib/paraglide/runtime'; import { locales } from '$lib/paraglide/runtime';
@ -21,12 +19,12 @@
} }
const getLocaleName = (locale: string) => { const getLocaleName = (locale: string) => {
return new Intl.DisplayNames([locale], { type: 'language' }).of(locale) return new Intl.DisplayNames([locale], { type: 'language' }).of(locale);
} };
const getLanguageName = (locale: string) => { const getLanguageName = (locale: string) => {
return getLocaleName(locale.split('-')[0]) return getLocaleName(locale.split('-')[0]);
} };
const getEmojiFlag = (locale: string) => { const getEmojiFlag = (locale: string) => {
const countryCode = locale.split('-')[1]; const countryCode = locale.split('-')[1];
@ -36,26 +34,40 @@
} }
return [...countryCode.toUpperCase()] return [...countryCode.toUpperCase()]
.map(char => String.fromCodePoint(127397 + char.charCodeAt(0))) .map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
.join(''); .join('');
} };
// Function to get the current flag // Function to get the current flag
const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧'; const getCurrentFlag = () => getEmojiFlag(getLocale()) || '🇬🇧';
</script> </script>
<div class="navbar bg-base-100 fixed top-0 z-50 shadow-sm w-full" {...restProps}> <div class="navbar bg-base-100 fixed top-0 z-50 w-full shadow-sm" {...restProps}>
<div class="navbar-start"> <div class="navbar-start">
<div class="dropdown"> <div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden"> <div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /> xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg> </svg>
</div> </div>
<ul tabindex="-1" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"> <ul
{#each navItems as { href, label }} tabindex="-1"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
>
{#each navItems as { href, label } (href)}
<li> <li>
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a> <a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
</li> </li>
{/each} {/each}
</ul> </ul>
@ -64,9 +76,9 @@
</div> </div>
<div class="navbar-center hidden lg:flex"> <div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1"> <ul class="menu menu-horizontal px-1">
{#each navItems as { href, label }} {#each navItems as { href, label } (href)}
<li> <li>
<a href={href} class={isActive(href) ? 'menu-active' : ''}>{label}</a> <a {href} class={isActive(href) ? 'menu-active' : ''}>{label}</a>
</li> </li>
{/each} {/each}
</ul> </ul>
@ -76,12 +88,18 @@
<div tabindex="0" role="button" class="btn btn-ghost"> <div tabindex="0" role="button" class="btn btn-ghost">
<span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span> <span class="text-sm">{getCurrentFlag()} {getLanguageName(getLocale())}</span>
</div> </div>
<ul tabindex="-1" class="mt-3 z-[1] p-2 shadow menu dropdown-content bg-base-100 rounded-box w-auto"> <ul
{#each locales as locale} tabindex="-1"
<li><button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap">{getEmojiFlag(locale)} {getLanguageName(locale)}</button></li> class="menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-auto p-2 shadow"
>
{#each locales as locale (locale)}
<li>
<button onclick={() => setLocale(locale)} class="flex items-center gap-2 text-nowrap"
>{getEmojiFlag(locale)} {getLanguageName(locale)}</button
>
</li>
{/each} {/each}
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>

View file

@ -17,10 +17,10 @@
import type { LedStatus } from '$lib/types'; import type { LedStatus } from '$lib/types';
let ledStatus = $state<LedStatus[]>([ let ledStatus = $state<LedStatus[]>([
{hex: '#000000'}, { hex: '#000000' },
{hex: '#000000'}, { hex: '#000000' },
{hex: '#000000'}, { hex: '#000000' },
{hex: '#000000'} { hex: '#000000' }
]); ]);
let customText = $state(''); let customText = $state('');
let keepLedsSameColor = $state(false); let keepLedsSameColor = $state(false);
@ -81,7 +81,7 @@
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="mb-4 flex flex-wrap gap-2"> <div class="mb-4 flex flex-wrap gap-2">
{#if ledStatus.length > 0} {#if ledStatus.length > 0}
{#each ledStatus as led} {#each ledStatus as led (led)}
<input <input
type="color" type="color"
class="btn btn-square" class="btn btn-square"
@ -93,7 +93,9 @@
<Toggle label={m['sections.control.keepSameColor']()} bind:checked={keepLedsSameColor} /> <Toggle label={m['sections.control.keepSameColor']()} bind:checked={keepLedsSameColor} />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button class="btn btn-secondary" onclick={turnOffLeds}>{m['section.control.turnOff']()}</button> <button class="btn btn-secondary" onclick={turnOffLeds}
>{m['section.control.turnOff']()}</button
>
<button class="btn btn-primary" onclick={() => setLEDcolor(ledStatus)} <button class="btn btn-primary" onclick={() => setLEDcolor(ledStatus)}
>{m['section.control.setColor']()}</button >{m['section.control.setColor']()}</button
> >
@ -104,17 +106,22 @@
{#if $settings.hasFrontlight && !$settings.flDisable} {#if $settings.hasFrontlight && !$settings.flDisable}
<div> <div>
<h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3> <h3 class="mb-2 font-medium">{m['section.control.frontlight']()}</h3>
<div class="flex gap-2 justify-end"> <div class="flex justify-end gap-2">
<button class="btn btn-secondary" onclick={() => turnOnFrontlight()}>{m['section.control.turnOn']()}</button> <button class="btn btn-secondary" onclick={() => turnOnFrontlight()}
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}>{m['section.control.turnOff']()}</button> >{m['section.control.turnOn']()}</button
<button class="btn btn-accent" onclick={() => flashFrontlight()}>{m['section.control.flashFrontlight']()}</button> >
<button class="btn btn-primary" onclick={() => turnOffFrontlight()}
>{m['section.control.turnOff']()}</button
>
<button class="btn btn-accent" onclick={() => flashFrontlight()}
>{m['section.control.flashFrontlight']()}</button
>
</div> </div>
</div> </div>
{/if} {/if}
<div> <div>
<h3 class="mb-2 font-medium">{m['section.control.title']()}</h3> <div class="flex justify-end gap-2">
<div class="flex gap-2 justify-end">
<button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button> <button class="btn btn-error" onclick={restartClock}>{m['button.restart']()}</button>
<button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button> <button class="btn" onclick={forceFullRefresh}>{m['button.forceFullRefresh']()}</button>
</div> </div>

View file

@ -20,43 +20,51 @@
} }
</script> </script>
<CardContainer title={m["section.settings.title"]()} {...restProps}> <CardContainer title={m['section.settings.title']()} {...restProps}>
<div class="flex justify-end gap-2 mb-4"> <div class="mb-4 flex justify-end gap-2">
<button class="btn btn-sm" onclick={toggleShowAll}>{m["section.settings.showAll"]()}</button> <button class="btn btn-sm" onclick={toggleShowAll}>{m['section.settings.showAll']()}</button>
<button class="btn btn-sm" onclick={toggleHideAll}>{m["section.settings.hideAll"]()}</button> <button class="btn btn-sm" onclick={toggleHideAll}>{m['section.settings.hideAll']()}</button>
</div> </div>
<div class="grid gap-4 grid-cols-2"> <div class="grid grid-cols-2 gap-4">
<CollapsibleSection
<CollapsibleSection title={m["section.settings.section.screenSettings"]()} open={showAll || !hideAll}> title={m['section.settings.section.screenSettings']()}
<div class="grid gap-4 grid-cols-2"> open={showAll || !hideAll}
>
<div class="grid grid-cols-2 gap-4">
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.StealFocusOnNewBlock"]()} label={m['section.settings.StealFocusOnNewBlock']()}
bind:checked={$settings.stealFocus} bind:checked={$settings.stealFocus}
/> />
<p class="text-xs">When a new block is mined, it will switch focus from the current screen.</p> <p class="text-xs">
When a new block is mined, it will switch focus from the current screen.
</p>
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.useBigCharsMcap"]()} label={m['section.settings.useBigCharsMcap']()}
bind:checked={$settings.mcapBigChar} bind:checked={$settings.mcapBigChar}
/> />
<p class="text-xs">Use big characters for the market cap screen instead of using a suffix.</p> <p class="text-xs">
Use big characters for the market cap screen instead of using a suffix.
</p>
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.useBlkCountdown"]()} label={m['section.settings.useBlkCountdown']()}
bind:checked={$settings.useBlkCountdown} bind:checked={$settings.useBlkCountdown}
/> />
<p class="text-xs">When enabled it count down blocks instead of years/monts/days/hours/minutes.</p> <p class="text-xs">
When enabled it count down blocks instead of years/monts/days/hours/minutes.
</p>
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.useSatsSymbol"]()} label={m['section.settings.useSatsSymbol']()}
bind:checked={$settings.useSatsSymbol} bind:checked={$settings.useSatsSymbol}
/> />
<p class="text-xs">Prefix satoshi amounts with the sats symbol.</p> <p class="text-xs">Prefix satoshi amounts with the sats symbol.</p>
@ -64,7 +72,7 @@
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.suffixPrice"]()} label={m['section.settings.suffixPrice']()}
bind:checked={$settings.suffixPrice} bind:checked={$settings.suffixPrice}
/> />
<p class="text-xs">Always use a suffix for the ticker screen.</p> <p class="text-xs">Always use a suffix for the ticker screen.</p>
@ -72,7 +80,7 @@
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.verticalDesc"]()} label={m['section.settings.verticalDesc']()}
bind:checked={$settings.verticalDesc} bind:checked={$settings.verticalDesc}
/> />
<p class="text-xs">Rotate the description of the screen 90 degrees.</p> <p class="text-xs">Rotate the description of the screen 90 degrees.</p>
@ -80,44 +88,50 @@
</div> </div>
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection title={m["section.settings.screens"]()} open={showAll || !hideAll}> <CollapsibleSection title={m['section.settings.screens']()} open={showAll || !hideAll}>
<div class="grid gap-4 grid-cols-2"> <div class="grid grid-cols-2 gap-4">
{#each $settings.screens as screen} {#each $settings.screens as screen (screen.id)}
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label={screen.name} checked={screen.enabled} />
label={screen.name}
checked={screen.enabled}
/>
</div> </div>
{/each} {/each}
</div> </div>
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection title={m["section.settings.currencies"]()} open={showAll || !hideAll}> <CollapsibleSection title={m['section.settings.currencies']()} open={showAll || !hideAll}>
<div class="alert alert-warning"> <div class="alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg> <svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/></svg
>
<span>restart required</span> <span>restart required</span>
</div> </div>
<div class="grid gap-4 grid-cols-2"> <div class="grid grid-cols-2 gap-4">
{#each $settings.actCurrencies as currency (currency)}
{#each $settings.actCurrencies as currency}
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label={currency} checked={$settings.actCurrencies.includes(currency)} />
label={currency}
checked={$settings.actCurrencies.includes(currency)}
/>
</div> </div>
{/each} {/each}
</div> </div>
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection title={m["section.settings.section.displaysAndLed"]()} open={showAll || !hideAll}> <CollapsibleSection
title={m['section.settings.section.displaysAndLed']()}
open={showAll || !hideAll}
>
<div class="grid gap-4"> <div class="grid gap-4">
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.textColor"]()}</span> <span class="label-text">{m['section.settings.textColor']()}</span>
</label> </label>
<select class="select select-bordered w-full"> <select class="select select-bordered w-full">
<option>White on Black</option> <option>White on Black</option>
@ -136,60 +150,57 @@
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.timePerScreen"]()}</span> <span class="label-text">{m['section.settings.timePerScreen']()}</span>
</label> </label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input type="number" class="input input-bordered w-20" min="1" max="60" value="1" /> <input type="number" class="input input-bordered w-20" min="1" max="60" value="1" />
<span>{m["time.minutes"]()}</span> <span>{m['time.minutes']()}</span>
</div> </div>
</div> </div>
<div class="form-control flex justify-between"> <div class="form-control flex justify-between">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.fullRefreshEvery"]()}</span> <span class="label-text">{m['section.settings.fullRefreshEvery']()}</span>
</label> </label>
<div class="w-auto input"> <div class="input w-auto">
<input type="number" class="" min="1" max="60" value="60" /> <input type="number" class="" min="1" max="60" value="60" />
<span class="label">{m["time.minutes"]()}</span> <span class="label">{m['time.minutes']()}</span>
</div> </div>
</div> </div>
<div class="form-control flex justify-between"> <div class="form-control flex justify-between">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.timeBetweenPriceUpdates"]()}</span> <span class="label-text">{m['section.settings.timeBetweenPriceUpdates']()}</span>
</label> </label>
<div class="w-auto input"> <div class="input w-auto">
<input type="number" class="" min="1" max="60" value="30" /> <input type="number" class="" min="1" max="60" value="30" />
<span class="label">{m["time.seconds"]()}</span> <span class="label">{m['time.seconds']()}</span>
</div> </div>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.ledBrightness"]()}</span> <span class="label-text">{m['section.settings.ledBrightness']()}</span>
</label> </label>
<input type="range" min="0" max="100" class="range" value="50" /> <input type="range" min="0" max="100" class="range" value="50" />
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.ledPowerOnTest"]()} label={m['section.settings.ledPowerOnTest']()}
checked={$settings.ledTestOnPower} checked={$settings.ledTestOnPower}
/> />
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle
label={m["section.settings.ledFlashOnBlock"]()} label={m['section.settings.ledFlashOnBlock']()}
checked={$settings.ledFlashOnUpd} checked={$settings.ledFlashOnUpd}
/> />
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label={m['section.settings.disableLeds']()} checked={$settings.disableLeds} />
label={m["section.settings.disableLeds"]()}
checked={$settings.disableLeds}
/>
</div> </div>
</div> </div>
</CollapsibleSection> </CollapsibleSection>
@ -198,46 +209,37 @@
<CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}> <CollapsibleSection title="Frontlight Settings" open={showAll || !hideAll}>
<div class="grid gap-4"> <div class="grid gap-4">
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label="Disable Frontlight" checked={$settings.flDisable} />
label="Disable Frontlight"
checked={$settings.flDisable}
/>
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label="Always On" checked={$settings.flAlwaysOn} />
label="Always On"
checked={$settings.flAlwaysOn}
/>
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label="Flash on Updates" checked={$settings.flFlashOnUpd} />
label="Flash on Updates"
checked={$settings.flFlashOnUpd}
/>
</div> </div>
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label="Flash on Zaps" checked={$settings.flFlashOnZap} />
label="Flash on Zaps"
checked={$settings.flFlashOnZap}
/>
</div> </div>
{#if $settings.hasLightLevel} {#if $settings.hasLightLevel}
<div class="form-control"> <div class="form-control">
<Toggle <Toggle label="Turn Off in Dark" checked={$settings.flOffWhenDark} />
label="Turn Off in Dark"
checked={$settings.flOffWhenDark}
/>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">Light Level Threshold</span> <span class="label-text">Light Level Threshold</span>
</label> </label>
<input type="range" min="0" max="255" class="range" value={$settings.luxLightToggle} /> <input
type="range"
min="0"
max="255"
class="range"
value={$settings.luxLightToggle}
/>
</div> </div>
{/if} {/if}
@ -245,97 +247,115 @@
<label class="label"> <label class="label">
<span class="label-text">Maximum Brightness</span> <span class="label-text">Maximum Brightness</span>
</label> </label>
<input type="range" min="0" max="4095" class="range" value={$settings.flMaxBrightness} /> <input
type="range"
min="0"
max="4095"
class="range"
value={$settings.flMaxBrightness}
/>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">Effect Delay (ms)</span> <span class="label-text">Effect Delay (ms)</span>
</label> </label>
<input type="number" class="input input-bordered w-20" min="10" max="1000" value={$settings.flEffectDelay} /> <input
type="number"
class="input input-bordered w-20"
min="10"
max="1000"
value={$settings.flEffectDelay}
/>
</div> </div>
</div> </div>
</CollapsibleSection> </CollapsibleSection>
{/if} {/if}
<CollapsibleSection title={m["section.settings.section.dataSource"]()} open={showAll || !hideAll}> <CollapsibleSection
title={m['section.settings.section.dataSource']()}
open={showAll || !hideAll}
>
<div class="grid gap-4"> <div class="grid gap-4">
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.dataSource.label"]()}</span> <span class="label-text">{m['section.settings.dataSource.label']()}</span>
</label> </label>
<select class="select select-bordered w-full"> <select class="select select-bordered w-full">
<option value="btclock">{m["section.settings.dataSource.btclock"]()}</option> <option value="btclock">{m['section.settings.dataSource.btclock']()}</option>
<option value="thirdparty">{m["section.settings.dataSource.thirdParty"]()}</option> <option value="thirdparty">{m['section.settings.dataSource.thirdParty']()}</option>
<option value="nostr">{m["section.settings.dataSource.nostr"]()}</option> <option value="nostr">{m['section.settings.dataSource.nostr']()}</option>
<option value="custom">{m["section.settings.dataSource.custom"]()}</option> <option value="custom">{m['section.settings.dataSource.custom']()}</option>
</select> </select>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.mempoolnstance"]()}</span> <span class="label-text">{m['section.settings.mempoolnstance']()}</span>
</label> </label>
<input type="text" class="input input-bordered w-full" value="mempool.space/coinlcp.io" /> <input type="text" class="input input-bordered w-full" value="mempool.space/coinlcp.io" />
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.ceEndpoint"]()}</span> <span class="label-text">{m['section.settings.ceEndpoint']()}</span>
</label> </label>
<input type="text" class="input input-bordered w-full" placeholder="Custom Endpoint URL" /> <input
</div> type="text"
</div> class="input input-bordered w-full"
</CollapsibleSection> placeholder="Custom Endpoint URL"
<CollapsibleSection title={m["section.settings.section.extraFeatures"]()} open={showAll || !hideAll}>
<div class="grid gap-4">
<div class="form-control">
<Toggle
label={m["section.settings.timeBasedDnd"]()}
checked={$settings.dnd.enabled}
/> />
</div> </div>
</div> </div>
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection title={m["section.settings.section.system"]()} open={showAll || !hideAll}> <CollapsibleSection
title={m['section.settings.section.extraFeatures']()}
open={showAll || !hideAll}
>
<div class="grid gap-4">
<div class="form-control">
<Toggle label={m['section.settings.timeBasedDnd']()} checked={$settings.dnd.enabled} />
</div>
</div>
</CollapsibleSection>
<CollapsibleSection title={m['section.settings.section.system']()} open={showAll || !hideAll}>
<div class="grid gap-4"> <div class="grid gap-4">
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.timezoneOffset"]()}</span> <span class="label-text">{m['section.settings.timezoneOffset']()}</span>
</label> </label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<select class="select select-bordered w-full"> <select class="select select-bordered w-full">
<option>Europe/Amsterdam</option> <option>Europe/Amsterdam</option>
</select> </select>
<button class="btn">{m["auto-detect"]()}</button> <button class="btn">{m['auto-detect']()}</button>
</div> </div>
<p class="text-sm mt-1">{m["section.settings.tzOffsetHelpText"]()}</p> <p class="mt-1 text-sm">{m['section.settings.tzOffsetHelpText']()}</p>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.hostnamePrefix"]()}</span> <span class="label-text">{m['section.settings.hostnamePrefix']()}</span>
</label> </label>
<input type="text" class="input input-bordered w-full" value="btclock" /> <input type="text" class="input input-bordered w-full" value="btclock" />
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label"> <label class="label">
<span class="label-text">{m["section.settings.wpTimeout"]()}</span> <span class="label-text">{m['section.settings.wpTimeout']()}</span>
</label> </label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input type="number" class="input input-bordered w-20" min="1" max="900" value="600" /> <input type="number" class="input input-bordered w-20" min="1" max="900" value="600" />
<span>{m["time.seconds"]()}</span> <span>{m['time.seconds']()}</span>
</div> </div>
</div> </div>
</div> </div>
</CollapsibleSection> </CollapsibleSection>
</div> </div>
<div class="flex justify-between mt-6"> <div class="mt-6 flex justify-between">
<button class="btn btn-error">{m["button.reset"]()}</button> <button class="btn btn-error">{m['button.reset']()}</button>
<button class="btn btn-primary">{m["button.save"]()}</button> <button class="btn btn-primary">{m['button.save']()}</button>
</div> </div>
</CardContainer> </CardContainer>

View file

@ -7,26 +7,29 @@
import { DataSourceType } from '$lib/types'; import { DataSourceType } from '$lib/types';
import { toUptimestring } from '$lib/utils'; import { toUptimestring } from '$lib/utils';
const screens = $settings.screens.map(screen => ({ const screens = $settings.screens.map((screen) => ({
id: screen.id, id: screen.id,
label: screen.name label: screen.name
})); }));
</script> </script>
<CardContainer title={m['section.status.title']()}> <CardContainer title={m['section.status.title']()}>
<div class="space-y-4 mx-auto"> <div class="mx-auto space-y-4">
<div class="join"> <div class="join">
{#each screens as screen} {#each screens as screen (screen.id)}
<TabButton active={$status.currentScreen === screen.id} onClick={() => setActiveScreen(screen.id)}> <TabButton
active={$status.currentScreen === screen.id}
onClick={() => setActiveScreen(screen.id)}
>
{screen.label} {screen.label}
</TabButton> </TabButton>
{/each} {/each}
</div> </div>
<div class="join flex justify-center"> <div class="join flex justify-center">
{#each $settings.actCurrencies as currency} {#each $settings.actCurrencies as currency (currency)}
<CurrencyButton <CurrencyButton
currency={currency} {currency}
active={$status.currency === currency} active={$status.currency === currency}
onClick={() => setActiveCurrency(currency)} onClick={() => setActiveCurrency(currency)}
/> />
@ -35,9 +38,7 @@
<div class="mt-8 flex justify-center"> <div class="mt-8 flex justify-center">
<div class="w-3/4"> <div class="w-3/4">
<!-- Bitcoin value display showing blocks/price -->
<BTClock displays={$status.data} verticalDesc={$settings.verticalDesc} /> <BTClock displays={$status.data} verticalDesc={$settings.verticalDesc} />
{$settings.verticalDesc}
</div> </div>
</div> </div>
@ -47,8 +48,8 @@
<small> <small>
{#if $status.dnd?.timeBasedEnabled} {#if $status.dnd?.timeBasedEnabled}
{m['section.status.timeBasedDnd']()} ( {$settings.dnd {m['section.status.timeBasedDnd']()} ( {$settings.dnd
.startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings .startHour}:{$settings.dnd.startMinute.toString().padStart(2, '0')} - {$settings.dnd
.dnd.endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} ) .endHour}:{$settings.dnd.endMinute.toString().padStart(2, '0')} )
{/if} {/if}
</small> </small>
</div> </div>
@ -58,17 +59,27 @@
<Status text="Nostr Relay connection" status={$status.nostr ? 'online' : 'offline'} /> <Status text="Nostr Relay connection" status={$status.nostr ? 'online' : 'offline'} />
{/if} {/if}
{#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE} {#if $settings.dataSource === DataSourceType.THIRD_PARTY_SOURCE}
<Status text={m['section.status.wsPriceConnection']()} status={$status.connectionStatus.price ? 'online' : 'offline'} /> <Status
<Status text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })} status={$status.connectionStatus.blocks ? 'online' : 'offline'} /> text={m['section.status.wsPriceConnection']()}
status={$status.connectionStatus.price ? 'online' : 'offline'}
/>
<Status
text={m['section.status.wsMempoolConnection']({ instance: $settings.mempoolInstance })}
status={$status.connectionStatus.blocks ? 'online' : 'offline'}
/>
{:else} {:else}
<Status text={m['section.status.wsDataConnection']()} status={$status.connectionStatus.V2 ? 'online' : 'offline'} /> <Status
text={m['section.status.wsDataConnection']()}
status={$status.connectionStatus.V2 ? 'online' : 'offline'}
/>
{/if} {/if}
</div> </div>
</div> </div>
<div class="flex justify-center stats shadow mt-4"> <div class="stats mt-4 flex justify-center shadow">
<Stat title={m['section.status.memoryFree']()} value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`} /> <Stat
title={m['section.status.memoryFree']()}
value={`${Math.round($status.espFreeHeap / 1024)} / ${Math.round($status.espHeapSize / 1024)} KiB`}
/>
<Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} /> <Stat title={m['section.status.wifiSignalStrength']()} value={`${$status.rssi} dBm`} />
<Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} /> <Stat title={m['section.status.uptime']()} value={`${toUptimestring($status.espUptime)}`} />
</div> </div>

View file

@ -2,10 +2,7 @@
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import { CardContainer } from '$lib/components'; import { CardContainer } from '$lib/components';
import { settings } from '$lib/stores'; import { settings } from '$lib/stores';
import { import { restartClock, forceFullRefresh } from '$lib/clockControl';
restartClock,
forceFullRefresh
} from '$lib/clockControl';
</script> </script>
<CardContainer title="System Information"> <CardContainer title="System Information">

View file

@ -1,12 +1,8 @@
<script lang="ts"> <script lang="ts">
let { let { title, className = '', ...restProps } = $props();
title,
className = "",
...restProps
} = $props();
</script> </script>
<div class="card bg-base-100 shadow-xl w-full {className}" {...restProps}> <div class="card bg-base-100 w-full shadow-xl {className}" {...restProps}>
<div class="card-body"> <div class="card-body">
{#if title} {#if title}
<h2 class="card-title">{title}</h2> <h2 class="card-title">{title}</h2>

View file

@ -1,12 +1,5 @@
<script lang="ts"> <script lang="ts">
let { let { title, value, desc = '', icon = '', className = '', ...restProps } = $props();
title,
value,
desc = "",
icon = "",
className = "",
...restProps
} = $props();
</script> </script>
<div class="stat {className}" {...restProps}> <div class="stat {className}" {...restProps}>

View file

@ -1,23 +1,23 @@
<script lang="ts"> <script lang="ts">
let { let {
status = "offline" as "online" | "offline" | "error" | "warning", status = 'offline' as 'online' | 'offline' | 'error' | 'warning',
text, text,
className = "", className = '',
...restProps ...restProps
} = $props(); } = $props();
const getStatusColor = () => { const getStatusColor = () => {
switch (status) { switch (status) {
case "online": case 'online':
return "bg-success"; return 'bg-success';
case "offline": case 'offline':
return "bg-base-300"; return 'bg-base-300';
case "error": case 'error':
return "bg-error"; return 'bg-error';
case "warning": case 'warning':
return "bg-warning"; return 'bg-warning';
default: default:
return "bg-base-300"; return 'bg-base-300';
} }
}; };
</script> </script>
@ -25,8 +25,10 @@
<div class="flex items-center gap-2 {className}" {...restProps}> <div class="flex items-center gap-2 {className}" {...restProps}>
<div class="relative flex"> <div class="relative flex">
<div class="{getStatusColor()} h-3 w-3 rounded-full"></div> <div class="{getStatusColor()} h-3 w-3 rounded-full"></div>
{#if status === "online"} {#if status === 'online'}
<div class="{getStatusColor()} animate-ping absolute inline-flex h-3 w-3 rounded-full opacity-75"></div> <div
class="{getStatusColor()} absolute inline-flex h-3 w-3 animate-ping rounded-full opacity-75"
></div>
{/if} {/if}
</div> </div>
<span class="text-sm">{text}</span> <span class="text-sm">{text}</span>

View file

@ -1,9 +1,5 @@
<script lang="ts"> <script lang="ts">
let { let { active = false, onClick, ...restProps } = $props();
active = false,
onClick,
...restProps
} = $props();
</script> </script>
<button <button

View file

@ -1,8 +1,14 @@
<script lang="ts"> <script lang="ts">
type Position = type Position =
| 'top-start' | 'top-center' | 'top-end' | 'top-start'
| 'middle-start' | 'middle-center' | 'middle-end' | 'top-center'
| 'bottom-start' | 'bottom-center' | 'bottom-end'; | 'top-end'
| 'middle-start'
| 'middle-center'
| 'middle-end'
| 'bottom-start'
| 'bottom-center'
| 'bottom-end';
type AlertType = 'info' | 'success' | 'warning' | 'error'; type AlertType = 'info' | 'success' | 'warning' | 'error';
@ -61,8 +67,17 @@
<span>{message}</span> <span>{message}</span>
{#if showClose} {#if showClose}
<button class="btn btn-circle btn-xs" onclick={closeToast}> <button class="btn btn-circle btn-xs" onclick={closeToast}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <svg
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /> xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg> </svg>
</button> </button>
{/if} {/if}

View file

@ -11,19 +11,19 @@ const defaultSettings: Settings = {
minSecPriceUpd: 30, minSecPriceUpd: 30,
fullRefreshMin: 60, fullRefreshMin: 60,
wpTimeout: 600, wpTimeout: 600,
tzString: "UTC", tzString: 'UTC',
dataSource: 0, dataSource: 0,
mempoolInstance: "mempool.space", mempoolInstance: 'mempool.space',
mempoolSecure: true, mempoolSecure: true,
localPoolEndpoint: "localhost:2019", localPoolEndpoint: 'localhost:2019',
nostrPubKey: "", nostrPubKey: '',
nostrRelay: "wss://relay.damus.io", nostrRelay: 'wss://relay.damus.io',
nostrZapNotify: false, nostrZapNotify: false,
nostrZapPubkey: "", nostrZapPubkey: '',
ledFlashOnZap: true, ledFlashOnZap: true,
fontName: "oswald", fontName: 'oswald',
availableFonts: ["antonio", "oswald"], availableFonts: ['antonio', 'oswald'],
customEndpoint: "ws-staging.btclock.dev", customEndpoint: 'ws-staging.btclock.dev',
customEndpointDisableSSL: false, customEndpointDisableSSL: false,
ledTestOnPower: true, ledTestOnPower: true,
ledFlashOnUpd: true, ledFlashOnUpd: true,
@ -40,30 +40,30 @@ const defaultSettings: Settings = {
verticalDesc: true, verticalDesc: true,
suffixShareDot: false, suffixShareDot: false,
enableDebugLog: false, enableDebugLog: false,
hostnamePrefix: "btclock", hostnamePrefix: 'btclock',
hostname: "btclock", hostname: 'btclock',
ip: "", ip: '',
txPower: 80, txPower: 80,
gitReleaseUrl: "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest", gitReleaseUrl: 'https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest',
bitaxeEnabled: false, bitaxeEnabled: false,
bitaxeHostname: "bitaxe1", bitaxeHostname: 'bitaxe1',
miningPoolStats: false, miningPoolStats: false,
miningPoolName: "noderunners", miningPoolName: 'noderunners',
miningPoolUser: "", miningPoolUser: '',
availablePools: [ availablePools: [
"ocean", 'ocean',
"noderunners", 'noderunners',
"satoshi_radio", 'satoshi_radio',
"braiins", 'braiins',
"public_pool", 'public_pool',
"local_public_pool", 'local_public_pool',
"gobrrr_pool", 'gobrrr_pool',
"ckpool", 'ckpool',
"eu_ckpool" 'eu_ckpool'
], ],
httpAuthEnabled: false, httpAuthEnabled: false,
httpAuthUser: "btclock", httpAuthUser: 'btclock',
httpAuthPass: "satoshi", httpAuthPass: 'satoshi',
hasFrontlight: false, hasFrontlight: false,
// Default frontlight settings // Default frontlight settings
flDisable: false, flDisable: false,
@ -76,24 +76,24 @@ const defaultSettings: Settings = {
hasLightLevel: false, hasLightLevel: false,
luxLightToggle: 128, luxLightToggle: 128,
flOffWhenDark: false, flOffWhenDark: false,
hwRev: "", hwRev: '',
fsRev: "", fsRev: '',
gitRev: "", gitRev: '',
gitTag: "", gitTag: '',
lastBuildTime: "", lastBuildTime: '',
screens: [ screens: [
{id: 0, name: "Block Height", enabled: true}, { id: 0, name: 'Block Height', enabled: true },
{id: 3, name: "Time", enabled: false}, { id: 3, name: 'Time', enabled: false },
{id: 4, name: "Halving countdown", enabled: false}, { id: 4, name: 'Halving countdown', enabled: false },
{id: 6, name: "Block Fee Rate", enabled: false}, { id: 6, name: 'Block Fee Rate', enabled: false },
{id: 10, name: "Sats per dollar", enabled: true}, { id: 10, name: 'Sats per dollar', enabled: true },
{id: 20, name: "Ticker", enabled: true}, { id: 20, name: 'Ticker', enabled: true },
{id: 30, name: "Market Cap", enabled: false} { id: 30, name: 'Market Cap', enabled: false }
], ],
actCurrencies: ["USD"], actCurrencies: ['USD'],
availableCurrencies: ["USD", "EUR", "GBP", "JPY", "AUD", "CAD"], availableCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD'],
poolLogosUrl: "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main", poolLogosUrl: 'https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main',
ceEndpoint: "ws-staging.btclock.dev", ceEndpoint: 'ws-staging.btclock.dev',
ceDisableSSL: false, ceDisableSSL: false,
dnd: { dnd: {
enabled: false, enabled: false,
@ -130,9 +130,9 @@ function createSettingsStore() {
const response = await fetch(`${baseUrl}/api/settings`, { const response = await fetch(`${baseUrl}/api/settings`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify(newSettings), body: JSON.stringify(newSettings)
}); });
if (!response.ok) { if (!response.ok) {
@ -140,7 +140,7 @@ function createSettingsStore() {
} }
// Update the local store with the new settings // Update the local store with the new settings
update(currentSettings => ({ ...currentSettings, ...newSettings })); update((currentSettings) => ({ ...currentSettings, ...newSettings }));
return true; return true;
} catch (error) { } catch (error) {

View file

@ -17,20 +17,20 @@ const defaultStatus: Status = {
nostr: false nostr: false
}, },
rssi: 0, rssi: 0,
currency: "USD", currency: 'USD',
dnd: { dnd: {
enabled: false, enabled: false,
timeBasedEnabled: false, timeBasedEnabled: false,
startTime: "00:00", startTime: '00:00',
endTime: "00:00", endTime: '00:00',
active: false active: false
}, },
data: [], data: [],
leds: [ leds: [
{red: 0, green: 0, blue: 0, hex: "#000000"}, { red: 0, green: 0, blue: 0, hex: '#000000' },
{red: 0, green: 0, blue: 0, hex: "#000000"}, { red: 0, green: 0, blue: 0, hex: '#000000' },
{red: 0, green: 0, blue: 0, hex: "#000000"}, { red: 0, green: 0, blue: 0, hex: '#000000' },
{red: 0, green: 0, blue: 0, hex: "#000000"} { red: 0, green: 0, blue: 0, hex: '#000000' }
] ]
}; };
@ -79,7 +79,7 @@ function createStatusStore() {
eventSource.addEventListener('status', (event) => { eventSource.addEventListener('status', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
update(currentStatus => ({ ...currentStatus, ...data })); update((currentStatus) => ({ ...currentStatus, ...data }));
} catch (error) { } catch (error) {
console.error('Error processing status event:', error); console.error('Error processing status event:', error);
} }
@ -89,7 +89,7 @@ function createStatusStore() {
eventSource.addEventListener('connection', (event) => { eventSource.addEventListener('connection', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
update(currentStatus => ({ update((currentStatus) => ({
...currentStatus, ...currentStatus,
connectionStatus: { connectionStatus: {
...currentStatus.connectionStatus, ...currentStatus.connectionStatus,
@ -105,7 +105,7 @@ function createStatusStore() {
eventSource.addEventListener('screen', (event) => { eventSource.addEventListener('screen', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
update(currentStatus => ({ update((currentStatus) => ({
...currentStatus, ...currentStatus,
currentScreen: data.screen || currentStatus.currentScreen currentScreen: data.screen || currentStatus.currentScreen
})); }));
@ -121,7 +121,7 @@ function createStatusStore() {
} }
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
update(currentStatus => ({ ...currentStatus, ...data })); update((currentStatus) => ({ ...currentStatus, ...data }));
} catch (error) { } catch (error) {
console.error('Error processing message event:', error); console.error('Error processing message event:', error);
} }
@ -151,7 +151,7 @@ function createStatusStore() {
} }
// Update the store with the new screen ID // Update the store with the new screen ID
update(currentStatus => ({ update((currentStatus) => ({
...currentStatus, ...currentStatus,
currentScreen: id currentScreen: id
})); }));

View file

@ -48,9 +48,9 @@ export interface Status {
hex: string; hex: string;
}>; }>;
[key: string]: unknown; [key: string]: unknown;
} }
// Define the Settings interface based on the API response structure // Define the Settings interface based on the API response structure
export interface Settings { export interface Settings {
numScreens: number; numScreens: number;
invertedColor: boolean; invertedColor: boolean;
@ -138,5 +138,4 @@ export interface Settings {
endMinute: number; endMinute: number;
}; };
[key: string]: unknown; [key: string]: unknown;
} }

View file

@ -25,7 +25,6 @@
status.stopListening(); status.stopListening();
} }
}); });
export const prerender = true;
</script> </script>
<Navbar /> <Navbar />

2
src/routes/+layout.ts Normal file
View file

@ -0,0 +1,2 @@
export const prerender = true;
export const ssr = false;

View file

@ -1,14 +1,9 @@
<script lang="ts"> <script lang="ts">
import { m } from '$lib/paraglide/messages'; import { ControlSection, StatusSection } from '$lib/components';
import { settings, status } from '$lib/stores';
import { onMount, onDestroy } from 'svelte';
import { ControlSection, StatusSection, SettingsSection } from '$lib/components';
</script> </script>
<div class=" mx-auto px-2 py-4"> <div class=" mx-auto px-2 py-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
<div> <div>
<ControlSection /> <ControlSection />
</div> </div>
@ -16,6 +11,5 @@
<div class="col-span-2"> <div class="col-span-2">
<StatusSection /> <StatusSection />
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,9 +6,9 @@
let scalarApiReference; let scalarApiReference;
function initializeScalar() { function initializeScalar() {
// @ts-ignore - Scalar is loaded dynamically // @ts-expect-error - Scalar is loaded dynamically
if (window.Scalar) { if (window.Scalar) {
// @ts-ignore - Scalar is loaded dynamically // @ts-expect-error - Scalar is loaded dynamically
scalarApiReference = window.Scalar.createApiReference('#app', { scalarApiReference = window.Scalar.createApiReference('#app', {
url: '/swagger.json', url: '/swagger.json',
hideDarkModeToggle: true, hideDarkModeToggle: true,
@ -56,7 +56,7 @@
mediaQuery.removeEventListener('change', handler); mediaQuery.removeEventListener('change', handler);
} }
if (isLoaded) { if (isLoaded) {
document.querySelectorAll('style[data-scalar]').forEach(el => el.remove()); document.querySelectorAll('style[data-scalar]').forEach((el) => el.remove());
scalarApiReference.destroy(); scalarApiReference.destroy();
} }

View file

@ -3,6 +3,6 @@
</script> </script>
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Settings</h1> <h1 class="mb-4 text-2xl font-bold">Settings</h1>
<SettingsSection /> <SettingsSection />
</div> </div>

View file

@ -3,6 +3,6 @@
</script> </script>
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">System Management</h1> <h1 class="mb-4 text-2xl font-bold">System Management</h1>
<SystemSection /> <SystemSection />
</div> </div>

View file

@ -3,11 +3,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { const config = {
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { adapter: adapter({ kit: {
fallback: 'index.html', adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'bundle.html',
precompress: false, precompress: false,
strict: true strict: true
}) } }),
appDir: 'build'
}
}; };
export default config; export default config;