feat!: Remove old frontend codes
10
frontend/.gitignore
vendored
|
@ -1,10 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
|
@ -1 +0,0 @@
|
|||
engine-strict=true
|
|
@ -1,4 +0,0 @@
|
|||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
# UI
|
||||
|
||||
The UI is generated and embedded when the Go project is built. See
|
||||
[./frontend/embed.go](https://github.com/ditatompel/xmr-remote-nodes/blob/main/frontend/embed.go#L10-L13).
|
||||
|
||||
> **NOTE**:
|
||||
>
|
||||
> Since this project will not using Svelte anymore, anything under this
|
||||
> directory will soon be removed.
|
|
@ -1,21 +0,0 @@
|
|||
package frontend
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:generate npm ci
|
||||
//go:generate npm run build
|
||||
//go:embed build/*
|
||||
var f embed.FS
|
||||
|
||||
func SvelteKitHandler() http.FileSystem {
|
||||
build, err := fs.Sub(f, "build")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return http.FS(build)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
|
||||
/** @type {import('eslint').Linter.FlatConfig[]} */
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||
}
|
||||
];
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
3908
frontend/package-lock.json
generated
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"name": "xmr-nodes-frontend",
|
||||
"version": "v0.1.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "VITE_API_URL=http://127.0.0.1:18901 vite dev --host 127.0.0.1",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@skeletonlabs/skeleton": "^2.10.3",
|
||||
"@skeletonlabs/tw-plugin": "^0.4.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.7.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@vincjo/datatables": "^1.14.10",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.46.0",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"svelte": "^5.1.3",
|
||||
"svelte-check": "^4.0.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.8"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
|
@ -1,25 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply h-full;
|
||||
}
|
||||
p {
|
||||
@apply mb-2;
|
||||
}
|
||||
.link {
|
||||
@apply text-primary-800 dark:text-primary-500 hover:brightness-110;
|
||||
}
|
||||
a.external {
|
||||
@apply link after:content-['_↗'];
|
||||
}
|
||||
.section-container {
|
||||
@apply mx-auto w-full max-w-7xl p-4;
|
||||
}
|
||||
.hero-gradient {
|
||||
background-image: radial-gradient(at 0% 0%, rgba(242, 104, 34, 0.4) 0px, transparent 50%),
|
||||
radial-gradient(at 98% 1%, rgba(var(--color-warning-900) / 0.33) 0px, transparent 50%);
|
||||
}
|
34
frontend/src/app.d.ts
vendored
|
@ -1,34 +0,0 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
interface ImportMetaEnv {
|
||||
VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface MoneroNode {
|
||||
id: number;
|
||||
hostname: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
protocol: string;
|
||||
is_tor: boolean;
|
||||
is_available: boolean;
|
||||
nettype: string;
|
||||
ip_addresses: string;
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
data: null | object | object[];
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
|
@ -1,13 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="false" data-theme="skeleton">
|
||||
<div style="display: contents" class="h-full">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,14 +0,0 @@
|
|||
<script>
|
||||
import { version } from '$app/environment';
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-end border-t border-surface-500/10 bg-surface-50 dark:bg-surface-900">
|
||||
<footer class="w-full">
|
||||
<div class="bg-surface-500/5">
|
||||
<div class="container mx-auto px-5 py-4">
|
||||
<!-- prettier-ignore -->
|
||||
<p class="text-center text-sm">XMR Nodes {version}, <a href="https://github.com/ditatompel/xmr-remote-nodes" target="_blank" rel="noopener" class="external">source code</a> licensed under <strong>GLWTPL</strong>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||
<!-- prettier-ignore -->
|
||||
<section id="site-news" class="mx-auto w-full max-w-4xl px-4 pb-7">
|
||||
<div class="alert variant-ghost-secondary shadow-xl">
|
||||
<div class="alert-message">
|
||||
<h2 class="h3 text-center">Quick Info</h2>
|
||||
<p>
|
||||
On November 7th, 2024, localmonero.co website will be taken down. If you have an account and a balance, withdraw your funds prior to this date. For more information, visit <a href="https://localmonero.co/blog/announcements/winding-down" class="external" target="_blank" rel="noopener">localmonero.co announcement page</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,58 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
export let handler: DataHandler<T>;
|
||||
|
||||
let intervalId: number | undefined;
|
||||
let intervalValue = 0;
|
||||
|
||||
const intervalOptions = [
|
||||
{ value: 0, label: 'No' },
|
||||
{ value: 5, label: '5s' },
|
||||
{ value: 10, label: '10s' },
|
||||
{ value: 30, label: '30s' },
|
||||
{ value: 60, label: '1m' }
|
||||
];
|
||||
|
||||
const startInterval = () => {
|
||||
const seconds = intervalValue;
|
||||
if (isNaN(seconds) || seconds < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!intervalOptions.some((option) => option.value === seconds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
||||
if (seconds > 0) {
|
||||
handler.invalidate();
|
||||
intervalId = setInterval(() => {
|
||||
handler.invalidate();
|
||||
}, seconds * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
$: startInterval();
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<label for="autoRefreshInterval">Auto Refresh:</label>
|
||||
<select
|
||||
class="select ml-2"
|
||||
id="autoRefreshInterval"
|
||||
bind:value={intervalValue}
|
||||
on:change={startInterval}
|
||||
>
|
||||
{#each intervalOptions as { value, label }}
|
||||
<option {value}>{label}</option>
|
||||
{/each}
|
||||
</select>
|
|
@ -1,66 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
|
||||
const pageNumber = handler.getPageNumber();
|
||||
const pageCount = handler.getPageCount();
|
||||
const pages = handler.getPages({ ellipsis: true });
|
||||
|
||||
const setPage = (value: 'previous' | 'next' | number) => {
|
||||
handler.setPage(value);
|
||||
handler.invalidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class={$$props.class ?? ''}>
|
||||
{#if $pages === undefined}
|
||||
<button type="button" class="sm-btn" on:click={() => setPage('previous')}> ❮ </button>
|
||||
<button class="mx-4">page <b>{$pageNumber}</b></button>
|
||||
<button type="button" class="sm-btn" on:click={() => setPage('next')}>❯</button>
|
||||
{:else}
|
||||
<div class="lg:hidden">
|
||||
<button type="button" class="sm-btn" on:click={() => setPage('previous')}> ❮ </button>
|
||||
<button class="mx-4">page <b>{$pageNumber}</b></button>
|
||||
<button
|
||||
class="sm-btn"
|
||||
class:disabled={$pageNumber === $pageCount}
|
||||
on:click={() => setPage('next')}
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group variant-ghost-surface hidden lg:block">
|
||||
<button
|
||||
type="button"
|
||||
class="hover:variant-soft-secondary"
|
||||
class:disabled={$pageNumber === 1}
|
||||
on:click={() => setPage('previous')}>❮</button
|
||||
>
|
||||
{#each $pages as page}<button
|
||||
type="button"
|
||||
class="hover:variant-filled-secondary"
|
||||
class:!variant-filled-primary={$pageNumber === page}
|
||||
class:ellipse={page === null}
|
||||
on:click={() => setPage(page)}>{page ?? '...'}</button
|
||||
>{/each}
|
||||
<button
|
||||
type="button"
|
||||
class="hover:variant-soft-secondary"
|
||||
class:disabled={$pageNumber === $pageCount}
|
||||
on:click={() => setPage('next')}
|
||||
>
|
||||
❯
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="postcss">
|
||||
.disabled {
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
</style>
|
|
@ -1,22 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
const rowCount = handler.getRowCount();
|
||||
</script>
|
||||
|
||||
{#if $rowCount === undefined}
|
||||
<div></div>
|
||||
{:else}
|
||||
<div class={$$props.class ?? 'mr-6 leading-8 lg:leading-10'}>
|
||||
{#if $rowCount.total > 0}
|
||||
<b>{$rowCount.start}</b>
|
||||
- <b>{$rowCount.end}</b>
|
||||
/ <b>{$rowCount.total}</b>
|
||||
{:else}
|
||||
No entries found
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
|
@ -1,33 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
export let options = [5, 10, 20, 50, 100];
|
||||
export let labelId = 'rowsPerPage';
|
||||
|
||||
const rowsPerPage = handler.getRowsPerPage();
|
||||
|
||||
const setRowsPerPage = () => {
|
||||
handler.setPage(1);
|
||||
handler.invalidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex place-items-center">
|
||||
<label for={labelId}>Show</label>
|
||||
<select
|
||||
class="select ml-2"
|
||||
id={labelId}
|
||||
name="rowsPerPage"
|
||||
bind:value={$rowsPerPage}
|
||||
on:change={setRowsPerPage}
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option}>
|
||||
{option}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,23 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler } from '@vincjo/datatables/remote';
|
||||
export let handler: DataHandler;
|
||||
let value: string;
|
||||
let timeout: any;
|
||||
|
||||
const search = () => {
|
||||
handler.search(value);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
handler.invalidate();
|
||||
}, 400);
|
||||
};
|
||||
</script>
|
||||
|
||||
<input
|
||||
class="input-variant-secondary input w-36 sm:w-64"
|
||||
type="search"
|
||||
name="tableGlobalSearch"
|
||||
placeholder="Search..."
|
||||
bind:value
|
||||
on:input={search}
|
||||
/>
|
|
@ -1,35 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
export let filterBy: keyof T;
|
||||
|
||||
/** @type {string} */
|
||||
export let placeholder: string = 'Filter';
|
||||
|
||||
/** @type {number} */
|
||||
export let colspan: number = 1;
|
||||
|
||||
let value: string = '';
|
||||
let timeout: any;
|
||||
|
||||
const filter = () => {
|
||||
handler.filter(value, filterBy);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
handler.invalidate();
|
||||
}, 400);
|
||||
};
|
||||
</script>
|
||||
|
||||
<th {colspan}>
|
||||
<input
|
||||
class="input variant-form-material h-8 w-full text-sm"
|
||||
type="text"
|
||||
{placeholder}
|
||||
bind:value
|
||||
on:input={filter}
|
||||
/>
|
||||
</th>
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { DataHandler, Row } from '@vincjo/datatables/remote';
|
||||
type T = $$Generic<Row>;
|
||||
|
||||
export let handler: DataHandler<T>;
|
||||
export let orderBy: keyof T;
|
||||
|
||||
const update = () => {
|
||||
handler.sort(orderBy);
|
||||
handler.invalidate();
|
||||
};
|
||||
</script>
|
||||
|
||||
<th on:click={update} class="cursor-pointer select-none p-2 px-5">
|
||||
<div class="flex h-full items-center justify-start gap-x-2">
|
||||
<slot /> ↕️
|
||||
</div>
|
||||
</th>
|
|
@ -1,7 +0,0 @@
|
|||
export { default as DtSrPagination } from './DtSrPagination.svelte';
|
||||
export { default as DtSrRowCount } from './DtSrRowCount.svelte';
|
||||
export { default as DtSrRowsPerPage } from './DtSrRowsPerPage.svelte';
|
||||
export { default as DtSrSearch } from './DtSrSearch.svelte';
|
||||
export { default as DtSrThFilter } from './DtSrThFilter.svelte';
|
||||
export { default as DtSrThSort } from './DtSrThSort.svelte';
|
||||
export { default as DtSrAutoRefresh } from './DtSrAutoRefresh.svelte';
|
|
@ -1,10 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let estimate_fee: number;
|
||||
export let majority_fee: number;
|
||||
</script>
|
||||
|
||||
{#if estimate_fee !== majority_fee}
|
||||
<span class="text-orange-800 dark:text-orange-300">{estimate_fee}<br />(CAUTION!)</span>
|
||||
{:else}
|
||||
{estimate_fee}
|
||||
{/if}
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { getDistinct } from '$lib/utils/arrays';
|
||||
|
||||
export let filterValue; //: Writable<string>;
|
||||
export let preFilteredValues; //: Readable<unknown[]>;
|
||||
$: uniqueValues = getDistinct($preFilteredValues);
|
||||
</script>
|
||||
|
||||
<div class="pt-2">
|
||||
<select name="filterAnonymity" class="select" bind:value={$filterValue} on:click|stopPropagation>
|
||||
<option value={undefined}>All</option>
|
||||
{#each uniqueValues as value}
|
||||
{#if value === true}
|
||||
<option {value}>TOR</option>
|
||||
{:else}
|
||||
<option {value}>CLEARNET</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||
<script>
|
||||
import { getDistinct } from '$lib/utils/arrays';
|
||||
|
||||
/** @type {string} */
|
||||
export let filterName;
|
||||
export let filterValue; //: Writable<string>;
|
||||
export let preFilteredValues; //: Readable<unknown[]>;
|
||||
$: uniqueValues = getDistinct($preFilteredValues);
|
||||
</script>
|
||||
|
||||
<div class="pt-2">
|
||||
<select name={filterName} class="select" bind:value={$filterValue} on:click|stopPropagation>
|
||||
<option value={undefined}>All</option>
|
||||
{#each uniqueValues as value}
|
||||
{#if value === ''}
|
||||
<option {value}>UNKNOWN</option>
|
||||
{:else}
|
||||
<option {value}>{value.toUpperCase()}</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { getDistinct } from '$lib/utils/arrays';
|
||||
|
||||
export let filterValue; //: Writable<string>;
|
||||
export let preFilteredValues; //: Readable<unknown[]>;
|
||||
$: uniqueValues = getDistinct($preFilteredValues);
|
||||
</script>
|
||||
|
||||
<div class="pt-2">
|
||||
<select name="filterStatus" class="select" bind:value={$filterValue} on:click|stopPropagation>
|
||||
<option value={undefined}>All</option>
|
||||
{#each uniqueValues as value}
|
||||
{#if value === true}
|
||||
<option {value}>ONLINE</option>
|
||||
{:else}
|
||||
<option {value}>OFFLINE</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
|
@ -1,20 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let is_available: boolean;
|
||||
export let statuses: number[];
|
||||
</script>
|
||||
|
||||
{#if is_available}
|
||||
<span class="font-semibold text-green-800 dark:text-green-500">Online</span>
|
||||
{:else}
|
||||
<span class="text-rose-800 dark:text-rose-400">Offline</span>
|
||||
{/if}
|
||||
<br />
|
||||
{#each statuses as status}
|
||||
{#if status === 1}
|
||||
<span class="text-success-700 dark:text-success-400 mr-1">•</span>
|
||||
{:else if status === 0}
|
||||
<span class="text-error-700 dark:text-error-400 mr-1">•</span>
|
||||
{:else}
|
||||
<span class="text-surface-400 dark:text-surface-600 mr-1">•</span>
|
||||
{/if}
|
||||
{/each}
|
|
@ -1,13 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let uptime: number;
|
||||
</script>
|
||||
|
||||
{#if uptime >= 98}
|
||||
<span class="text-green-800 dark:text-green-500">{uptime}%</span>
|
||||
{:else if uptime < 98 && uptime >= 80}
|
||||
<span class="text-sky-800 dark:text-sky-500">{uptime}%</span>
|
||||
{:else if uptime < 80 && uptime > 75}
|
||||
<span class="text-orange-800 dark:text-orange-300">{uptime}%</span>
|
||||
{:else}
|
||||
<span class="text-rose-800 dark:text-rose-400">{uptime}%</span>
|
||||
{/if}
|
|
@ -1,10 +0,0 @@
|
|||
export { default as CountryCellWithAsn } from './CountryCellWithAsn.svelte';
|
||||
export { default as EstimateFeeCell } from './EstimateFeeCell.svelte';
|
||||
export { default as HostPortCell } from './HostPortCell.svelte';
|
||||
export { default as NetTypeCell } from './NetTypeCell.svelte';
|
||||
export { default as ProtocolCell } from './ProtocolCell.svelte';
|
||||
export { default as SelectAnonymityFilter } from './SelectAnonymityFilter.svelte';
|
||||
export { default as SelectFilter } from './SelectFilter.svelte';
|
||||
export { default as SelectStatusFilter } from './SelectStatusFilter.svelte';
|
||||
export { default as StatusCell } from './StatusCell.svelte';
|
||||
export { default as UptimeCell } from './UptimeCell.svelte';
|
|
@ -1,21 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { adminNavs } from './navs';
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
$: style = (/** @type {string} */ href) =>
|
||||
$page.url.pathname.startsWith(href) ? 'bg-primary-500' : '';
|
||||
</script>
|
||||
|
||||
<nav class="list-nav p-4">
|
||||
<ul>
|
||||
{#each adminNavs as nav}
|
||||
<li>
|
||||
<a href={nav.path} class={style(nav.path)} on:click={() => drawerStore.close()}
|
||||
>{nav.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,72 +0,0 @@
|
|||
<script>
|
||||
import { invalidateAll, goto } from '$app/navigation';
|
||||
import { LightSwitch, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { apiUri } from '$lib/utils/common';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
/** @type {ApiResponse} */
|
||||
let formResult;
|
||||
|
||||
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||
async function handleLogout(event) {
|
||||
const data = new FormData(event.currentTarget);
|
||||
|
||||
const response = await fetch(event.currentTarget.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: JSON.stringify(Object.fromEntries(data))
|
||||
});
|
||||
|
||||
formResult = await response.json();
|
||||
|
||||
if (formResult.status === 'ok') {
|
||||
await invalidateAll();
|
||||
goto('/login/');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="bg-surface-100-800-token fixed top-0 z-30 w-full shadow-2xl">
|
||||
<div class="px-3 py-2 lg:px-5 lg:pl-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-start rtl:justify-end">
|
||||
<button
|
||||
class="btn btn-sm inline-flex items-center md:hidden"
|
||||
aria-label="Mobile Drawer Button"
|
||||
on:click={() => drawerStore.open({})}
|
||||
>
|
||||
<span>
|
||||
<svg viewBox="0 0 100 80" class="fill-token h-4 w-4">
|
||||
<rect width="100" height="20" />
|
||||
<rect y="30" width="100" height="20" />
|
||||
<rect y="60" width="100" height="20" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<a href="/app/prober/" class="ms-2 flex md:me-24" aria-label="title">
|
||||
<span class="hidden self-center whitespace-nowrap text-2xl font-semibold lg:block"
|
||||
>XMR Nodes</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="ms-3 flex items-center space-x-4">
|
||||
<LightSwitch />
|
||||
<form
|
||||
action={apiUri('/auth/logout')}
|
||||
method="POST"
|
||||
on:submit|preventDefault={handleLogout}
|
||||
>
|
||||
<input type="hidden" name="logout" value="logout" />
|
||||
<button type="submit" class="btn btn-sm variant-filled-error" role="menuitem">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,34 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { adminNavs } from './navs';
|
||||
</script>
|
||||
|
||||
<aside
|
||||
id="logo-sidebar"
|
||||
class="bg-surface-100-800-token fixed left-0 top-0 z-20 h-screen w-64 -translate-x-full pt-20 shadow-2xl transition-transform sm:translate-x-0"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<div class="h-full overflow-y-auto px-3 pb-4">
|
||||
<ul class="space-y-2 font-medium list-none" data-sveltekit-preload-data="false">
|
||||
{#each adminNavs as nav}
|
||||
<li>
|
||||
<a
|
||||
href={nav.path}
|
||||
class={$page.url.pathname.startsWith(nav.path) ? 'active' : 'nav-link'}
|
||||
>
|
||||
<span class="ms-3">{nav.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style lang="postcss">
|
||||
.active {
|
||||
@apply flex items-center rounded-lg bg-primary-500 p-2;
|
||||
}
|
||||
.nav-link {
|
||||
@apply flex items-center rounded-lg p-2 hover:bg-secondary-500 hover:text-white;
|
||||
}
|
||||
</style>
|
|
@ -1,63 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { navs } from './navs';
|
||||
import { LightSwitch, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
</script>
|
||||
|
||||
<nav class="fixed w-full z-20 top-0 start-0 bg-surface-100-800-token shadow-2xl">
|
||||
<div class="mx-auto flex max-w-screen-xl flex-wrap items-center justify-between px-4 py-1">
|
||||
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse" aria-label="xmr nodes">
|
||||
<span class="self-center whitespace-nowrap text-2xl font-semibold lg:block">XMR Nodes</span>
|
||||
</a>
|
||||
<div class="flex items-center space-x-1 md:order-2 md:space-x-0 rtl:space-x-reverse">
|
||||
<LightSwitch />
|
||||
<button
|
||||
class="btn btn-sm mr-4 md:hidden"
|
||||
aria-label="Mobile Drawer Button"
|
||||
on:click={() => drawerStore.open({})}
|
||||
>
|
||||
<span>
|
||||
<svg viewBox="0 0 100 80" class="fill-token h-4 w-4">
|
||||
<rect width="100" height="20" />
|
||||
<rect y="30" width="100" height="20" />
|
||||
<rect y="60" width="100" height="20" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hidden w-full items-center justify-between md:order-1 md:flex md:w-auto">
|
||||
<ul
|
||||
class="flex flex-row space-x-1 rounded-lg bg-white p-0 dark:bg-gray-900 rtl:space-x-reverse"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
class={$page.url.pathname === '/' ? 'active' : 'nav-link'}
|
||||
aria-current={$page.url.pathname === '/' ? 'page' : undefined}>Home</a
|
||||
>
|
||||
</li>
|
||||
{#each navs as nav}
|
||||
<li>
|
||||
<a
|
||||
href={nav.path}
|
||||
class={$page.url.pathname.startsWith(nav.path) ? 'active' : 'nav-link'}
|
||||
>
|
||||
{nav.name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="postcss">
|
||||
.active {
|
||||
@apply block rounded bg-primary-500 p-2 text-black;
|
||||
}
|
||||
.nav-link {
|
||||
@apply block rounded hover:bg-secondary-500 md:p-2 hover:text-white;
|
||||
}
|
||||
</style>
|
|
@ -1,28 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { navs } from './navs';
|
||||
import { getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
$: classes = (/** @type {string} */ href) =>
|
||||
$page.url.pathname.startsWith(href) ? 'bg-primary-500' : '';
|
||||
</script>
|
||||
|
||||
<nav class="list-nav p-4">
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/"
|
||||
class={$page.url.pathname === '/' ? 'bg-primary-500' : ''}
|
||||
on:click={() => drawerStore.close()}>Home</a
|
||||
>
|
||||
</li>
|
||||
{#each navs as nav}
|
||||
<li>
|
||||
<a href={nav.path} class={classes(nav.path)} on:click={() => drawerStore.close()}
|
||||
>{nav.name}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,5 +0,0 @@
|
|||
export { default as MainNav } from './MainNav.svelte';
|
||||
export { default as MobileDrawer } from './MobileDrawer.svelte';
|
||||
export { default as AdminNav } from './AdminNav.svelte';
|
||||
export { default as AdminSidebar } from './AdminSidebar.svelte';
|
||||
export { default as AdminMobileDrawer } from './AdminMobileDrawer.svelte';
|
|
@ -1,9 +0,0 @@
|
|||
export const adminNavs = [
|
||||
{ name: 'Prober', path: '/app/prober/' },
|
||||
{ name: 'Crons', path: '/app/crons/' }
|
||||
];
|
||||
|
||||
export const navs = [
|
||||
{ name: 'Remote Nodes', path: '/remote-nodes/' },
|
||||
{ name: 'Add Node', path: '/add-node/' }
|
||||
];
|
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
d="M512 256C512 114.6 397.4 0 256 0S0 114.6 0 256C0 376 82.7 476.8 194.2 504.5V334.2H141.4V256h52.8V222.3c0-87.1 39.4-127.5 125-127.5c16.2 0 44.2 3.2 55.7 6.4V172c-6-.6-16.5-1-29.6-1c-42 0-58.2 15.9-58.2 57.2V256h83.6l-14.4 78.2H287V510.1C413.8 494.8 512 386.9 512 256h0z"
|
||||
/></svg
|
||||
>
|
Before Width: | Height: | Size: 582 B |
|
@ -1,10 +0,0 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
||||
></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 880 B |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 496 512"
|
||||
><path
|
||||
d="M352 384h108.4C417 455.9 338.1 504 248 504S79 455.9 35.6 384H144V256.2L248 361l104-105v128zM88 336V128l159.4 159.4L408 128v208h74.8c8.5-25.1 13.2-52 13.2-80C496 119 385 8 248 8S0 119 0 256c0 28 4.6 54.9 13.2 80H88z"
|
||||
/></svg
|
||||
>
|
Before Width: | Height: | Size: 528 B |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
d="M373 138.6c-25.2 0-46.3-17.5-51.9-41l0 0c-30.6 4.3-54.2 30.7-54.2 62.4l0 .2c47.4 1.8 90.6 15.1 124.9 36.3c12.6-9.7 28.4-15.5 45.5-15.5c41.3 0 74.7 33.4 74.7 74.7c0 29.8-17.4 55.5-42.7 67.5c-2.4 86.8-97 156.6-213.2 156.6S45.5 410.1 43 323.4C17.6 311.5 0 285.7 0 255.7c0-41.3 33.4-74.7 74.7-74.7c17.2 0 33 5.8 45.7 15.6c34-21.1 76.8-34.4 123.7-36.4l0-.3c0-44.3 33.7-80.9 76.8-85.5C325.8 50.2 347.2 32 373 32c29.4 0 53.3 23.9 53.3 53.3s-23.9 53.3-53.3 53.3zM157.5 255.3c-20.9 0-38.9 20.8-40.2 47.9s17.1 38.1 38 38.1s36.6-9.8 37.8-36.9s-14.7-49.1-35.7-49.1zM395 303.1c-1.2-27.1-19.2-47.9-40.2-47.9s-36.9 22-35.7 49.1c1.2 27.1 16.9 36.9 37.8 36.9s39.3-11 38-38.1zm-60.1 70.8c1.5-3.6-1-7.7-4.9-8.1c-23-2.3-47.9-3.6-73.8-3.6s-50.8 1.3-73.8 3.6c-3.9 .4-6.4 4.5-4.9 8.1c12.9 30.8 43.3 52.4 78.7 52.4s65.8-21.6 78.7-52.4z"
|
||||
/></svg
|
||||
>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 496 512"
|
||||
><path
|
||||
d="M248 8C111 8 0 119 0 256S111 504 248 504 496 393 496 256 385 8 248 8zM363 176.7c-3.7 39.2-19.9 134.4-28.1 178.3-3.5 18.6-10.3 24.8-16.9 25.4-14.4 1.3-25.3-9.5-39.3-18.7-21.8-14.3-34.2-23.2-55.3-37.2-24.5-16.1-8.6-25 5.3-39.5 3.7-3.8 67.1-61.5 68.3-66.7 .2-.7 .3-3.1-1.2-4.4s-3.6-.8-5.1-.5q-3.3 .7-104.6 69.1-14.8 10.2-26.9 9.9c-8.9-.2-25.9-5-38.6-9.1-15.5-5-27.9-7.7-26.8-16.3q.8-6.7 18.5-13.7 108.4-47.2 144.6-62.3c68.9-28.6 83.2-33.6 92.5-33.8 2.1 0 6.6 .5 9.6 2.9a10.5 10.5 0 0 1 3.5 6.7A43.8 43.8 0 0 1 363 176.7z"
|
||||
/></svg
|
||||
>
|
Before Width: | Height: | Size: 831 B |
|
@ -1,10 +0,0 @@
|
|||
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={`${$$props.fill ?? 'currentColor'}`}
|
||||
class={`${$$props.class}`}
|
||||
viewBox="0 0 512 512"
|
||||
><path
|
||||
d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"
|
||||
/></svg
|
||||
>
|
Before Width: | Height: | Size: 470 B |
|
@ -1,6 +0,0 @@
|
|||
export { default as IcnGitHub } from './IcnGitHub.svelte';
|
||||
export { default as IcnMonero } from './IcnMonero.svelte';
|
||||
export { default as IcnReddit } from './IcnReddit.svelte';
|
||||
export { default as IcnTwitter } from './IcnTwitter.svelte';
|
||||
export { default as IcnFacebook } from './IcnFacebook.svelte';
|
||||
export { default as IcnTelegram } from './IcnTelegram.svelte';
|
|
@ -1 +0,0 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
|
@ -1,17 +0,0 @@
|
|||
export const getDistinct = (items) => {
|
||||
return Array.from(getCounter(items).keys());
|
||||
};
|
||||
|
||||
export const getDuplicates = (items) => {
|
||||
return Array.from(getCounter(items).entries())
|
||||
.filter(([, count]) => count !== 1)
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
export const getCounter = (items) => {
|
||||
const result = new Map();
|
||||
items.forEach((item) => {
|
||||
result.set(item, (result.get(item) ?? 0) + 1);
|
||||
});
|
||||
return result;
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
/** @param {string} path */
|
||||
export const apiUri = (path) => {
|
||||
return `${import.meta.env.VITE_API_URL || ''}${path}`;
|
||||
};
|
|
@ -1,76 +0,0 @@
|
|||
/**
|
||||
* Modifies the input string based on whether it is an IPv6 address.
|
||||
* If the input is an IPv6 address, it wraps it in square brackets `[ ]`.
|
||||
* Otherwise, it returns the input string as-is (for domain names or
|
||||
* IPv4 addresses). AND I'M SORRY USING REGEX FOR THIS!
|
||||
*
|
||||
* @param {string} hostname
|
||||
* @returns {string} - The modified string, IPv6 addresses wrapped in `[ ]`.
|
||||
*/
|
||||
export const formatHostname = (hostname) => {
|
||||
// const ipv6Pattern = /^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/; // full
|
||||
// pattern for both full and compressed IPv6 addresses.
|
||||
// source: https://regex101.com/library/cP9mH9?filterFlavors=dotnet&filterFlavors=javascript&orderBy=RELEVANCE&search=ip
|
||||
// This may be incorrect, but let's assume it's correct. xD
|
||||
const ipv6Pattern =
|
||||
/^(([0-9A-Fa-f]{1,4}:){7})([0-9A-Fa-f]{1,4})$|(([0-9A-Fa-f]{1,4}:){1,6}:)(([0-9A-Fa-f]{1,4}:){0,4})([0-9A-Fa-f]{1,4})$/;
|
||||
if (ipv6Pattern.test(hostname)) {
|
||||
return `[${hostname}]`;
|
||||
}
|
||||
|
||||
return hostname;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} bytes
|
||||
* @param {number} decimals
|
||||
* @returns {string}
|
||||
*/
|
||||
export const formatBytes = (bytes, decimals = 2) => {
|
||||
if (!+bytes) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a number with a maximum precision.
|
||||
*
|
||||
* This function was copied from jtgrassie/monero-pool project.
|
||||
* Source: https://github.com/jtgrassie/monero-pool/blob/master/src/webui-embed.html
|
||||
*
|
||||
* Copyright (c) 2018, The Monero Project
|
||||
*
|
||||
* @param {number} n
|
||||
* @param {number} p
|
||||
*/
|
||||
const maxPrecision = (n, p) => {
|
||||
return parseFloat(n.toFixed(p));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a hash value (h) into human readable format.
|
||||
*
|
||||
* This function was copied from jtgrassie/monero-pool project.
|
||||
* Source: https://github.com/jtgrassie/monero-pool/blob/master/src/webui-embed.html
|
||||
*
|
||||
* Copyright (c) 2018, The Monero Project
|
||||
*
|
||||
* @param {number} h
|
||||
*/
|
||||
export const formatHashes = (h) => {
|
||||
if (h < 1e-12) return '0 H';
|
||||
else if (h < 1e-9) return maxPrecision(h * 1e12, 0) + ' pH';
|
||||
else if (h < 1e-6) return maxPrecision(h * 1e9, 0) + ' nH';
|
||||
else if (h < 1e-3) return maxPrecision(h * 1e6, 0) + ' μH';
|
||||
else if (h < 1) return maxPrecision(h * 1e3, 0) + ' mH';
|
||||
else if (h < 1e3) return h + ' H';
|
||||
else if (h < 1e6) return maxPrecision(h * 1e-3, 2) + ' KH';
|
||||
else if (h < 1e9) return maxPrecision(h * 1e-6, 2) + ' MH';
|
||||
else return maxPrecision(h * 1e-9, 2) + ' GH';
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
|
@ -1,85 +0,0 @@
|
|||
<script>
|
||||
import '../app.css';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
Toast,
|
||||
Modal,
|
||||
Drawer,
|
||||
ProgressBar,
|
||||
initializeStores,
|
||||
storePopup
|
||||
} from '@skeletonlabs/skeleton';
|
||||
import { beforeNavigate, afterNavigate } from '$app/navigation';
|
||||
import { computePosition, autoUpdate, offset, shift, flip, arrow } from '@floating-ui/dom';
|
||||
import { MainNav, MobileDrawer } from '$lib/components/navigation';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
|
||||
initializeStores();
|
||||
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
|
||||
let isLoading = false;
|
||||
|
||||
beforeNavigate(() => (isLoading = true));
|
||||
afterNavigate(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
/* prettier-ignore */
|
||||
const meta = {
|
||||
title: 'Monero Remote Node',
|
||||
description: 'A website that helps you monitor your favourite Monero remote nodes, a device on the internet running the Monero software with copy of the Monero blockchain.',
|
||||
keywords: 'monero,monero,xmr,monero node,xmrnode,cryptocurrency,monero remote node,monero testnet,monero stagenet'
|
||||
};
|
||||
|
||||
page.subscribe((page) => {
|
||||
if (typeof page.data.meta === 'object') {
|
||||
meta.title = page.data.meta.title ?? meta.title;
|
||||
meta.description = page.data.meta.description ?? meta.description;
|
||||
meta.keywords = page.data.meta.keywords ?? meta.description;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{meta.title} — xmr.ditatompel.com</title>
|
||||
<!-- Meta Tags -->
|
||||
<meta name="title" content="{meta.title} — xmr.ditatompel.com" />
|
||||
<meta name="description" content={meta.description} />
|
||||
<meta name="keywords" content={meta.keywords} />
|
||||
<meta name="theme-color" content="#272b31" />
|
||||
<meta name="author" content="ditatompel" />
|
||||
|
||||
<!-- Open Graph - https://ogp.me/ -->
|
||||
<meta property="og:site_name" content="xmr.ditatompel.com" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://xmr.ditatompel.com{$page.url.pathname}" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:title" content="{meta.title} — xmr.ditatompel.com" />
|
||||
<meta property="og:description" content={meta.description} />
|
||||
</svelte:head>
|
||||
|
||||
<Modal />
|
||||
<Toast />
|
||||
|
||||
{#if isLoading}
|
||||
<ProgressBar
|
||||
class="fixed top-0 z-50"
|
||||
height="h-1"
|
||||
track="bg-opacity-100"
|
||||
meter="bg-gradient-to-br from-purple-600 via-pink-600 to-blue-600"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Drawer>
|
||||
<h2 class="p-4">Navigation</h2>
|
||||
<hr />
|
||||
<MobileDrawer />
|
||||
<hr />
|
||||
</Drawer>
|
||||
|
||||
<MainNav />
|
||||
|
||||
<div class="pt-10 md:pt-12 min-h-screen">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<Footer />
|
|
@ -1,37 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
return {
|
||||
meta: {
|
||||
title: 'Monero Remote Node',
|
||||
description:
|
||||
'A website that helps you monitor your favourite Monero remote nodes, but YOU BETTER RUN AND USE YOUR OWN NODE.',
|
||||
keywords:
|
||||
'monero,monero,xmr,monero node,xmrnode,cryptocurrency,monero remote node,monero testnet,monero stagenet'
|
||||
},
|
||||
links: [
|
||||
{ text: 'moneroworld.com', uri: 'https://moneroworld.com' },
|
||||
{ text: 'monero.how', uri: 'https://www.monero.how' },
|
||||
{ text: 'monero.observer', uri: 'https://www.monero.observer' },
|
||||
{ text: 'revuo-xmr.com', uri: 'https://revuo-xmr.com' },
|
||||
{ text: 'themonoeromoon.com', uri: 'https://www.themoneromoon.com' },
|
||||
{ text: 'monerotopia.com', uri: 'https://monerotopia.com' },
|
||||
{ text: 'sethforprivacy.com', uri: 'https://sethforprivacy.com' }
|
||||
],
|
||||
stagenet: [
|
||||
{ label: 'P2P', value: 'stagenet.xmr.ditatompel.com:38080', key: 'snetP2P' },
|
||||
{ label: 'RPC', value: 'stagenet.xmr.ditatompel.com:38089', key: 'snetRPC' },
|
||||
{ label: 'RPC SSL', value: 'stagenet.xmr.ditatompel.com:443', key: 'snetSSL' }
|
||||
],
|
||||
testnet: [
|
||||
{ label: 'P2P', value: 'testnet.xmr.ditatompel.com:28080', key: 'tnetP2P' },
|
||||
{ label: 'RPC', value: 'testnet.xmr.ditatompel.com:28089', key: 'tnetRPC' },
|
||||
{ label: 'RPC SSL', value: 'testnet.xmr.ditatompel.com:443', key: 'tnetSSL' }
|
||||
],
|
||||
donation: {
|
||||
// You change donation address and qr image below if you run your own "instance"
|
||||
address:
|
||||
'8BWYe6GzbNKbxe3D8mPkfFMQA2rViaZJFhWShhZTjJCNG6EZHkXRZCKHiuKmwwe4DXDYF8KKcbGkvNYaiRG3sNt7JhnVp7D',
|
||||
qr: '/img/monerotip.png'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
<script>
|
||||
import { clipboard } from '@skeletonlabs/skeleton';
|
||||
import {
|
||||
IcnGitHub,
|
||||
IcnMonero,
|
||||
IcnReddit,
|
||||
IcnTwitter,
|
||||
IcnFacebook,
|
||||
IcnTelegram
|
||||
} from '$lib/components/svg';
|
||||
import News from '$lib/components/News.svelte';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
let donationCopied = false;
|
||||
|
||||
/** @param {Event & { target: HTMLInputElement }} e */
|
||||
function copyHandler(e) {
|
||||
e.target.disabled = true;
|
||||
e.target.innerText = 'Copied 👍';
|
||||
setTimeout(() => {
|
||||
e.target.innerText = 'Copy';
|
||||
e.target.disabled = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function copyDonationAddr() {
|
||||
donationCopied = true;
|
||||
setTimeout(() => {
|
||||
donationCopied = false;
|
||||
}, 2500);
|
||||
}
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
<p>{data.meta.description}</p>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<div class="pt-2">
|
||||
<a href="https://www.getmonero.org" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnMonero class="h-4 w-4" /></span>
|
||||
<span>GetMonero.org</span>
|
||||
</a>
|
||||
<a href="https://github.com/monero-project" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnGitHub fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>monero-project</span>
|
||||
</a>
|
||||
<a href="https://www.reddit.com/r/Monero/" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnReddit fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>r/Monero</span>
|
||||
</a>
|
||||
<a href="https://twitter.com/monero" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnTwitter fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>@monero</span>
|
||||
</a>
|
||||
<a href="https://www.facebook.com/monerocurrency/" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnFacebook fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>monerocurrency</span>
|
||||
</a>
|
||||
<a href="https://telegram.me/monero" class="variant-ghost chip mt-2 hover:variant-filled" target="_blank" rel="noopener">
|
||||
<span><IcnTelegram fill="currentColor" class="h-4 w-4" /></span>
|
||||
<span>monero</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="introduction">
|
||||
<div class="section-container text-center">
|
||||
<p>If you're new to Monero, the official links above is a perfect place to start.</p>
|
||||
<p class="py-2">
|
||||
Of course, there are lots of personal and community sites which generally discusses a lot
|
||||
about Monero, such as
|
||||
{#each data.links as link}
|
||||
<a href={link.uri} class="external" target="_blank" rel="noopener">{link.text}</a>,
|
||||
{/each} etc; can be an other good reference for you.
|
||||
</p>
|
||||
<p>You can find few resources I provide related to Monero below:</p>
|
||||
</div>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<div class="section-container text-token grid grid-cols-1 gap-2 md:grid-cols-3">
|
||||
<a class="card card-hover overflow-hidden py-2 text-center" href="/remote-nodes/">
|
||||
<h2 class="h2 font-bold">Remote Nodes</h2>
|
||||
<div class="space-y-4 p-4">
|
||||
<p>List of submitted Monero remote nodes you can use when you <strong>cannot</strong> run your own node.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-hover overflow-hidden py-2 text-center" href="/add-node/">
|
||||
<h2 class="h2 font-bold">Add Node</h2>
|
||||
<div class="space-y-4 p-4">
|
||||
<p>Add your Monero public node to be monitored and see how it performs.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card card-hover overflow-hidden py-2 text-center" href="https://monitor.ditatompel.com/d/xmr_metrics/monero-metrics?orgId=2" target="_blank" rel="noopener" >
|
||||
<h2 class="h2 font-bold">Metrics</h2>
|
||||
<div class="space-y-4 p-4">
|
||||
<p>Collection of my Monero metrics (GitHub repository, blockchain, market, P2Pool) presented through Grafana. ↗</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<News />
|
||||
|
||||
<section id="my-monero-public-nodes" class="bg-surface-100-800-token">
|
||||
<div class="section-container text-token grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div class="text-center">
|
||||
<h2 class="h2 pb-2 font-bold">My Stagenet Public Node</h2>
|
||||
<p>
|
||||
Stagenet is what you need to learn Monero safely. Stagenet is technically equivalent to
|
||||
mainnet, both in terms of features and consensus rules.
|
||||
</p>
|
||||
{#each data.stagenet as { label, value, key }}
|
||||
<div class="input-group input-group-divider my-2 grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim"><label for={key}>{label}</label></div>
|
||||
<input class="text-center" type="text" id={key} name={key} {value} data-clipboard={key} />
|
||||
<button
|
||||
class="variant-filled-secondary"
|
||||
use:clipboard={{ input: key }}
|
||||
on:click={copyHandler}>Copy</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<h2 class="h2 pb-2 font-bold">My Testnet Public Node</h2>
|
||||
<p>
|
||||
Testnet is the <em>"experimental"</em> network and blockchain where things get released long
|
||||
before mainnet. As a normal user, use mainnet instead.
|
||||
</p>
|
||||
{#each data.testnet as { label, value, key }}
|
||||
<div class="input-group input-group-divider my-2 grid-cols-[auto_1fr_auto]">
|
||||
<div class="input-group-shim"><label for={key}>{label}</label></div>
|
||||
<input class="text-center" type="text" id={key} name={key} {value} data-clipboard={key} />
|
||||
<button
|
||||
class="variant-filled-secondary"
|
||||
use:clipboard={{ input: key }}
|
||||
on:click={copyHandler}>Copy</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="privacy-quote">
|
||||
<div class="text-token mx-auto w-full max-w-4xl py-4 text-center">
|
||||
<!-- prettier-ignore -->
|
||||
<blockquote class="blockquote">
|
||||
<p class="text-3xl">
|
||||
Since we desire privacy, we must ensure that each party to a transaction have knowledge only of that which is directly necessary for that transaction.
|
||||
</p>
|
||||
<p class="my-2">
|
||||
<strong>Eric Hughes</strong> in <a href="https://www.activism.net/cypherpunk/manifesto.html" class="external" target="_blank" rel="noopener"><cite title="Source Title">A Cypherpunk's Manifesto</cite></a>.
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="monero-donation" class="section-container text-token text-center">
|
||||
<div class="mx-auto flex w-full max-w-4xl flex-row items-center gap-10">
|
||||
<div class="md:basis-3/4">
|
||||
<label for="donate">If you like to buy me a coffee, here is my Monero address:</label>
|
||||
<textarea class="textarea my-2" id="donate" name="donate" data-clipboard="donate" readonly
|
||||
>{data.donation.address}</textarea
|
||||
>
|
||||
<button
|
||||
class="variant-filled-success btn"
|
||||
use:clipboard={{ input: 'donate' }}
|
||||
disabled={donationCopied}
|
||||
on:click={copyDonationAddr}
|
||||
>{donationCopied ? 'Donation Address Copied! 🤩' : 'Copy Donation Address'}</button
|
||||
>
|
||||
</div>
|
||||
<div class="md:basis-1/4">
|
||||
<img src={data.donation.qr} alt="ditatompel's monero address" />
|
||||
<p>Thank you so much! It means a lot to me. 🥰</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,12 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
/* prettier-ignore */
|
||||
return {
|
||||
meta: {
|
||||
title: 'Add Monero Node',
|
||||
description:
|
||||
'You can use this page to add known remote node to the system so my bots can monitor it.',
|
||||
keywords: 'monero,monero node,monero public node,monero wallet,list monero node,monero node monitoring'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,129 +0,0 @@
|
|||
<script>
|
||||
import { invalidateAll, goto } from '$app/navigation';
|
||||
import { apiUri } from '$lib/utils/common';
|
||||
import { ProgressBar } from '@skeletonlabs/skeleton';
|
||||
|
||||
/** @type {import('./$types').PageData} */
|
||||
export let data;
|
||||
/** @type {ApiResponse} */
|
||||
export let formResult;
|
||||
|
||||
let isProcessing = false;
|
||||
|
||||
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
|
||||
async function handleSubmit(event) {
|
||||
isProcessing = true;
|
||||
const data = new FormData(event.currentTarget);
|
||||
|
||||
const response = await fetch(event.currentTarget.action, {
|
||||
method: 'POST',
|
||||
body: data
|
||||
});
|
||||
|
||||
formResult = await response.json();
|
||||
isProcessing = false;
|
||||
|
||||
if (formResult.status === 'ok') {
|
||||
await invalidateAll();
|
||||
goto('/remote-nodes');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
<p>{data.meta.description}</p>
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="page-info" class="mx-auto w-full max-w-4xl px-4 pb-7">
|
||||
<div class="alert card shadow-xl">
|
||||
<div class="alert-message">
|
||||
<h2 class="h3 text-center">Important Note</h2>
|
||||
<ul class="list-inside list-disc">
|
||||
<li>
|
||||
As an administrator of this instance, I have full rights to delete, and blacklist any
|
||||
submitted node with or without providing any reason.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="form-add-monero-node">
|
||||
<div class="section-container text-center">
|
||||
<p>Enter your Monero node information below (IPv6 host check is experimental):</p>
|
||||
|
||||
<form
|
||||
class="mx-auto w-full max-w-3xl py-2"
|
||||
action={apiUri('/api/v1/nodes')}
|
||||
method="POST"
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-4 py-6 md:grid-cols-4">
|
||||
<label class="label">
|
||||
<span>Protocol *</span>
|
||||
<select name="protocol" class="select variant-form-material" disabled={isProcessing}>
|
||||
<option value="http">HTTP or TOR</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="label md:col-span-2">
|
||||
<span>Host / IP *</span>
|
||||
<input
|
||||
class="input variant-form-material"
|
||||
name="hostname"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Eg: node.example.com or 172.16.17.18"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>Port *</span>
|
||||
<input
|
||||
class="input variant-form-material"
|
||||
name="port"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Eg: 18081"
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button class="variant-filled-success btn" disabled={isProcessing}
|
||||
>{isProcessing ? 'Processing...' : 'Submit'}</button
|
||||
>
|
||||
</form>
|
||||
|
||||
<div class="mx-auto w-full max-w-3xl py-2">
|
||||
{#if !isProcessing}
|
||||
{#if formResult?.status === 'error'}
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-red-400" role="alert">
|
||||
<span class="font-medium">Error:</span>
|
||||
{formResult.message}!
|
||||
</div>
|
||||
{/if}
|
||||
{#if formResult?.status === 'ok'}
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-green-400" role="alert">
|
||||
<span class="font-medium">Success:</span>
|
||||
{formResult.message}!
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ProgressBar meter="bg-secondary-500" track="bg-secondary-500/30" value={undefined} />
|
||||
<div class="mx-4 p-4 mb-4 text-sm rounded-lg bg-gray-700 text-blue-400" role="alert">
|
||||
<span class="font-medium">Processing...</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Here you can find list of <a class="anchor" href="/remote-nodes/">Monero Remote Node</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
|
@ -1,11 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
return {
|
||||
// prettier-ignore
|
||||
meta: {
|
||||
title: 'Public Monero Remote Nodes List',
|
||||
description: "Although it's possible to use these existing public Monero nodes, you're MUST RUN AND USE YOUR OWN NODE!",
|
||||
keywords: 'monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,343 +0,0 @@
|
|||
<script>
|
||||
import { DataHandler } from '@vincjo/datatables/remote';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import { loadData, loadFees, loadCountries } from './api-handler';
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
DtSrRowsPerPage,
|
||||
DtSrThSort,
|
||||
DtSrThFilter,
|
||||
DtSrRowCount,
|
||||
DtSrPagination,
|
||||
DtSrAutoRefresh
|
||||
} from '$lib/components/datatables/server';
|
||||
import {
|
||||
HostPortCell,
|
||||
NetTypeCell,
|
||||
ProtocolCell,
|
||||
CountryCellWithAsn,
|
||||
StatusCell,
|
||||
UptimeCell,
|
||||
EstimateFeeCell
|
||||
} from '$lib/components/datatables/xmr';
|
||||
import News from '$lib/components/News.svelte';
|
||||
|
||||
export let data;
|
||||
let filterNettype = 'any';
|
||||
let filterProtocol = 'any';
|
||||
let filterCc = 'any';
|
||||
let filterStatus = -1;
|
||||
let checkboxCors = false;
|
||||
|
||||
/** @type {{total_nodes: number, cc: string, name: string}[]} */
|
||||
let countries = [];
|
||||
let fees = [];
|
||||
|
||||
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||
let rows = handler.getRows();
|
||||
|
||||
/** @type {Object.<string, number>} */
|
||||
let majorityFee;
|
||||
|
||||
onMount(() => {
|
||||
loadFees().then((data) => {
|
||||
fees = data;
|
||||
majorityFee = fees.reduce(
|
||||
/**
|
||||
* @param {Object.<string, number>} o
|
||||
* @param {{ nettype: string, estimate_fee: number }} key
|
||||
* @returns {Object.<string, number>}
|
||||
*/
|
||||
(o, key) => ({
|
||||
...o,
|
||||
[key.nettype]: key.estimate_fee
|
||||
}),
|
||||
{}
|
||||
);
|
||||
handler.onChange((state) => loadData(state));
|
||||
handler.invalidate();
|
||||
});
|
||||
loadCountries().then((data) => {
|
||||
countries = data;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
<!-- prettier-ignore -->
|
||||
<p class="mx-auto max-w-3xl">
|
||||
<strong>Monero remote node</strong> is a device on the internet running the Monero software with full copy of the Monero blockchain that doesn't run on the same local machine where the Monero wallet is located.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<section id="introduction">
|
||||
<div class="section-container text-center !max-w-4xl">
|
||||
<p>Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.</p>
|
||||
<p>Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the <strong>Monero community suggests to <span class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<News />
|
||||
|
||||
<section id="monero-remote-node">
|
||||
<div class="section-container">
|
||||
<div class="space-y-2 overflow-x-auto">
|
||||
<div class="flex justify-between">
|
||||
<DtSrRowsPerPage {handler} />
|
||||
<div class="invisible flex place-items-center md:visible">
|
||||
<DtSrAutoRefresh {handler} />
|
||||
</div>
|
||||
<div class="flex place-items-center">
|
||||
<button
|
||||
id="reloadDt"
|
||||
name="reloadDt"
|
||||
class="variant-filled-primary btn"
|
||||
on:click={() => handler.invalidate()}>Reload</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-compact w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host:Port</th>
|
||||
<th>Nettype</th>
|
||||
<th>Protocol</th>
|
||||
<th>Country</th>
|
||||
<th>Status</th>
|
||||
<th>Est. Fee</th>
|
||||
<DtSrThSort {handler} orderBy="uptime">Uptime</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="last_checked">Check</DtSrThSort>
|
||||
</tr>
|
||||
<tr>
|
||||
<DtSrThFilter {handler} filterBy="host" placeholder="Filter Host / IP" />
|
||||
<th>
|
||||
<select
|
||||
id="nettype"
|
||||
name="nettype"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterNettype}
|
||||
on:change={() => {
|
||||
handler.filter(filterNettype, 'nettype');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value="any">Any</option>
|
||||
<option value="mainnet">MAINNET</option>
|
||||
<option value="stagenet">STAGENET</option>
|
||||
<option value="testnet">TESTNET</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<select
|
||||
id="protocol"
|
||||
name="protocol"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterProtocol}
|
||||
on:change={() => {
|
||||
handler.filter(filterProtocol, 'protocol');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value="any">Any</option>
|
||||
<option value="tor">TOR</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</th>
|
||||
<th>
|
||||
<select
|
||||
id="cc"
|
||||
name="cc"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterCc}
|
||||
on:change={() => {
|
||||
handler.filter(filterCc, 'cc');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value="any">Any</option>
|
||||
{#each countries as country}
|
||||
{#if country.cc === ''}
|
||||
<option value="UNKNOWN">UNKNOWN ({country.total_nodes})</option>
|
||||
{:else}
|
||||
<option value={country.cc}
|
||||
>{country.name === '' ? country.cc : country.name} ({country.total_nodes})</option
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2">
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterStatus}
|
||||
on:change={() => {
|
||||
handler.filter(filterStatus, 'status');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value={-1}>Any</option>
|
||||
<option value="0">Offline</option>
|
||||
<option value="1">Online</option>
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2">
|
||||
<label for="cors" class="flex items-center justify-center space-x-2">
|
||||
<input
|
||||
id="cors"
|
||||
name="cors"
|
||||
class="checkbox"
|
||||
type="checkbox"
|
||||
bind:checked={checkboxCors}
|
||||
on:change={() => {
|
||||
handler.filter(checkboxCors === true ? 1 : -1, 'cors');
|
||||
handler.invalidate();
|
||||
}}
|
||||
/>
|
||||
<p>CORS</p>
|
||||
</label>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rows as row (row.id)}
|
||||
<tr>
|
||||
<td
|
||||
><HostPortCell
|
||||
ip_addresses={row.ip_addresses}
|
||||
is_tor={row.is_tor}
|
||||
hostname={row.hostname}
|
||||
port={row.port}
|
||||
ipv6_only={row.ipv6_only}
|
||||
/>
|
||||
</td>
|
||||
<td><NetTypeCell nettype={row.nettype} height={row.height} /></td>
|
||||
<td><ProtocolCell protocol={row.protocol} cors={row.cors} /></td>
|
||||
<td
|
||||
><CountryCellWithAsn
|
||||
cc={row.cc}
|
||||
country_name={row.country_name}
|
||||
city={row.city}
|
||||
asn={row.asn}
|
||||
asn_name={row.asn_name}
|
||||
/></td
|
||||
>
|
||||
<td
|
||||
><StatusCell
|
||||
is_available={row.is_available}
|
||||
statuses={row.last_check_statuses}
|
||||
/></td
|
||||
>
|
||||
<td>
|
||||
<EstimateFeeCell
|
||||
estimate_fee={row.estimate_fee}
|
||||
majority_fee={majorityFee[row.nettype]}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
><UptimeCell uptime={row.uptime} /><br />
|
||||
<a
|
||||
class="anchor !text-purple-800 dark:!text-purple-400"
|
||||
href="/remote-nodes/logs/?node_id={row.id}">[Logs]</a
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
{format(row.last_checked * 1000, 'PP HH:mm')}<br />
|
||||
{formatDistance(row.last_checked * 1000, new Date(), { addSuffix: true })}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<DtSrRowCount {handler} />
|
||||
<DtSrPagination {handler} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="page-info" class="mx-auto w-full max-w-4xl px-4 pb-7">
|
||||
<div class="alert card shadow-xl">
|
||||
<div class="alert-message">
|
||||
<h2 class="h3">Info</h2>
|
||||
<ul class="list-inside list-disc">
|
||||
<li>
|
||||
If you find any remote nodes that are strange or suspicious, please <a
|
||||
class="external"
|
||||
href="https://github.com/ditatompel/xmr-remote-nodes/issues"
|
||||
target="_blank"
|
||||
rel="noopener">open an issue on GitHub</a
|
||||
> for removal.
|
||||
</li>
|
||||
<li>
|
||||
Uptime percentage calculated is the <strong>last 1 month</strong> uptime.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Est. Fee</strong> here is just fee estimation / byte from
|
||||
<code class="code text-rose-900 font-bold">get_fee_estimate</code> RPC call method.
|
||||
</li>
|
||||
<li>
|
||||
Malicious actors who running remote nodes <a
|
||||
class="link"
|
||||
href="/img/node-tx-fee.jpg"
|
||||
rel="noopener">still can return high fee only if you about to create a transactions</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
<strong
|
||||
class="font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500"
|
||||
>The best and safest way is running your own node!</strong
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
Nodes with 0% uptime within 1 month with more than 300 check attempt will be removed. You
|
||||
can always add your node again latter.
|
||||
</li>
|
||||
<li>
|
||||
You can filter remote node by selecting on <strong>nettype</strong>,
|
||||
<strong>protocol</strong>, <strong>country</strong>,
|
||||
<strong>tor</strong>, and <strong>online status</strong> option.
|
||||
</li>
|
||||
<li>
|
||||
If you know one or more remote node that we don't currently monitor, please add them using <a
|
||||
href="/add-node">this form</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
I deliberately cut the long Tor addresses, click the <span
|
||||
class="text-orange-800 dark:text-orange-300">👁 torhostname...</span
|
||||
> to see the full Tor address.
|
||||
</li>
|
||||
<li>
|
||||
You can found larger remote nodes database from <a
|
||||
class="external"
|
||||
href="https://monero.fail/"
|
||||
role="button"
|
||||
target="_blank"
|
||||
rel="noopener">monero.fail</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
If you are developer or power user who like to fetch Monero remote node above in JSON
|
||||
format, you can read <a
|
||||
class="external"
|
||||
href="https://insights.ditatompel.com/en/blog/2022/01/public-api-monero-remote-node-list/"
|
||||
>Public API Monero Remote Node List</a
|
||||
> blog post for more detailed information.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,37 +0,0 @@
|
|||
import { apiUri } from '$lib/utils/common';
|
||||
|
||||
/**
|
||||
* @typedef {import('@vincjo/datatables/remote').State} State
|
||||
* @param {State} state - The state object from the data table.
|
||||
*/
|
||||
export const loadData = async (state) => {
|
||||
const response = await fetch(apiUri(`/api/v1/nodes?${getParams(state)}`));
|
||||
const json = await response.json();
|
||||
state.setTotalRows(json.data.total_rows ?? 0);
|
||||
return json.data.items ?? [];
|
||||
};
|
||||
|
||||
export const loadCountries = async () => {
|
||||
const response = await fetch(apiUri('/api/v1/countries'));
|
||||
const json = await response.json();
|
||||
return json.data ?? [];
|
||||
};
|
||||
|
||||
export const loadFees = async () => {
|
||||
const response = await fetch(apiUri('/api/v1/fees'));
|
||||
const json = await response.json();
|
||||
return json.data ?? [];
|
||||
};
|
||||
|
||||
/** @param {State} state - The state object from the data table. */
|
||||
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||
|
||||
if (sort) {
|
||||
params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
|
||||
}
|
||||
if (filters) {
|
||||
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||
}
|
||||
return params;
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {
|
||||
/* prettier-ignore */
|
||||
return {
|
||||
meta: {
|
||||
title: 'Probe Logs',
|
||||
description: 'Monero RPC response frpm monitored remote nodes',
|
||||
keywords: 'monero log,monero node log,monitoring monero log,monero,xmr,monero node,xmrnode,cryptocurrency'
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
<script>
|
||||
import { DataHandler } from '@vincjo/datatables/remote';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import { loadData, loadNodeInfo } from './api-handler';
|
||||
import { onMount } from 'svelte';
|
||||
import { formatHostname, formatHashes, formatBytes } from '$lib/utils/strings';
|
||||
import {
|
||||
DtSrRowsPerPage,
|
||||
DtSrThSort,
|
||||
DtSrThFilter,
|
||||
DtSrRowCount,
|
||||
DtSrPagination,
|
||||
DtSrAutoRefresh
|
||||
} from '$lib/components/datatables/server';
|
||||
|
||||
/** @param {number | null } runtime */
|
||||
function parseRuntime(runtime) {
|
||||
return runtime === null ? '' : runtime.toLocaleString(undefined) + 's';
|
||||
}
|
||||
|
||||
export let data;
|
||||
|
||||
let pageId = '0';
|
||||
let filterStatus = -1;
|
||||
|
||||
/** @type {MoneroNode | null} */
|
||||
let nodeInfo;
|
||||
|
||||
const handler = new DataHandler([], { rowsPerPage: 10, totalRows: 0 });
|
||||
let rows = handler.getRows();
|
||||
|
||||
onMount(() => {
|
||||
pageId = new URLSearchParams(window.location.search).get('node_id') || '0';
|
||||
loadNodeInfo(pageId).then((data) => {
|
||||
nodeInfo = data;
|
||||
});
|
||||
handler.filter(pageId, 'node_id');
|
||||
handler.onChange((state) => loadData(state));
|
||||
handler.invalidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<header id="hero" class="hero-gradient py-7">
|
||||
<div class="card text-token mx-auto flex w-fit justify-center p-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="crumb"><a class="link underline" href="/remote-nodes">Remote Nodes</a></li>
|
||||
<li class="crumb-separator" aria-hidden="true">/</li>
|
||||
<li>Logs</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section-container text-center">
|
||||
<h1 class="h1 pb-2 font-extrabold">{data.meta.title}</h1>
|
||||
</div>
|
||||
<div class="mx-auto w-full max-w-3xl px-20">
|
||||
<hr class="!border-primary-400-500-token !border-t-4 !border-double" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if nodeInfo === undefined}
|
||||
<div class="section-container mx-auto w-full max-w-3xl text-center">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else if nodeInfo === null}
|
||||
<div class="section-container mx-auto w-full max-w-3xl text-center">
|
||||
<p>Node ID does not exist</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="section-container">
|
||||
<div class="table-container mx-auto w-full max-w-3xl">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-bold">Hostname:Port</td>
|
||||
<td>{formatHostname(nodeInfo?.hostname)}:{nodeInfo?.port}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Public IP</td>
|
||||
<td>{nodeInfo?.ip_addresses.replace(/,/g, ', ')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="font-bold">Net Type</td>
|
||||
<td>{nodeInfo?.nettype.toUpperCase()}</td>
|
||||
</tr></tbody
|
||||
>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="node-logs">
|
||||
<div class="section-container">
|
||||
<div class="space-y-2 overflow-x-auto">
|
||||
<div class="flex justify-between">
|
||||
<DtSrRowsPerPage {handler} />
|
||||
<div class="invisible flex place-items-center md:visible">
|
||||
<DtSrAutoRefresh {handler} />
|
||||
</div>
|
||||
<div class="flex place-items-center">
|
||||
<button
|
||||
id="reloadDt"
|
||||
name="reloadDt"
|
||||
class="variant-filled-primary btn"
|
||||
on:click={() => handler.invalidate()}>Reload</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover table-compact w-full table-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#ID</th>
|
||||
<th>Prober ID</th>
|
||||
<th><label for="status">Status</label></th>
|
||||
<th>Height</th>
|
||||
<th>Adjusted Time</th>
|
||||
<th>DB Size</th>
|
||||
<th>Difficulty</th>
|
||||
<DtSrThSort {handler} orderBy="estimate_fee">Est. Fee</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="date_checked">Date Checked</DtSrThSort>
|
||||
<DtSrThSort {handler} orderBy="fetch_runtime">Runtime</DtSrThSort>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="3">
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
class="select variant-form-material"
|
||||
bind:value={filterStatus}
|
||||
on:change={() => {
|
||||
handler.filter(filterStatus, 'status');
|
||||
handler.invalidate();
|
||||
}}
|
||||
>
|
||||
<option value={-1}>Any</option>
|
||||
<option value="1">Online</option>
|
||||
<option value="0">Offline</option>
|
||||
</select>
|
||||
</th>
|
||||
<DtSrThFilter
|
||||
{handler}
|
||||
filterBy="failed_reason"
|
||||
placeholder="Filter reason"
|
||||
colspan={7}
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $rows as row (row.id)}
|
||||
<tr>
|
||||
<td>{row.id}</td>
|
||||
<td>{row.prober_id}</td>
|
||||
<td>{row.status === 1 ? 'OK' : 'ERR'}</td>
|
||||
{#if row.status !== 1}
|
||||
<td colspan="5">{row.failed_reason ?? ''}</td>
|
||||
{:else}
|
||||
<td class="text-right">{row.height.toLocaleString(undefined)}</td>
|
||||
<td>{format(row.adjusted_time * 1000, 'yyyy-MM-dd HH:mm')}</td>
|
||||
<td class="text-right">{formatBytes(row.database_size, 2)}</td>
|
||||
<td class="text-right">{formatHashes(row.difficulty)}</td>
|
||||
<td class="text-right">{row.estimate_fee.toLocaleString(undefined)}</td>
|
||||
{/if}
|
||||
<td>
|
||||
{format(row.date_checked * 1000, 'PP HH:mm')}<br />
|
||||
{formatDistance(row.date_checked * 1000, new Date(), { addSuffix: true })}
|
||||
</td>
|
||||
<td class="text-right">{parseRuntime(row.fetch_runtime)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="flex justify-between mb-2">
|
||||
<DtSrRowCount {handler} />
|
||||
<DtSrPagination {handler} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
|
@ -1,32 +0,0 @@
|
|||
import { apiUri } from '$lib/utils/common';
|
||||
|
||||
/**
|
||||
* @typedef {import('@vincjo/datatables/remote').State} State
|
||||
* @param {State} state - The state object from the data table.
|
||||
*/
|
||||
export const loadData = async (state) => {
|
||||
const response = await fetch(apiUri(`/api/v1/nodes/logs?${getParams(state)}`));
|
||||
const json = await response.json();
|
||||
state.setTotalRows(json.data.total_rows ?? 0);
|
||||
return json.data.items ?? [];
|
||||
};
|
||||
|
||||
/** @param {string} nodeId */
|
||||
export const loadNodeInfo = async (nodeId) => {
|
||||
const response = await fetch(apiUri(`/api/v1/nodes/id/${nodeId}`));
|
||||
const json = await response.json();
|
||||
return json.data;
|
||||
};
|
||||
|
||||
/** @param {State} state - The state object from the data table. */
|
||||
const getParams = ({ pageNumber, rowsPerPage, sort, filters }) => {
|
||||
let params = `page=${pageNumber}&limit=${rowsPerPage}`;
|
||||
|
||||
if (sort) {
|
||||
params += `&sort_by=${sort.orderBy}&sort_direction=${sort.direction}`;
|
||||
}
|
||||
if (filters) {
|
||||
params += filters.map(({ filterBy, value }) => `&${filterBy}=${value}`).join('');
|
||||
}
|
||||
return params;
|
||||
};
|
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 19 KiB |
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"name": "xmr.ditatompel.com",
|
||||
"short_name": "xmr-remote-nodes",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#272b31",
|
||||
"theme_color": "#272b31",
|
||||
"description": "Monero Remote Nodes",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/img/icon/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "4.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/android-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any",
|
||||
"density": "4.0"
|
||||
},
|
||||
{
|
||||
"src": "/img/icon/maskable-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
User-agent: *
|
||||
Allow: /
|
|
@ -1,58 +0,0 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import * as child_process from 'node:child_process';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// Helper function to execute shell commands
|
||||
function execSync(cmd) {
|
||||
return child_process.execSync(cmd).toString().trim();
|
||||
}
|
||||
|
||||
// Read version from package.json
|
||||
const packageJsonPath = join(process.cwd(), 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||
const VERSION = packageJson.version;
|
||||
|
||||
// Retrieve current branch
|
||||
const BRANCH = execSync('git rev-parse --abbrev-ref HEAD');
|
||||
|
||||
// Retrieve current tag if it exists
|
||||
const RELEASE_TAG = execSync('git tag -l --points-at HEAD');
|
||||
|
||||
// Generate version suffix
|
||||
const commitCount = execSync('git rev-list --count HEAD');
|
||||
const shortCommitHash = execSync('git show --no-patch --no-notes --pretty="%h" HEAD');
|
||||
const VERSION_SUFFIX = `-beta.${commitCount}.${shortCommitHash}`;
|
||||
|
||||
// Determine branch-specific values
|
||||
let TAG_BRANCH = `.${BRANCH}`;
|
||||
|
||||
if (BRANCH === 'HEAD' || BRANCH === 'main') {
|
||||
TAG_BRANCH = '';
|
||||
}
|
||||
|
||||
// Determine final tag
|
||||
let TAG = `${VERSION}${VERSION_SUFFIX}${TAG_BRANCH}`;
|
||||
if (RELEASE_TAG) {
|
||||
TAG = RELEASE_TAG;
|
||||
}
|
||||
|
||||
console.log('Building with tag', TAG);
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
version: {
|
||||
name: TAG
|
||||
},
|
||||
// paths: {
|
||||
// base: '/'
|
||||
// },
|
||||
// trailingSlash: 'always',
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,23 +0,0 @@
|
|||
import { join } from 'path';
|
||||
import { skeleton } from '@skeletonlabs/tw-plugin';
|
||||
import forms from '@tailwindcss/forms';
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
|
||||
],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: [
|
||||
forms,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: ['skeleton']
|
||||
}
|
||||
})
|
||||
]
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
// @ts-check
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |