hashtree-cli 0.2.39

Hashtree daemon and CLI - content-addressed storage with P2P sync
Documentation
use axum::response::Html;

pub fn root_page() -> Html<&'static str> {
    Html(
        r#"<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hashtree - Content-Addressed Storage</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e0e0e0; line-height: 1.6; }
        .container { max-width: 900px; margin: 0 auto; padding: 40px 20px; }
        h1 { font-size: 2.5em; margin-bottom: 10px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
        .subtitle { color: #888; margin-bottom: 40px; }
        .card { background: #1a1a1a; border-radius: 12px; padding: 30px; margin-bottom: 30px; border: 1px solid #2a2a2a; }
        .upload-area { border: 2px dashed #444; border-radius: 8px; padding: 40px; text-align: center; transition: all 0.3s; cursor: pointer; }
        .upload-area:hover { border-color: #667eea; background: #1f1f1f; }
        input[type="file"] { width: 100%; padding: 12px; margin: 10px 0; background: #0a0a0a; border: 1px solid #333; border-radius: 6px; color: #e0e0e0; font-size: 14px; }
        button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 30px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 600; transition: transform 0.2s; }
        button:hover { transform: translateY(-2px); }
        .status { padding: 10px; margin: 10px 0; border-radius: 6px; display: none; }
        .status.success { background: #1a3a1a; color: #4ade80; border: 1px solid #2a5a2a; display: block; }
        .status.error { background: #3a1a1a; color: #f87171; border: 1px solid #5a2a2a; display: block; }
        .cid { font-family: monospace; color: #667eea; word-break: break-all; }
        a { color: #667eea; text-decoration: none; }
        a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Hashtree</h1>
        <p class="subtitle">Like Blossom, but with directories and chunking</p>

        <div class="card">
            <h2>Upload File</h2>
            <form id="uploadForm" enctype="multipart/form-data">
                <div class="upload-area" id="dropArea">
                    <input type="file" id="fileInput" name="file" required>
                </div>
                <button type="submit" id="uploadBtn">Upload</button>
            </form>
            <div id="status" class="status"></div>
        </div>

        <div class="card">
            <h2>Pinned CIDs</h2>
            <div id="pinList">Loading...</div>
            <button onclick="loadPins()">Refresh</button>
            <button onclick="runGC()">Run Garbage Collection</button>
        </div>

        <div class="card">
            <h2>Git Repositories</h2>
            <div id="gitRepos">Loading...</div>
            <button onclick="loadGitRepos()">Refresh</button>
        </div>

        <div class="card">
            <h2>Storage Stats</h2>
            <div id="stats">Loading...</div>
            <button onclick="loadStats()">Refresh</button>
        </div>

        <div class="card">
            <h2>Retrieve by CID</h2>
            <input type="text" id="cidInput" placeholder="Enter CID (e.g., bafireign7yfvtni25wlzwj6hm7zlrkq3ecxdlpifisu5y5d4kynug2bgyy)">
            <button onclick="retrieveCID()">Retrieve</button>
        </div>

        <div class="card">
            <h2>Nostr Resolver</h2>
            <p style="color: #888; margin-bottom: 15px;">Look up content by npub/treename (resolves to hash via Nostr relays)</p>
            <input type="text" id="npubInput" placeholder="npub1... or hex pubkey">
            <input type="text" id="treeInput" placeholder="tree name">
            <button onclick="resolveTree()">Resolve & View</button>
            <button onclick="listTrees()">List Trees</button>
            <div id="resolverStatus" class="status"></div>
            <div id="treeList" style="margin-top: 15px;"></div>
        </div>
    </div>

    <script>
        // Read auth from URL hash or localStorage
        let AUTH_USER = localStorage.getItem('hashtree_auth_user');
        let AUTH_PASS = localStorage.getItem('hashtree_auth_pass');

        // Check URL hash for credentials
        if (window.location.hash) {
            const hash = window.location.hash.substring(1);
            const parts = hash.split(':');
            if (parts.length === 2) {
                AUTH_USER = parts[0];
                AUTH_PASS = parts[1];

                // Save to localStorage
                localStorage.setItem('hashtree_auth_user', AUTH_USER);
                localStorage.setItem('hashtree_auth_pass', AUTH_PASS);

                // Clear hash from URL
                history.replaceState(null, '', window.location.pathname + window.location.search);
            }
        }

        function getAuthHeaders() {
            if (AUTH_USER && AUTH_PASS) {
                const credentials = btoa(`${AUTH_USER}:${AUTH_PASS}`);
                return { 'Authorization': `Basic ${credentials}` };
            }
            return {};
        }

        async function loadPins() {
            const response = await fetch('/api/pins');
            const data = await response.json();
            const pinList = document.getElementById('pinList');
            if (data.pins && data.pins.length > 0) {
                pinList.innerHTML = data.pins.map(pin => {
                    const icon = pin.is_directory ? '📁' : '📄';
                    return `<div style="padding: 10px; margin: 5px 0; background: #0a0a0a; border-radius: 6px; display: flex; align-items: center; justify-content: space-between;">
                        <div style="flex: 1;">
                            <div style="margin-bottom: 5px;">
                                <span style="font-size: 18px;">${icon}</span>
                                <a href="/${pin.cid}" style="font-weight: 600; margin-left: 8px;">${pin.name}</a>
                            </div>
                            <div class="cid" style="font-size: 12px; opacity: 0.7; margin-left: 26px;">${pin.cid}</div>
                        </div>
                        <button onclick="unpinCID('${pin.cid}')" style="padding: 5px 15px; font-size: 12px;">Unpin</button>
                    </div>`;
                }).join('');
            } else {
                pinList.innerHTML = '<p style="color: #666;">No pins yet</p>';
            }
        }

        async function loadStats() {
            const response = await fetch('/api/stats');
            const data = await response.json();
            const stats = document.getElementById('stats');
            stats.innerHTML = `
                <p>Total DAGs: ${data.total_dags}</p>
                <p>Pinned DAGs: ${data.pinned_dags}</p>
                <p>Total Size: ${(data.total_bytes / 1024).toFixed(2)} KB</p>
            `;
        }

        async function loadGitRepos() {
            const gitRepos = document.getElementById('gitRepos');
            try {
                const response = await fetch('/api/git/repos');
                if (!response.ok) {
                    gitRepos.innerHTML = '<p style="color: #666;">Git not configured</p>';
                    return;
                }
                const data = await response.json();
                if (data.error) {
                    gitRepos.innerHTML = `<p style="color: #666;">${data.error}</p>`;
                    return;
                }
                if (!data.has_repo || data.branches.length === 0) {
                    const cloneUrl = window.location.origin + data.clone_url;
                    gitRepos.innerHTML = `<p style="color: #666;">No repositories yet</p>
                        <p style="font-size: 12px; color: #888; margin-top: 10px;">Push a repo:</p>
                        <code style="display: block; background: #0a0a0a; padding: 10px; border-radius: 4px; font-size: 12px; word-break: break-all;">git remote add hashtree ${cloneUrl}<br>git push hashtree main</code>`;
                    return;
                }
                const cloneUrl = window.location.origin + data.clone_url;
                gitRepos.innerHTML = `
                    <div style="padding: 10px; margin: 5px 0; background: #0a0a0a; border-radius: 6px;">
                        <div style="margin-bottom: 10px;">
                            <span style="font-size: 18px;">📦</span>
                            <span style="font-weight: 600; margin-left: 8px;">Repository</span>
                        </div>
                        <div style="font-size: 12px; margin-bottom: 10px;">
                            <span style="color: #888;">Clone:</span>
                            <code class="cid" style="margin-left: 5px;">${cloneUrl}</code>
                        </div>
                        <div style="font-size: 12px;">
                            <span style="color: #888;">Branches:</span>
                            ${data.branches.map(b => `<span style="background: #667eea33; padding: 2px 8px; border-radius: 4px; margin-left: 5px;">${b.name}</span>`).join('')}
                        </div>
                    </div>`;
            } catch (e) {
                gitRepos.innerHTML = '<p style="color: #666;">Git not configured</p>';
            }
        }

        async function runGC() {
            const response = await fetch('/api/gc', {
                method: 'POST',
                headers: getAuthHeaders()
            });
            const data = await response.json();
            alert(`GC complete: Deleted ${data.deleted_dags} DAGs, freed ${(data.freed_bytes / 1024).toFixed(2)} KB`);
            loadPins();
            loadStats();
        }

        async function unpinCID(cid) {
            await fetch(`/api/unpin/${cid}`, {
                method: 'POST',
                headers: getAuthHeaders()
            });
            loadPins();
        }

        function retrieveCID() {
            const cid = document.getElementById('cidInput').value;
            if (cid) {
                window.open(`/${cid}`, '_blank');
            }
        }

        document.getElementById('uploadForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const formData = new FormData(e.target);
            const status = document.getElementById('status');

            try {
                const response = await fetch('/upload', {
                    method: 'POST',
                    headers: getAuthHeaders(),
                    body: formData
                });
                const data = await response.json();

                if (data.success) {
                    status.className = 'status success';
                    status.innerHTML = `Upload successful! CID: <a href="/${data.cid}" class="cid">${data.cid}</a>`;
                    loadPins();
                    loadStats();
                } else {
                    status.className = 'status error';
                    status.textContent = `Error: ${data.error}`;
                }
            } catch (error) {
                status.className = 'status error';
                status.textContent = `Error: ${error.message}`;
            }
        });

        async function resolveTree() {
            const npub = document.getElementById('npubInput').value;
            const tree = document.getElementById('treeInput').value;
            const status = document.getElementById('resolverStatus');

            if (!npub || !tree) {
                status.className = 'status error';
                status.textContent = 'Please enter both npub/pubkey and tree name';
                return;
            }

            status.className = 'status';
            status.style.display = 'block';
            status.style.background = '#1a1a3a';
            status.style.color = '#667eea';
            status.textContent = 'Resolving...';

            try {
                const response = await fetch(`/api/resolve/${encodeURIComponent(npub)}/${encodeURIComponent(tree)}`);
                const data = await response.json();

                if (data.error) {
                    status.className = 'status error';
                    status.textContent = `Error: ${data.error}`;
                } else {
                    status.className = 'status success';
                    status.innerHTML = `Resolved! Hash: <a href="/${data.hash}" class="cid">${data.hash}</a>`;
                    // Open in new tab
                    window.open(`/n/${encodeURIComponent(npub)}/${encodeURIComponent(tree)}`, '_blank');
                }
            } catch (error) {
                status.className = 'status error';
                status.textContent = `Error: ${error.message}`;
            }
        }

        async function listTrees() {
            const npub = document.getElementById('npubInput').value;
            const treeList = document.getElementById('treeList');
            const status = document.getElementById('resolverStatus');

            if (!npub) {
                status.className = 'status error';
                status.textContent = 'Please enter an npub or pubkey';
                return;
            }

            status.className = 'status';
            status.style.display = 'block';
            status.style.background = '#1a1a3a';
            status.style.color = '#667eea';
            status.textContent = 'Fetching trees...';

            try {
                const response = await fetch(`/api/trees/${encodeURIComponent(npub)}`);
                const data = await response.json();

                if (data.error) {
                    status.className = 'status error';
                    status.textContent = `Error: ${data.error}`;
                    treeList.innerHTML = '';
                } else {
                    status.style.display = 'none';
                    if (data.trees && data.trees.length > 0) {
                        treeList.innerHTML = data.trees.map(t => `
                            <div style="padding: 10px; margin: 5px 0; background: #0a0a0a; border-radius: 6px;">
                                <div style="margin-bottom: 5px;">
                                    <span style="font-size: 18px;">🌲</span>
                                    <a href="/n/${encodeURIComponent(npub)}/${encodeURIComponent(t.name)}" style="font-weight: 600; margin-left: 8px;">${t.name}</a>
                                </div>
                                <div class="cid" style="font-size: 12px; opacity: 0.7; margin-left: 26px;">${t.hash}</div>
                            </div>
                        `).join('');
                    } else {
                        treeList.innerHTML = '<p style="color: #666;">No trees found for this pubkey</p>';
                    }
                }
            } catch (error) {
                status.className = 'status error';
                status.textContent = `Error: ${error.message}`;
                treeList.innerHTML = '';
            }
        }

        // Load initial data
        loadPins();
        loadStats();
        loadGitRepos();
    </script>
</body>
</html>"#,
    )
}