mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 04:42:24 +00:00
feat: add hosted dashboard configuration and distribution plan
Added a settings modal to the dashboard (index.html) to allow configuration of hashtag, namespace, and relays via URL query parameters and localStorage. This enables the dashboard to be hosted as a static site and used by any project. Also added `docs/architecture/distribution-plan.md` outlining the strategy for NPM, Binary, and Hosted Dashboard distribution. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
1d8af21365
commit
268248b4eb
5 files changed
+266
-25
No files matched your search
+178
-25
@@ -76,6 +76,13 @@
|
|||||||
|
|
||||||
<div class="flex items-center gap-2 ml-auto">
|
<div class="flex items-center gap-2 ml-auto">
|
||||||
<span id="eventCount" class="text-xs text-muted mono">0 events</span>
|
<span id="eventCount" class="text-xs text-muted mono">0 events</span>
|
||||||
|
<button
|
||||||
|
id="settingsBtn"
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-surface border border-border px-3 py-1.5 text-sm font-medium text-text hover:bg-surface-alt focus:outline-none focus:ring-2 focus:ring-accent transition-colors"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
id="refreshBtn"
|
id="refreshBtn"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -164,6 +171,65 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Modal -->
|
||||||
|
<div id="settingsModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close-modal" id="closeSettings">×</span>
|
||||||
|
<h2 class="text-xl font-semibold mb-4 text-text-strong">Dashboard Settings</h2>
|
||||||
|
|
||||||
|
<form id="settingsForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="settingHashtag" class="block text-sm font-medium text-text mb-1">Hashtag (Required)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="settingHashtag"
|
||||||
|
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-text focus:outline-none focus:ring-2 focus:ring-accent"
|
||||||
|
placeholder="e.g. my-project-lock"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<p class="text-xs text-muted mt-1">Unique identifier for this project's locks.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="settingNamespace" class="block text-sm font-medium text-text mb-1">Namespace</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="settingNamespace"
|
||||||
|
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-text focus:outline-none focus:ring-2 focus:ring-accent"
|
||||||
|
placeholder="torch"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-muted mt-1">Default: torch</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="settingRelays" class="block text-sm font-medium text-text mb-1">Relays (Comma-separated)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="settingRelays"
|
||||||
|
class="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-text focus:outline-none focus:ring-2 focus:ring-accent"
|
||||||
|
placeholder="wss://relay.damus.io, wss://nos.lol"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-4 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="cancelSettings"
|
||||||
|
class="rounded-md border border-border bg-surface px-4 py-2 text-sm font-medium text-text hover:bg-surface-alt"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-md bg-accent px-4 py-2 text-sm font-medium text-white hover:bg-accent-strong"
|
||||||
|
>
|
||||||
|
Save & Reload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { KIND_APP_DATA } from '../src/constants.mjs';
|
import { KIND_APP_DATA } from '../src/constants.mjs';
|
||||||
import { createElement, escapeHtml } from './domUtils.js';
|
import { createElement, escapeHtml } from './domUtils.js';
|
||||||
@@ -171,31 +237,95 @@
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Modal Logic
|
// Modal Logic
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
const modal = document.getElementById("docsModal");
|
const docsModal = document.getElementById("docsModal");
|
||||||
const btn = document.getElementById("openDocs");
|
const settingsModal = document.getElementById("settingsModal");
|
||||||
const span = document.getElementsByClassName("close-modal")[0];
|
const openDocsBtn = document.getElementById("openDocs");
|
||||||
|
const openSettingsBtn = document.getElementById("settingsBtn");
|
||||||
|
const closeDocsBtn = document.getElementsByClassName("close-modal")[0];
|
||||||
|
const closeSettingsBtn = document.getElementById("closeSettings");
|
||||||
const contentDiv = document.getElementById("markdown-content");
|
const contentDiv = document.getElementById("markdown-content");
|
||||||
let docsLoaded = false;
|
let docsLoaded = false;
|
||||||
|
|
||||||
if (btn) {
|
// Docs Modal
|
||||||
btn.onclick = function(e) {
|
if (openDocsBtn) {
|
||||||
|
openDocsBtn.onclick = function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
modal.classList.add("show");
|
docsModal.classList.add("show");
|
||||||
if (!docsLoaded) {
|
if (!docsLoaded) {
|
||||||
fetchDocs();
|
fetchDocs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (span) {
|
if (closeDocsBtn) {
|
||||||
span.onclick = function() {
|
closeDocsBtn.onclick = function() {
|
||||||
modal.classList.remove("show");
|
docsModal.classList.remove("show");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings Modal
|
||||||
|
if (openSettingsBtn) {
|
||||||
|
openSettingsBtn.onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Pre-fill values
|
||||||
|
document.getElementById('settingHashtag').value = HASHTAG;
|
||||||
|
document.getElementById('settingNamespace').value = DASHBOARD_CONFIG.namespace;
|
||||||
|
document.getElementById('settingRelays').value = RELAYS.join(', ');
|
||||||
|
|
||||||
|
settingsModal.classList.add("show");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeSettingsBtn) {
|
||||||
|
closeSettingsBtn.onclick = function() {
|
||||||
|
settingsModal.classList.remove("show");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('cancelSettings').onclick = function() {
|
||||||
|
settingsModal.classList.remove("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Settings Save
|
||||||
|
document.getElementById('settingsForm').onsubmit = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const newHashtag = document.getElementById('settingHashtag').value.trim();
|
||||||
|
const newNamespace = document.getElementById('settingNamespace').value.trim();
|
||||||
|
const newRelays = document.getElementById('settingRelays').value.split(',').map(r => r.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
if (!newHashtag) {
|
||||||
|
alert('Hashtag is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefs = {
|
||||||
|
hashtag: newHashtag,
|
||||||
|
namespace: newNamespace,
|
||||||
|
relays: newRelays
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save to LocalStorage
|
||||||
|
localStorage.setItem('torch_dashboard_prefs', JSON.stringify(prefs));
|
||||||
|
|
||||||
|
// Update URL to reflect new settings (so link is shareable)
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set('hashtag', newHashtag);
|
||||||
|
url.searchParams.set('namespace', newNamespace);
|
||||||
|
url.searchParams.set('relays', newRelays.join(','));
|
||||||
|
window.history.pushState({}, '', url);
|
||||||
|
|
||||||
|
// Reload to apply
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
if (event.target == modal) {
|
if (event.target == docsModal) {
|
||||||
modal.classList.remove("show");
|
docsModal.classList.remove("show");
|
||||||
|
}
|
||||||
|
if (event.target == settingsModal) {
|
||||||
|
settingsModal.classList.remove("show");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,23 +393,46 @@
|
|||||||
|
|
||||||
function parseDashboardConfig(torchConfig = {}) {
|
function parseDashboardConfig(torchConfig = {}) {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// 1. LocalStorage Preferences
|
||||||
|
let localPrefs = {};
|
||||||
|
try {
|
||||||
|
localPrefs = JSON.parse(localStorage.getItem('torch_dashboard_prefs')) || {};
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const dashboardConfig = torchConfig.dashboard || {};
|
const dashboardConfig = torchConfig.dashboard || {};
|
||||||
const lockConfig = torchConfig.nostrLock || {};
|
const lockConfig = torchConfig.nostrLock || {};
|
||||||
|
|
||||||
const configuredNamespace =
|
// Hierarchy: URL Param > LocalStorage > Config File > Default
|
||||||
(dashboardConfig.namespace || lockConfig.namespace || DASHBOARD_DEFAULTS.namespace);
|
|
||||||
const namespace =
|
// Namespace
|
||||||
(params.get('namespace') || configuredNamespace).trim() || DASHBOARD_DEFAULTS.namespace;
|
const configNamespace = dashboardConfig.namespace || lockConfig.namespace || DASHBOARD_DEFAULTS.namespace;
|
||||||
const hashtag =
|
const namespace = (
|
||||||
(params.get('hashtag') || dashboardConfig.hashtag || DASHBOARD_DEFAULTS.hashtag).trim() ||
|
params.get('namespace') ||
|
||||||
`${namespace}-agent-lock`;
|
localPrefs.namespace ||
|
||||||
const relaysParam = (params.get('relays') || '').trim();
|
configNamespace
|
||||||
const relays = relaysParam
|
).trim() || DASHBOARD_DEFAULTS.namespace;
|
||||||
? relaysParam
|
|
||||||
.split(',')
|
// Hashtag
|
||||||
.map((relay) => relay.trim())
|
const configHashtag = dashboardConfig.hashtag || DASHBOARD_DEFAULTS.hashtag;
|
||||||
.filter(Boolean)
|
const hashtag = (
|
||||||
: [...(dashboardConfig.relays || lockConfig.relays || DASHBOARD_DEFAULTS.relays)];
|
params.get('hashtag') ||
|
||||||
|
localPrefs.hashtag ||
|
||||||
|
configHashtag ||
|
||||||
|
`${namespace}-agent-lock`
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
// Relays
|
||||||
|
const paramsRelays = params.get('relays') ? params.get('relays').split(',') : null;
|
||||||
|
const localRelays = localPrefs.relays;
|
||||||
|
const configRelays = dashboardConfig.relays || lockConfig.relays || DASHBOARD_DEFAULTS.relays;
|
||||||
|
|
||||||
|
let relays = (paramsRelays || localRelays || configRelays)
|
||||||
|
.map(r => r.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (relays.length === 0) relays = DASHBOARD_DEFAULTS.relays;
|
||||||
|
|
||||||
const defaultCadenceView = normalizeCadence(dashboardConfig.defaultCadenceView);
|
const defaultCadenceView = normalizeCadence(dashboardConfig.defaultCadenceView);
|
||||||
const defaultStatusView = normalizeStatus(dashboardConfig.defaultStatusView);
|
const defaultStatusView = normalizeStatus(dashboardConfig.defaultStatusView);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
> [email protected] dashboard:serve
|
||||||
|
> node bin/torch-lock.mjs dashboard
|
||||||
|
|
||||||
|
node:internal/modules/package_json_reader:314
|
||||||
|
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
|
||||||
|
^
|
||||||
|
|
||||||
|
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'nostr-tools' imported from /app/src/lib.mjs
|
||||||
|
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:314:9)
|
||||||
|
at packageResolve (node:internal/modules/esm/resolve:767:81)
|
||||||
|
at moduleResolve (node:internal/modules/esm/resolve:853:18)
|
||||||
|
at defaultResolve (node:internal/modules/esm/resolve:983:11)
|
||||||
|
at #cachedDefaultResolve (node:internal/modules/esm/loader:731:20)
|
||||||
|
at ModuleLoader.resolve (node:internal/modules/esm/loader:708:38)
|
||||||
|
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:310:38)
|
||||||
|
at ModuleJob._link (node:internal/modules/esm/module_job:182:49) {
|
||||||
|
code: 'ERR_MODULE_NOT_FOUND'
|
||||||
|
}
|
||||||
|
|
||||||
|
Node.js v22.22.0
|
||||||
|
npm notice
|
||||||
|
npm notice New minor version of npm available! 11.7.0 -> 11.10.0
|
||||||
|
npm notice Changelog: https://github.com/npm/cli/releases/tag/v11.10.0
|
||||||
|
npm notice To update run: npm install -g [email protected]
|
||||||
|
npm notice
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
> [email protected] dashboard:serve
|
||||||
|
> node bin/torch-lock.mjs dashboard
|
||||||
|
|
||||||
|
Dashboard running at http://localhost:4173/dashboard/
|
||||||
|
Serving files from /app
|
||||||
|
Using configuration from /app
|
||||||
|
Terminated
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
> [email protected] dashboard:serve
|
||||||
|
> node bin/torch-lock.mjs dashboard
|
||||||
|
|
||||||
|
Dashboard running at http://localhost:4173/dashboard/
|
||||||
|
Serving files from /app
|
||||||
|
Using configuration from /app
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# TORCH Distribution Strategy: Hybrid "Drop-in"
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
To make TORCH the universal "operating system for AI agents" across all languages and environments, we must remove the dependency on Node.js/NPM for end users while maintaining a robust development experience for contributors.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
Currently, TORCH is distributed as a tarball that requires `npm install`. This assumes:
|
||||||
|
1. The user has Node.js installed.
|
||||||
|
2. The user is comfortable with `package.json` and `node_modules`.
|
||||||
|
3. The project is a JavaScript/TypeScript project.
|
||||||
|
|
||||||
|
For Python, Go, or Rust developers, this is friction. A "drop-in" tool should work without language-specific prerequisites.
|
||||||
|
|
||||||
|
## The Solution: Hybrid Distribution
|
||||||
|
|
||||||
|
We will support three primary distribution methods to cover all use cases:
|
||||||
|
|
||||||
|
### 1. The Official NPM Package (Current)
|
||||||
|
**Target Audience:** Node.js / TypeScript developers.
|
||||||
|
**Experience:** `npm install torch-lock` (or similar).
|
||||||
|
**Pros:** seamless integration with existing JS toolchains, easy updates via dependabot/renovate.
|
||||||
|
|
||||||
|
### 2. Standalone Binary CLI (New)
|
||||||
|
**Target Audience:** Python, Rust, Go, and non-Node developers.
|
||||||
|
**Experience:** Download a single binary (`torch`) from GitHub Releases and add it to `$PATH`.
|
||||||
|
**Implementation:**
|
||||||
|
- Use Node.js Single Executable Applications (SEA) or `pkg` to compile the JS source into binaries for Linux, macOS, and Windows.
|
||||||
|
- **Zero Dependencies:** No `npm`, no `node_modules`.
|
||||||
|
- **Fast:** Instant startup, no install process.
|
||||||
|
|
||||||
|
### 3. Hosted Dashboard (New)
|
||||||
|
**Target Audience:** All users (especially those using the binary).
|
||||||
|
**Experience:** A static web app hosted at a public URL (e.g., `dashboard.torch.sh` or GitHub Pages).
|
||||||
|
**Workflow:**
|
||||||
|
1. User runs `torch lock ...` locally.
|
||||||
|
2. CLI prints: `View active locks: https://dashboard.torch.sh/?hashtag=my-project-hash`
|
||||||
|
3. User clicks the link to see the dashboard.
|
||||||
|
**Features:**
|
||||||
|
- **Stateless:** The dashboard is a static HTML/JS file.
|
||||||
|
- **Client-side Logic:** It connects directly to Nostr relays from the browser.
|
||||||
|
- **URL Configuration:** Configuration (hashtag, namespace, relays) is passed via URL query parameters, making links shareable.
|
||||||
|
- **Local Persistence:** Settings are saved to `localStorage` for returning visitors.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
- **Universal Access:** Removes the Node.js barrier to entry.
|
||||||
|
- **True "Drop-in":** The binary + hosted dashboard requires *zero* local setup beyond downloading one file.
|
||||||
|
- **Maintainability:** The core logic remains in JavaScript/Node.js, shared across all three distribution methods.
|
||||||
Reference in new issue
Block a user