mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +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">
|
||||
<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
|
||||
id="refreshBtn"
|
||||
type="button"
|
||||
@@ -164,6 +171,65 @@
|
||||
</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">
|
||||
import { KIND_APP_DATA } from '../src/constants.mjs';
|
||||
import { createElement, escapeHtml } from './domUtils.js';
|
||||
@@ -171,31 +237,95 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// Modal Logic
|
||||
// -----------------------------------------------------------------------
|
||||
const modal = document.getElementById("docsModal");
|
||||
const btn = document.getElementById("openDocs");
|
||||
const span = document.getElementsByClassName("close-modal")[0];
|
||||
const docsModal = document.getElementById("docsModal");
|
||||
const settingsModal = document.getElementById("settingsModal");
|
||||
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");
|
||||
let docsLoaded = false;
|
||||
|
||||
if (btn) {
|
||||
btn.onclick = function(e) {
|
||||
// Docs Modal
|
||||
if (openDocsBtn) {
|
||||
openDocsBtn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
modal.classList.add("show");
|
||||
docsModal.classList.add("show");
|
||||
if (!docsLoaded) {
|
||||
fetchDocs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (span) {
|
||||
span.onclick = function() {
|
||||
modal.classList.remove("show");
|
||||
if (closeDocsBtn) {
|
||||
closeDocsBtn.onclick = function() {
|
||||
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) {
|
||||
if (event.target == modal) {
|
||||
modal.classList.remove("show");
|
||||
if (event.target == docsModal) {
|
||||
docsModal.classList.remove("show");
|
||||
}
|
||||
if (event.target == settingsModal) {
|
||||
settingsModal.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,23 +393,46 @@
|
||||
|
||||
function parseDashboardConfig(torchConfig = {}) {
|
||||
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 lockConfig = torchConfig.nostrLock || {};
|
||||
|
||||
const configuredNamespace =
|
||||
(dashboardConfig.namespace || lockConfig.namespace || DASHBOARD_DEFAULTS.namespace);
|
||||
const namespace =
|
||||
(params.get('namespace') || configuredNamespace).trim() || DASHBOARD_DEFAULTS.namespace;
|
||||
const hashtag =
|
||||
(params.get('hashtag') || dashboardConfig.hashtag || DASHBOARD_DEFAULTS.hashtag).trim() ||
|
||||
`${namespace}-agent-lock`;
|
||||
const relaysParam = (params.get('relays') || '').trim();
|
||||
const relays = relaysParam
|
||||
? relaysParam
|
||||
.split(',')
|
||||
.map((relay) => relay.trim())
|
||||
.filter(Boolean)
|
||||
: [...(dashboardConfig.relays || lockConfig.relays || DASHBOARD_DEFAULTS.relays)];
|
||||
// Hierarchy: URL Param > LocalStorage > Config File > Default
|
||||
|
||||
// Namespace
|
||||
const configNamespace = dashboardConfig.namespace || lockConfig.namespace || DASHBOARD_DEFAULTS.namespace;
|
||||
const namespace = (
|
||||
params.get('namespace') ||
|
||||
localPrefs.namespace ||
|
||||
configNamespace
|
||||
).trim() || DASHBOARD_DEFAULTS.namespace;
|
||||
|
||||
// Hashtag
|
||||
const configHashtag = dashboardConfig.hashtag || DASHBOARD_DEFAULTS.hashtag;
|
||||
const hashtag = (
|
||||
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 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