first commit

This commit is contained in:
Raven Scott
2026-07-11 19:01:46 -04:00
commit acc2042f82
84 changed files with 13094 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
.DS_Store
*.log
.vite/
+49
View File
@@ -0,0 +1,49 @@
# peardock-website
Official marketing site and documentation for [peardock](https://peardock.boats)
([source](https://git.ssh.surf/snxraven/peardock)).
## Install story (keep in sync with main repo)
Canonical installer host: **https://install.peardock.boats**
```bash
curl -fsSL https://install.peardock.boats | bash
```
- **Server (Linux):** Bare binary → `/opt/peardock`, user `peardock` in docker group, `peardock.service`
- **Client:** rolling `peardock-client-VERSION-HOST.tar.gz` (macOS `.app`, Linux binary + desktop entry)
- **Assets:** `https://git.ssh.surf/snxraven/peardock/releases/tag/rolling`
When peardocks install process changes, update:
- `download.html`
- `docs/quickstart.html`
- `docs/operator.html`
- `docs/faq.html`
- `docs/release.html`
- home page `#quickstart` teaser in `index.html`
## Releases
- UI: `/releases` (live list from Gitea)
- API: `GET /api/releases`, `/api/releases/latest`, `/api/releases/tags/:tag`
- Upstream: `https://git.ssh.surf/api/v1/repos/snxraven/peardock/releases`
- Web: `https://git.ssh.surf/snxraven/peardock/releases`
## Dev
```bash
npm install
npm run dev # Vite (proxies /api/releases → Gitea)
npm run build # → dist/
npm start # static dist + Gitea proxy (PORT=4173)
```
## Production
```bash
npm run build
npm start
# or: pm2 start server.mjs --name peardock-website
```
+147
View File
@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Community · PearDock</title>
<meta name="description" content="Join the PearDock community: open source on Gitea, Discord-Linux for chat, contributing guide, and support links." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/community.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix=""></div>
<header class="community-hero">
<div class="community-hero-inner">
<div class="badge mb-4">connect</div>
<h1>Community</h1>
<p class="lead">
Build with others. Report bugs, ship improvements, and hang out on Discord-Linux,
where PearDock and related open-source projects are discussed.
</p>
</div>
</header>
<div class="container community-page">
<div class="community-grid">
<article class="community-card">
<div class="community-card-icon" aria-hidden="true"></div>
<h2>Repository</h2>
<p>
PearDock is open source under AGPL-3.0. Browse the code, open issues, and send pull requests.
</p>
<ul class="community-list">
<li>Source on Gitea (snxraven/peardock)</li>
<li>Rolling releases &amp; install artifacts</li>
<li>Docs in-repo and on this site</li>
</ul>
<div class="community-card-actions">
<a class="btn btn-primary" href="https://git.ssh.surf/snxraven/peardock" target="_blank" rel="noopener">View repository</a>
<a class="btn btn-secondary" href="/releases">Releases</a>
</div>
</article>
<article class="community-card community-card--discord">
<div class="community-card-icon" aria-hidden="true">💬</div>
<h2>Discord-Linux</h2>
<p>
Chat, help, and project updates live on <strong>Discord-Linux</strong>, not a separate Holesail-only server.
Join for PearDock questions, Holepunch stack discussion, and contributor coordination.
</p>
<ul class="community-list">
<li>Real-time help and design chat</li>
<li>Contributor registration via bot commands</li>
<li>Cross-project Linux / P2P community</li>
</ul>
<div class="community-card-actions">
<a class="btn btn-primary" href="https://join.discord-linux.com" target="_blank" rel="noopener">Join Discord-Linux</a>
</div>
</article>
</div>
<section class="community-section" id="contributing">
<h2>Contributing</h2>
<p class="community-lead">Ways to help PearDock grow:</p>
<ul class="community-bullets">
<li>Report bugs and edge cases (Docker versions, NAT, Swarm, tunnels)</li>
<li>Suggest features and UX improvements</li>
<li>Submit pull requests</li>
<li>Improve documentation and examples</li>
<li>Test on different platforms (linux / darwin / win32 · x64 / arm64)</li>
</ul>
<h3>Guidelines</h3>
<ol class="community-steps">
<li>
<strong>Join Discord-Linux</strong> at
<a href="https://join.discord-linux.com" target="_blank" rel="noopener">join.discord-linux.com</a>
</li>
<li>Run <code>/register-git</code> to register an account (community workflow)</li>
<li>Fork the repository on Gitea</li>
<li>Create a branch for your changes</li>
<li>Test thoroughly, then open a PR with a clear description</li>
<li>Prioritize stability, security, and cross-platform compatibility</li>
</ol>
<div class="callout info">
<div class="callout-icon">i</div>
<div class="callout-body">
<strong>License.</strong> Contributions are expected under the projects
<a href="/legal/license">AGPL-3.0</a> terms unless otherwise stated.
Be mindful of third-party licenses (e.g. Holesail AGPL when redistributing combined builds).
</div>
</div>
</section>
<section class="community-section" id="support">
<h2>Support</h2>
<div class="community-support-grid">
<a class="community-support-card" href="/docs/">
<h3>Documentation</h3>
<p>Quick start, operator guide, architecture, security, and API reference.</p>
</a>
<a class="community-support-card" href="/learn/">
<h3>How it works</h3>
<p>Deep explainer of P2P control plane, hole punching, and Holesail data plane.</p>
</a>
<a class="community-support-card" href="/docs/faq">
<h3>FAQ</h3>
<p>Common questions about install, keys, Docker group, and upgrades.</p>
</a>
<a class="community-support-card" href="https://git.ssh.surf/snxraven/peardock/issues" target="_blank" rel="noopener">
<h3>Open an issue</h3>
<p>Bug reports and feature requests on the Gitea issue tracker.</p>
</a>
<a class="community-support-card" href="https://join.discord-linux.com" target="_blank" rel="noopener">
<h3>Ask on Discord</h3>
<p>Fastest path for interactive help. Join Discord-Linux.</p>
</a>
<a class="community-support-card" href="mailto:[email protected]">
<h3>Security / legal</h3>
<p>Report vulnerabilities and legal matters to [email protected].</p>
</a>
</div>
</section>
<div class="community-cta">
<h2>Jump in</h2>
<p>Install PearDock, join Discord-Linux, and help shape decentralized Docker ops.</p>
<div class="community-cta-actions">
<a class="btn btn-primary btn-lg" href="https://join.discord-linux.com" target="_blank" rel="noopener">Join Discord-Linux</a>
<a class="btn btn-secondary btn-lg" href="/download">Install PearDock</a>
<a class="btn btn-ghost btn-lg" href="https://git.ssh.surf/snxraven/peardock" target="_blank" rel="noopener">View source</a>
</div>
</div>
</div>
<div data-footer data-prefix=""></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>RPC &amp; API · PearDock Docs</title>
<meta name="description" content="PearDock RPC method groups, server pushes, roles, and protocol version." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>RPC &amp; API reference</h1>
<p class="docs-lead">
PearDock speaks <strong>protomux-rpc</strong> over HyperDHT. Methods and push channels are defined in
<code>shared/protocol.js</code>. This page summarizes groups for operators and integrators.
</p>
<div class="callout info">
<div class="callout-icon">i</div>
<div class="callout-body">
There is no public HTTP API. The only control surface is the P2P RPC session after
Noise handshake. Typed helpers live in <code>client/api.js</code>.
</div>
</div>
<h2>Handshake</h2>
<p>On connect, the client calls <code>handshake</code>. The server returns:</p>
<ul>
<li>Negotiated <code>PROTOCOL_VERSION</code></li>
<li><code>peerId</code> for this connection</li>
<li>Role (viewer / operator / admin)</li>
<li>Feature flags (swarm, plugins, holesail, …)</li>
</ul>
<h2>Roles</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Role</th><th>Typical access</th></tr>
</thead>
<tbody>
<tr><td><code>viewer</code></td><td>List/read, stats, logs (read), tunnel list</td></tr>
<tr><td><code>operator</code></td><td>Start/stop, deploy, tunnels, most mutations</td></tr>
<tr><td><code>admin</code></td><td>Peers, vault, unrestricted CLI (if enabled), ACL</td></tr>
</tbody>
</table>
</div>
<p>Exact method→role map is in server ACL (<code>server/core/acl.js</code> / <code>MethodRoles</code>).</p>
<h2>Method groups (client → server)</h2>
<h3>Session &amp; system</h3>
<ul>
<li><code>handshake</code>, <code>ping</code></li>
<li><code>getSystemDf</code>, <code>getMetrics</code>, <code>systemPrune</code></li>
<li>Host suggestions / snapshot helpers</li>
</ul>
<h3>Containers</h3>
<ul>
<li><code>listContainers</code>, inspect, start, stop, kill, restart, remove</li>
<li><code>deployContainer</code>, <code>recreateContainer</code>, <code>updateContainer</code> (resources)</li>
<li><code>containerTop</code>, archive, rename, network connect/disconnect</li>
<li><code>startTerminal</code> / terminal I/O channels</li>
<li><code>getContainerLogs</code> (+ download/export in UI)</li>
</ul>
<h3>Images, volumes, networks</h3>
<ul>
<li>List / pull / remove / prune images; Hub search</li>
<li>Volume list/create/remove; <code>browseVolume</code></li>
<li>Network list/create/remove/connect/disconnect</li>
</ul>
<h3>Stacks &amp; GitOps</h3>
<ul>
<li><code>deployStack</code> with compose content, override, env file, profiles, build flag</li>
<li>GitOps helpers for remote compose sources</li>
</ul>
<h3>Swarm (feature-gated)</h3>
<ul>
<li>Services, nodes, tasks</li>
<li>Secrets / configs create + list</li>
<li><code>scaleService</code></li>
</ul>
<h3>Peers, vault, access</h3>
<ul>
<li>Peer list / register / revoke / invites</li>
<li><code>vaultStoreCredential</code>, <code>vaultUseCredential</code>, list vault entries</li>
</ul>
<h3>Holesail tunnels</h3>
<ul>
<li><code>getHolesailStatus</code>, <code>listTunnels</code>, <code>getTunnel</code></li>
<li><code>createTunnel</code>, <code>closeTunnel</code></li>
</ul>
<h3>Schedules</h3>
<ul>
<li>List / create / delete maintenance schedules (prune, ping, …)</li>
</ul>
<h2>Pushes (server → client)</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Channel</th><th>Payload (conceptually)</th></tr>
</thead>
<tbody>
<tr><td><code>push:containers</code></td><td>Container list deltas / refresh</td></tr>
<tr><td><code>push:allStats</code></td><td>Live CPU/mem/net stats</td></tr>
<tr><td><code>push:logs</code></td><td>Log lines for open streams</td></tr>
<tr><td><code>push:pullProgress</code></td><td>Image pull layers</td></tr>
<tr><td><code>push:buildProgress</code></td><td>Build / compose build output</td></tr>
<tr><td><code>push:dockerEvent</code></td><td>Engine events</td></tr>
<tr><td><code>push:terminalOutput</code></td><td>PTY bytes for exec sessions</td></tr>
</tbody>
</table>
</div>
<h2>Integrating programmatically</h2>
<p>Use the same stack as the desktop client:</p>
<ol>
<li>Open a HyperDHT connection to the server public key.</li>
<li>Attach protomux + protomux-rpc.</li>
<li>Call <code>handshake</code>, then domain methods.</li>
<li>Subscribe to push channels as needed.</li>
</ol>
<p>Reference implementations: <code>client/connection.js</code>, <code>client/api.js</code>, <code>client/manager.js</code>.</p>
<div class="docs-pager">
<a href="/docs/security"><span>Previous</span><strong>← Security</strong></a>
<a class="next" href="/docs/release"><span>Next</span><strong>Releases →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+177
View File
@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Architecture · PearDock Docs</title>
<meta name="description" content="PearDock architecture: HyperDHT, protomux-rpc, handlers, Holesail data plane." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>Architecture</h1>
<p class="docs-lead">How PearDock is structured: control plane, data plane, repository layout, and RPC model. All peer-to-peer, with no open ports required for remote Docker ops.</p>
<h2>High-level</h2>
<p>Control plane (Docker RPC) and optional data plane (Holesail port tunnels) stay separate:</p>
<pre class="mermaid">
flowchart LR
subgraph Client["Desktop client"]
UI[PearDock UI]
end
subgraph Server["peardock server"]
RPC[protomux-rpc handlers]
HS[HolesailServer tunnels]
D[dockerode]
end
UI -- "HyperDHT · Noise · protomux-rpc" --> RPC
RPC --> D
D --> DE[dockerd]
HS -. "hs:// per published port" .-> Port["127.0.0.1:hostPort"]
Remote[Remote user / peer] -- "Holesail client" --> HS
</pre>
<h3>Planes</h3>
<pre class="mermaid">
flowchart TB
subgraph Control["Control plane"]
C1[HyperDHT keypair]
C2[protomux-rpc methods]
C3[ACL · audit · stats pushes]
end
subgraph Data["Data plane · optional"]
D1[Holesail L4 proxy]
D2["hs:// capability URLs"]
end
Control --> Docker[Docker Engine API]
Data --> Ports[Host published ports]
</pre>
<div class="table-wrap">
<table>
<thead>
<tr><th>Plane</th><th>Technology</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><strong>Control</strong></td>
<td>HyperDHT + protomux-rpc</td>
<td>Docker RPC: containers, deploy, logs, ACL, stats pushes</td>
</tr>
<tr>
<td><strong>Data / tunnels</strong></td>
<td>Holesail</td>
<td>L4 proxy of host:port ↔ remote peer via hs://</td>
</tr>
</tbody>
</table>
</div>
<div class="callout info">
<div class="callout-icon">i</div>
<div class="callout-body">
<strong>Why not replace RPC with Holesail?</strong>
Holesail tunnels bytes between sockets. PearDock needs structured methods, roles, audit, and pushes (stats, logs, terminal). Keep both.
</div>
</div>
<h2>Repository layout</h2>
<pre><code>shared/ Protocol constants + encodings (both sides)
server/
server.js Entry: HyperDHT listen
core/ Keys, peer registry, ACL, audit, vault
rpc/ PeerSession, handler registration
handlers/ Domain methods (containers, images, volumes, …)
services/ Docker client, stats, events, Holesail, schedules
utils/ Validation, rate limit, logging, compose, GitOps
client/
connection.js Single HyperDHT + protomux-rpc link
manager.js Multi-server connections + persistence
api.js Typed RPC helpers
app.js + libs/ Desktop UI
electron/ Electron shell + OTA + GUI bundle
assets/ Logos + favicons
build/icon.* Package icons
peardock-branding/ Master brand package</code></pre>
<h2>RPC model</h2>
<pre class="mermaid">
sequenceDiagram
participant C as Client
participant S as Server
participant D as dockerd
C->>S: handshake / ping
S-->>C: role · protocol version
C->>S: listContainers / deploy / …
S->>D: dockerode API
D-->>S: result
S-->>C: response
S-->>C: push:containers / push:allStats / …
</pre>
<p><strong>Client → server</strong> methods (examples):</p>
<ul>
<li><code>handshake</code>, <code>ping</code></li>
<li><code>listContainers</code>, <code>killContainer</code>, <code>containerTop</code>, <code>deployContainer</code>, <code>recreateContainer</code></li>
<li><code>pruneImages</code>, <code>getSystemDf</code>, <code>systemPrune</code></li>
<li><code>startTerminal</code>, <code>getContainerLogs</code></li>
<li><code>deployStack</code>, Swarm methods, vault, tunnels, schedules…</li>
</ul>
<p><strong>Server → client</strong> pushes:</p>
<ul>
<li><code>push:containers</code>, <code>push:allStats</code>, <code>push:logs</code></li>
<li><code>push:pullProgress</code>, <code>push:buildProgress</code></li>
<li><code>push:dockerEvent</code>, <code>push:terminalOutput</code></li>
</ul>
<p>Defined in <code>shared/protocol.js</code>. <code>PROTOCOL_VERSION</code> is negotiated on connect.</p>
<h2>Security surfaces</h2>
<ul>
<li>Noise transport (HyperDHT)</li>
<li>Roles: viewer / operator / admin + method ACL</li>
<li>Optional peer allowlist + invites</li>
<li>Rate limits per peer</li>
<li>Audit log for privileged methods</li>
<li>Registry vault AES-GCM (keyed from seed)</li>
<li>Browse roots default-deny</li>
</ul>
<p>See <a href="/docs/security">Security &amp; threat model</a>.</p>
<h2>Breaking changes from v1</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>v1 (legacy)</th><th>v2 (current)</th></tr>
</thead>
<tbody>
<tr><td>Hyperswarm topic = SERVER_KEY</td><td>HyperDHT listen on keypair from seed</td></tr>
<tr><td>Share topic hex with clients</td><td>Share <strong>public key</strong> with clients</td></tr>
<tr><td>Raw JSON on duplex streams</td><td>protomux-rpc methods + push channels</td></tr>
<tr><td>Monolithic server.js switch</td><td>Modular handlers under server/handlers/</td></tr>
</tbody>
</table>
</div>
<div class="docs-pager">
<a href="/docs/quickstart"><span>Previous</span><strong>← Quick start</strong></a>
<a class="next" href="/docs/operator"><span>Next</span><strong>Operator guide →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>FAQ · PearDock Docs</title>
<meta name="description" content="Frequently asked questions about PearDock: install, keys, Docker group, Pear, Gatekeeper, AGPL, upgrades." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>FAQ</h1>
<p class="docs-lead">Short answers to common questions.</p>
<h2>Do I need to open ports or set up VPN / remote access?</h2>
<p>
<strong>No.</strong> PearDock is peer-to-peer over HyperDHT. You share the servers public key;
clients holepunch an encrypted connection. You do not open inbound firewall ports for Docker or the control UI,
expose <code>dockerd</code> to the internet, or maintain a permanent VPN just to run ops.
Optional Holesail tunnels reach published services the same way, still without punching host firewall holes.
</p>
<h2>How do I install PearDock?</h2>
<pre><code>curl -fsSL https://install.peardock.boats | bash</code></pre>
<p>
Interactive: choose <strong>server</strong> (Linux systemd) or <strong>client</strong> (desktop).
Non-interactive: <code>--server --yes</code> / <code>--client --yes</code>.
Details: <a href="/download">Download &amp; install</a>.
</p>
<h2>What do I share with clients?</h2>
<p>The server <strong>public key</strong> (64 hex characters). Never share <code>SERVER_SEED</code>.</p>
<h2>Where is the public key after install?</h2>
<pre><code>sudo journalctl -u peardock -n 80 --no-pager | grep -i 'public key'</code></pre>
<p>Also stored under <code>/opt/peardock/.env</code> as <code>SERVER_PUBLIC_KEY</code> when generated.</p>
<h2>Container list empty / “permission denied” on Docker</h2>
<p>The service user <code>peardock</code> needs the docker group:</p>
<pre><code>sudo usermod -aG docker peardock
sudo systemctl restart peardock
# if the group was just created:
sudo systemctl restart docker</code></pre>
<p>The one-line installer does this automatically on server install.</p>
<h2>Is there a cloud / SaaS component?</h2>
<p>No. HyperDHT holepunches between peers. Bootstrap nodes are part of the HyperDHT network; PearDock does not operate a proprietary control plane.</p>
<h2>Pear prints “DEPRECATED: pear run is deprecated”</h2>
<p>Platform-wide message. Prefer the Electron desktop client from the rolling release or <code>install.sh --client</code>. Source: <code>npm run start:client</code>.</p>
<h2>macOS says the app is damaged</h2>
<p>Usually quarantine or signature after download. Installer runs <code>xattr -cr</code>. Manually:</p>
<pre><code>xattr -cr ~/Applications/peardock.app
# or right-click → Open
# re-sign from source build:
npm run sign:macos -- out/peardock-darwin-arm64/peardock.app</code></pre>
<h2>Client lost connection. Does it reconnect?</h2>
<p>Yes. Unexpected disconnects retry every <strong>5 seconds</strong> until the peer is back or you remove it. Last active peer is restored on launch.</p>
<h2>Why is PearDock AGPL?</h2>
<p>PearDock is <strong>AGPL-3.0</strong>, matching Holesail (also AGPL-3.0). If you modify PearDock and let others use it over a network, review AGPL source-offer obligations. You can set <code>ENABLE_HOLESAIL=0</code> to disable tunnel creation.</p>
<h2>Can I run without Holesail?</h2>
<p>Yes: <code>ENABLE_HOLESAIL=0</code> in the server environment. Control-plane Docker management still works.</p>
<h2>Default role is admin. Is that safe?</h2>
<p>Convenient for a single trusted operator. For multi-operator fleets, set <code>PEARDOCK_DEFAULT_ROLE</code>, <code>PEARDOCK_ADMIN_KEYS</code>, and allowlist. See <a href="/docs/security">threat model</a>.</p>
<h2>v1 vs v2</h2>
<p>v1 used Hyperswarm topics and raw JSON streams. v2 uses HyperDHT keypairs, public keys, and protomux-rpc. Clients and servers are not wire-compatible across major versions.</p>
<h2>Where is peer state stored on the client?</h2>
<p>Typically <code>~/.config/peardock/cache/peers.json</code> (includes last active peer id).</p>
<h2>How do I update?</h2>
<ul>
<li><strong>Server:</strong> re-run <code>install.sh --server --yes</code> or replace the binary; keep <code>/opt/peardock/.env</code>; <code>systemctl restart peardock</code>.</li>
<li><strong>Client:</strong> re-run <code>install.sh --client --yes</code> or install a newer rolling tarball.</li>
<li><strong>Assets:</strong> <a href="https://git.ssh.surf/snxraven/peardock/releases/tag/rolling" target="_blank" rel="noopener">rolling</a> tag on every main CI build.</li>
</ul>
<h2>License</h2>
<p>PearDock and Holesail: <strong>AGPL-3.0</strong>. See <a href="/legal/license">Open Source Notices</a>.</p>
<div class="docs-pager">
<a href="/docs/release"><span>Previous</span><strong>← Releases</strong></a>
<a class="next" href="/docs/index"><span>Docs home</span><strong>Overview →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+137
View File
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Holesail tunnels · PearDock Docs</title>
<meta name="description" content="Holesail P2P tunnels in PearDock: reach published services without opening firewall ports. Architecture, RPC, security." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>Holesail tunnels</h1>
<p class="docs-lead">
<a href="https://github.com/holesail/holesail" target="_blank" rel="noopener">Holesail</a>
is a peer-to-peer TCP/UDP reverse proxy on HyperDHT. PearDock uses it <strong>beside</strong>
the Docker control plane so you can share published services via <code>hs://</code> keys.
<strong>No open ports, no inbound firewall rules, no “expose this to the internet” step.</strong>
</p>
<h2>Do not conflate the two planes</h2>
<pre class="mermaid">
flowchart LR
subgraph Control["Control plane"]
CLI[Client RPC]
SRV[Server handlers]
CLI -- "protomux-rpc" --> SRV
SRV --> DK[Docker]
end
subgraph Data["Data plane"]
HS[HolesailServer]
HC[Holesail client]
HC -- "hs:// P2P" --> HS
HS --> PT["host:port"]
end
Control -.->|"orthogonal · not a replacement"| Data
</pre>
<div class="table-wrap">
<table>
<thead>
<tr><th>Plane</th><th>Technology</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td>Control</td><td>HyperDHT + protomux-rpc</td><td>Docker RPC, ACL, logs, deploy</td></tr>
<tr><td>Data / tunnels</td><td>Holesail</td><td>L4 proxy host:port ↔ remote peer</td></tr>
</tbody>
</table>
</div>
<h2>Enable / disable</h2>
<p>Holesail tunnels are <strong>enabled by default</strong>. The <code>holesail</code> package is a required dependency.</p>
<pre><code># default: tunnels on
npm run server
# opt out
export ENABLE_HOLESAIL=0
npm run server</code></pre>
<div class="table-wrap">
<table>
<thead>
<tr><th>Variable</th><th>Default</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td><code>ENABLE_HOLESAIL</code></td><td>on</td><td>Set 0 / false / off / no to disable</td></tr>
<tr><td><code>PEARDOCK_MAX_TUNNELS</code></td><td>20</td><td>Concurrent tunnel cap</td></tr>
<tr><td><code>PEARDOCK_TUNNEL_HOSTS</code></td><td>loopback + 0.0.0.0</td><td>Allowed tunnel targets (SSRF guard)</td></tr>
</tbody>
</table>
</div>
<h2>RPC</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Method</th><th>Role</th><th>Description</th></tr>
</thead>
<tbody>
<tr><td><code>getHolesailStatus</code></td><td>viewer</td><td>Feature flag + availability</td></tr>
<tr><td><code>listTunnels</code></td><td>viewer</td><td>Active tunnels (+ hs:// URLs)</td></tr>
<tr><td><code>getTunnel</code></td><td>viewer</td><td>One tunnel by id</td></tr>
<tr><td><code>createTunnel</code></td><td>operator</td><td>Start tunnel (host/port or container + port)</td></tr>
<tr><td><code>closeTunnel</code></td><td>operator</td><td>Stop tunnel</td></tr>
</tbody>
</table>
</div>
<p><code>createTunnel</code> uses <strong>secure</strong> mode by default so the DHT capability is not the raw listen key.</p>
<h2>UI</h2>
<p>Sidebar → <strong>Tunnels</strong>: create by host/port, list active tunnels, copy <code>hs://</code> URL, close.</p>
<p>Remote connect (outside PearDock):</p>
<pre><code>npx holesail 'hs://s000…'
# then open http://127.0.0.1:&lt;bound-port&gt;</code></pre>
<h2>Security</h2>
<ul>
<li>Treat <code>hs://</code> URLs as <strong>secrets</strong> (capability to reach the service).</li>
<li>Default targets are loopback / published Docker binds only.</li>
<li>Create/close are audited when <code>PEARDOCK_AUDIT</code> is on.</li>
<li>Max tunnel count limits resource exhaustion.</li>
<li>Do <strong>not</strong> reuse <code>SERVER_SEED</code> for tunnel keypairs. Each tunnel gets its own Holesail seed.</li>
</ul>
<h2>License note</h2>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<code>holesail</code> is a required dependency.
PearDock and Holesail are both <strong>AGPL-3.0</strong>.
Operators of modified network servers should review AGPL source-offer obligations.
Set <code>ENABLE_HOLESAIL=0</code> if you must run without opening tunnels (package still installed).
</div>
</div>
<div class="docs-pager">
<a href="/docs/operator"><span>Previous</span><strong>← Operator</strong></a>
<a class="next" href="/docs/security"><span>Next</span><strong>Security →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+123
View File
@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Documentation · PearDock</title>
<meta name="description" content="PearDock documentation: quick start, architecture, operator guide, Holesail, security, RPC, and releases." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>Documentation</h1>
<p class="docs-lead">
Everything you need to run, harden, and extend PearDock, from first public key to multi-operator fleets and Holesail tunnels.
Peer-to-peer by design: <strong>never open ports, expose Docker, or re-solve remote access</strong> for every host.
</p>
<div class="docs-cards">
<a class="docs-card" href="/learn/">
<span class="card-tag">Concepts</span>
<h3>How PearDock works</h3>
<p>Deep explainer: P2P identity, hole punching, control vs data plane, security, and fleet model.</p>
</a>
<a class="docs-card" href="/docs/quickstart">
<span class="card-tag">Start here</span>
<h3>Quick start</h3>
<p>One-line install.sh, systemd server, desktop client, and first peer connection.</p>
</a>
<a class="docs-card" href="/docs/architecture">
<span class="card-tag">Design</span>
<h3>Architecture</h3>
<p>HyperDHT control plane, protomux-rpc, handlers, and how Holesail sits beside RPC.</p>
</a>
<a class="docs-card" href="/docs/operator">
<span class="card-tag">Production</span>
<h3>Operator guide</h3>
<p>systemd, roles, vault, feature flags, logging, compose stacks, backups.</p>
</a>
<a class="docs-card" href="/docs/holesail">
<span class="card-tag">Tunnels</span>
<h3>Holesail</h3>
<p>Reach services over hs:// with no inbound firewall ports.</p>
</a>
<a class="docs-card" href="/docs/security">
<span class="card-tag">Hardening</span>
<h3>Security &amp; threat model</h3>
<p>Assets, trust boundaries, ACL, residual risks, operator checklist.</p>
</a>
<a class="docs-card" href="/docs/api">
<span class="card-tag">Reference</span>
<h3>RPC &amp; API</h3>
<p>Method groups, pushes, roles, and protocol version negotiation.</p>
</a>
<a class="docs-card" href="/docs/release">
<span class="card-tag">CI</span>
<h3>Releases</h3>
<p>Host matrix, bare-standalone, electron-forge, rolling tag, checksums.</p>
</a>
<a class="docs-card" href="/docs/faq">
<span class="card-tag">Help</span>
<h3>FAQ</h3>
<p>Common questions about Pear, keys, Gatekeeper, AGPL, and v1→v2.</p>
</a>
</div>
<h2>Install in one command</h2>
<div class="install-snippet">
<pre><code>curl -fsSL https://install.peardock.boats | bash</code></pre>
</div>
<p>
Interactive installer: server (Linux + systemd + docker group) or desktop client.
See <a href="/docs/quickstart">Quick start</a> and <a href="/download">Download</a>.
</p>
<h2>What is PearDock?</h2>
<p>
PearDock is <strong>decentralized Docker management</strong> on the modern Holepunch stack.
There is no central control plane and <strong>no need to open or expose ports</strong>.
The server listens on a HyperDHT keypair. Clients connect with its public key.
Transport is Noise-encrypted. The desktop UI is a Pear/Electron app that feels like a full
commercial Docker console, without SaaS tenancy or remote-access headaches.
</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>Layer</th><th>Technology</th></tr>
</thead>
<tbody>
<tr><td>Transport</td><td>HyperDHT (Noise-encrypted P2P)</td></tr>
<tr><td>RPC</td><td>protomux-rpc + compact-encoding JSON</td></tr>
<tr><td>Client</td><td>Pear desktop / Electron shell</td></tr>
<tr><td>Docker</td><td>dockerode</td></tr>
<tr><td>Port tunnels</td><td>Holesail (hs://)</td></tr>
<tr><td>License</td><td>AGPL-3.0 (same as Holesail)</td></tr>
</tbody>
</table>
</div>
<div class="docs-pager">
<span></span>
<a class="next" href="/docs/quickstart"><span>Next</span><strong>Quick start →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+216
View File
@@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Operator guide · PearDock Docs</title>
<meta name="description" content="Deploy and operate PearDock: install.sh, systemd, docker group, roles, vault, flags, logging, stacks, backup." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>Operator guide</h1>
<p class="docs-lead">Production install, multi-operator ACL, feature flags, logging, compose, and backup.</p>
<h2>Recommended: one-line installer</h2>
<p>On a Linux host with Docker already installed:</p>
<pre><code>curl -fsSL https://install.peardock.boats | bash -s -- --server --yes</code></pre>
<p>This installs the Bare server binary, creates user <code>peardock</code>, adds it to the <strong>docker</strong> group, writes a systemd unit, and can enable the service immediately.</p>
<pre><code>sudo systemctl status peardock
sudo journalctl -u peardock -f
# copy public key from logs into the desktop client</code></pre>
<p>Full flags and client install: <a href="/download">Download &amp; install</a>.</p>
<h2>Paths (installer defaults)</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Path</th><th>Role</th></tr>
</thead>
<tbody>
<tr><td><code>/opt/peardock/peardock-server</code></td><td>Server binary</td></tr>
<tr><td><code>/opt/peardock/.env</code></td><td>Identity / env (mode 600)</td></tr>
<tr><td><code>/etc/systemd/system/peardock.service</code></td><td>systemd unit</td></tr>
<tr><td><code>journalctl -u peardock</code></td><td>Logs</td></tr>
</tbody>
</table>
</div>
<h2>systemd unit (current)</h2>
<p>Matches <code>deploy/peardock.service</code> and the unit written by <code>install.sh</code>:</p>
<pre><code>[Unit]
Description=peardock HyperDHT Docker control plane
Documentation=https://git.ssh.surf/snxraven/peardock
After=network-online.target
Wants=network-online.target
Wants=docker.service
[Service]
Type=simple
WorkingDirectory=/opt/peardock
ExecStart=/opt/peardock/peardock-server
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
NoNewPrivileges=true
PrivateTmp=true
User=peardock
Group=docker
SupplementaryGroups=docker
Environment=NODE_ENV=production
EnvironmentFile=-/opt/peardock/.env
ReadWritePaths=/opt/peardock
[Install]
WantedBy=multi-user.target</code></pre>
<p>Source install alternative for <code>ExecStart</code>:</p>
<pre><code>ExecStart=/usr/bin/node /opt/peardock/server/server.js</code></pre>
<h2>Docker group / socket access</h2>
<p>
Engine sockets are typically <code>root:docker</code> mode <code>660</code>.
The service <strong>must</strong> run as a user in group <code>docker</code> (or the sockets group).
</p>
<pre><code># Installer does this; manual recovery:
sudo usermod -aG docker peardock
# if docker group was just created, restart Docker once
sudo systemctl restart docker
sudo systemctl restart peardock</code></pre>
<p>Symptoms of a missing group: empty container lists, start failures, “permission denied” on the socket.</p>
<h2>Manual install from source</h2>
<pre><code>git clone https://git.ssh.surf/snxraven/peardock.git /opt/peardock
cd /opt/peardock
npm ci --omit=dev # Node.js ≥ 20
sudo useradd --system --home /opt/peardock --shell /usr/sbin/nologin peardock
sudo usermod -aG docker peardock
sudo cp deploy/peardock.service /etc/systemd/system/
# edit ExecStart if using node instead of Bare binary
sudo systemctl daemon-reload
sudo systemctl enable --now peardock
sudo journalctl -u peardock -f</code></pre>
<h2>Healthcheck</h2>
<pre><code>node scripts/healthcheck.js
# exit 0 if Docker socket answers</code></pre>
<h2>Multi-operator setup</h2>
<ol>
<li>On first connect, note the clients peer id (handshake returns <code>peerId</code>).</li>
<li>Configure <code>/opt/peardock/.env</code> (or Environment= in the unit):</li>
</ol>
<pre><code>PEARDOCK_DEFAULT_ROLE=viewer
PEARDOCK_ADMIN_KEYS=&lt;64-hex-client-public-key&gt;
PEARDOCK_PEER_ALLOWLIST=1
PEARDOCK_BROWSE_ROOTS=/var/lib/docker/volumes
PEARDOCK_AUDIT=1</code></pre>
<ol start="3">
<li>As admin: <strong>Access → Create invite</strong> → send token to operator.</li>
<li>Operator connects with server public key + invite token as supported.</li>
<li><strong>Revoke</strong> lost devices from Access.</li>
</ol>
<h2>Registry vault</h2>
<p>Credentials encrypted at rest (AES-GCM derived from <code>SERVER_SEED</code>):</p>
<ul>
<li>UI: <strong>Access → Registry vault</strong></li>
<li>RPC: <code>vaultStoreCredential</code>, <code>vaultUseCredential</code>, <code>listVaultCredentials</code></li>
<li>File: <code>peardock-vault.json</code> (mode 600). Override with <code>PEARDOCK_VAULT_PATH</code>.</li>
</ul>
<h2>Feature flags</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Env</th><th>Effect</th></tr>
</thead>
<tbody>
<tr><td><code>ENABLE_SWARM=0</code></td><td>Disable Swarm / services / secrets / configs RPC (on by default)</td></tr>
<tr><td><code>ENABLE_PLUGINS=1</code></td><td>Plugin install/enable/remove</td></tr>
<tr><td><code>ENABLE_HOLESAIL=0</code></td><td>Disable Holesail tunnels (on by default)</td></tr>
<tr><td><code>PEARDOCK_MAX_TUNNELS</code></td><td>Max concurrent tunnels (default 20)</td></tr>
<tr><td><code>PEARDOCK_TUNNEL_HOSTS</code></td><td>Extra allowed tunnel target hosts</td></tr>
<tr><td><code>PEARDOCK_UNRESTRICTED_CLI=1</code></td><td>Broader docker CLI for admin</td></tr>
<tr><td><code>PEARDOCK_BROWSE_OPEN=1</code></td><td>Legacy open host FS browse (discouraged)</td></tr>
</tbody>
</table>
</div>
<h2>Logging</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Env</th><th>Effect</th></tr>
</thead>
<tbody>
<tr><td><code>LOG_LEVEL</code></td><td>error · warn · info · debug</td></tr>
<tr><td><code>LOG_FORMAT</code></td><td>pretty (TTY) · json (journald)</td></tr>
<tr><td><code>ENABLE_FILE_LOGGING=1</code></td><td>JSON lines under LOG_DIR</td></tr>
<tr><td><code>NO_COLOR=1</code></td><td>Disable ANSI in pretty mode</td></tr>
</tbody>
</table>
</div>
<pre><code># Prefer journald JSON in production
# Environment=LOG_FORMAT=json
# Environment=LOG_LEVEL=info
sudo journalctl -u peardock -f</code></pre>
<h2>Compose stacks</h2>
<p><code>deployStack</code> accepts:</p>
<ul>
<li><code>composeContent</code> (required)</li>
<li><code>overrideContent</code>: second compose file</li>
<li><code>envFileContent</code>: written as <code>.env</code> for compose CLI</li>
<li><code>profiles</code>: array or comma string</li>
<li><code>build: true</code>: runs <code>docker compose up --build</code></li>
</ul>
<h2>Metrics</h2>
<p>RPC <code>getMetrics</code> returns process memory, RPC counters, latency percentiles, feature flags.</p>
<h2>Updating the server</h2>
<ol>
<li>Re-run the installer with <code>--server --yes</code>, or replace <code>/opt/peardock/peardock-server</code> from a newer rolling tarball.</li>
<li><strong>Keep</strong> <code>/opt/peardock/.env</code> (seed) and vault/peer files.</li>
<li><code>sudo systemctl restart peardock</code></li>
</ol>
<h2>Certification / soak</h2>
<pre><code>npm run soak # short Docker health loop
npm run soak:24h # 24h certification</code></pre>
<h2>Backup</h2>
<p>Back up atomically from the server data dir (default <code>/opt/peardock</code>):</p>
<ul>
<li><code>.env</code> (<code>SERVER_SEED</code>)</li>
<li><code>peardock-vault.json</code></li>
<li><code>peardock-peers.json</code></li>
<li><code>peardock-audit.log</code> (optional)</li>
</ul>
<div class="docs-pager">
<a href="/docs/architecture"><span>Previous</span><strong>← Architecture</strong></a>
<a class="next" href="/docs/holesail"><span>Next</span><strong>Holesail →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+170
View File
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Quick start · PearDock Docs</title>
<meta name="description" content="Install PearDock with install.sh, run the HyperDHT server, and connect the desktop client." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>Quick start</h1>
<p class="docs-lead">
Fastest path: the one-line installer (rolling binaries).
After install you connect with a <strong>public key</strong>. No ports to open, no VPN, no public Docker API.
Alternative: run from source with Node.js ≥ 20.
</p>
<nav class="toc">
<strong>On this page</strong>
<ol>
<li><a href="#installer">One-line installer</a></li>
<li><a href="#after-server">After server install</a></li>
<li><a href="#after-client">After client install</a></li>
<li><a href="#connect">Connect</a></li>
<li><a href="#source">From source</a></li>
<li><a href="#binaries">Build binaries</a></li>
</ol>
</nav>
<h2 id="installer">1. One-line installer (recommended)</h2>
<p>
Detects OS/arch, downloads matching assets from the
<a href="https://git.ssh.surf/snxraven/peardock/releases/tag/rolling" target="_blank" rel="noopener">rolling</a>
release, and installs either a <strong>Linux systemd server</strong> or a <strong>desktop client</strong>.
</p>
<pre><code>curl -fsSL https://install.peardock.boats | bash</code></pre>
<p>Non-interactive:</p>
<pre><code># Server on Linux (systemd → peardock.service)
curl -fsSL https://install.peardock.boats | bash -s -- --server --yes
# Desktop client (Linux or macOS)
curl -fsSL https://install.peardock.boats | bash -s -- --client --yes
# Both (Linux host that also runs the GUI)
curl -fsSL https://install.peardock.boats | bash -s -- --both --yes</code></pre>
<div class="callout info">
<div class="callout-icon">i</div>
<div class="callout-body">
<strong>Requirements.</strong> <code>curl</code> or <code>wget</code>, <code>tar</code>.
Server install needs root/sudo, Docker installed, and Linux for systemd.
Windows: grab tarballs from the rolling release (bash installer is not for win32).
</div>
</div>
<h2 id="after-server">2. After server install</h2>
<p>Default layout:</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>Path</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td><code>/opt/peardock/peardock-server</code></td><td>Bare standalone binary</td></tr>
<tr><td><code>/opt/peardock/.env</code></td><td><code>SERVER_SEED</code> / public key material</td></tr>
<tr><td><code>/etc/systemd/system/peardock.service</code></td><td>systemd unit (user <code>peardock</code>, group <code>docker</code>)</td></tr>
</tbody>
</table>
</div>
<pre><code>sudo systemctl enable --now peardock
sudo systemctl status peardock
sudo journalctl -u peardock -f</code></pre>
<p>On first start the logs print:</p>
<pre><code> peardock server ready
Public key (paste into the client):
&lt;64 hex characters&gt;</code></pre>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<strong>Docker socket.</strong> The installer adds user <code>peardock</code> to the docker group so
<code>/var/run/docker.sock</code> is usable. If list/start fails with permission errors:
<code>sudo usermod -aG docker peardock && sudo systemctl restart peardock</code>.
Back up <code>SERVER_SEED</code> in <code>.env</code>. Rotating it changes the public key.
</div>
</div>
<h2 id="after-client">3. After client install</h2>
<ul>
<li><strong>macOS:</strong> <code>~/Applications/peardock.app</code>. Open from Finder or Spotlight.</li>
<li><strong>Linux:</strong> <code>peardock-client</code> on <code>PATH</code> if <code>~/.local/bin</code> is configured; desktop entry under <code>~/.local/share/applications</code>.</li>
</ul>
<p>Dev/source alternative (Electron):</p>
<pre><code>npm run start:client
# builds electron/app.bundle.cjs then launches Electron</code></pre>
<p>Pear development:</p>
<pre><code>export PATH="$HOME/Library/Application Support/pear/bin:$PATH" # macOS
npm run dev</code></pre>
<h2 id="connect">4. Connect</h2>
<pre class="mermaid">
flowchart LR
A[Install server] --> B[Copy public key from journal]
B --> C[Install / open client]
C --> D[Add peer · paste key]
D --> E[Handshake · role · live UI]
</pre>
<ol>
<li>Copy the server <strong>public key</strong> (64 hex chars) from journal logs.</li>
<li>Open the client → <strong>Add peer</strong> / sidebar connection field.</li>
<li>Paste the key (optional alias / invite token).</li>
<li>Handshake negotiates protocol version, returns <code>peerId</code> and role.</li>
</ol>
<p>
Default role is <strong>admin</strong> for a single operator.
Harden multi-operator fleets via the <a href="/docs/operator">operator guide</a>
and <a href="/docs/security">threat model</a>.
</p>
<p>
Saved peers reconnect automatically (retry every 5s if a peer drops).
Last active peer is restored on client launch.
</p>
<h2 id="source">5. From source</h2>
<pre><code>git clone https://git.ssh.surf/snxraven/peardock.git
cd peardock
npm install # or: npm ci
npm run server # host with Docker
npm run start:client # Electron GUI
# npm run dev # Pear desktop (dev)</code></pre>
<p>Production server host from source:</p>
<pre><code>npm ci --omit=dev
# copy deploy/peardock.service, create user peardock, add to docker group
# or use install.sh for the binary path instead</code></pre>
<h2 id="binaries">6. Standalone binaries (local make)</h2>
<p>64-bit only: <code>linux-x64</code>, <code>linux-arm64</code>, <code>darwin-x64</code>, <code>darwin-arm64</code>, <code>win32-x64</code>, <code>win32-arm64</code>.</p>
<pre><code>npm run make # all server + client hosts
npm run make:server:linux-x64
npm run make:client:darwin-arm64</code></pre>
<p>
Outputs under <code>out/</code>. CI publishes tag <strong>rolling</strong>.
See <a href="/docs/release">Releases</a> and <a href="/download">Download</a>.
</p>
<div class="docs-pager">
<a href="/docs/index"><span>Previous</span><strong>← Overview</strong></a>
<a class="next" href="/docs/architecture"><span>Next</span><strong>Architecture →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+154
View File
@@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Releases · PearDock Docs</title>
<meta name="description" content="PearDock release process: host matrix, bare-standalone, electron-forge, rolling CI." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>Releases</h1>
<p class="docs-lead">How PearDock binaries are built, signed, and published for all 64-bit hosts.</p>
<h2>Host matrix</h2>
<p>No 32-bit (<code>ia32</code> / <code>armv7</code>) builds.</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>Host</th><th>Server (Bare)</th><th>Client (Electron)</th></tr>
</thead>
<tbody>
<tr><td><code>linux-x64</code></td><td></td><td></td></tr>
<tr><td><code>linux-arm64</code></td><td></td><td></td></tr>
<tr><td><code>darwin-x64</code></td><td></td><td>✅ (ad-hoc from Linux CI)</td></tr>
<tr><td><code>darwin-arm64</code></td><td></td><td>✅ (ad-hoc from Linux CI)</td></tr>
<tr><td><code>win32-x64</code></td><td></td><td></td></tr>
<tr><td><code>win32-arm64</code></td><td></td><td></td></tr>
</tbody>
</table>
</div>
<p>Canonical list: <code>scripts/hosts.cjs</code><code>ALL_64</code>.</p>
<h2>Builders</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Artifact</th><th>Tooling</th></tr>
</thead>
<tbody>
<tr>
<td><strong>peardock-server</strong></td>
<td><code>scripts/bare-standalone.cjs</code> (bare-pack + bare-runtime prebuild embed)</td>
</tr>
<tr>
<td><strong>peardock-client</strong></td>
<td>esbuild → <code>electron/app.bundle.cjs</code>, then electron-forge package</td>
</tr>
</tbody>
</table>
</div>
<h2>Local builds</h2>
<pre><code>npm ci
# All 64-bit server + client hosts (same as CI)
npm run make
# Server only / client only
npm run make:server
npm run make:client
# Single host
npm run make:server:linux-x64
npm run make:client:darwin-arm64</code></pre>
<p>Outputs:</p>
<pre><code>out/peardock-server-&lt;host&gt;/peardock-server[.exe]
out/peardock-&lt;platform&gt;-&lt;arch&gt;/ # e.g. peardock.app on macOS</code></pre>
<pre><code>PEARDOCK_SERVER_HOSTS=linux-x64,win32-x64 \
PEARDOCK_CLIENT_HOSTS=darwin-arm64,linux-x64 \
npm run make</code></pre>
<h2>Asset naming</h2>
<p>Published under tag <strong>rolling</strong>:</p>
<pre><code>peardock-server-VERSION-HOST.tar.gz
peardock-server-VERSION-HOST.tar.gz.sha256
peardock-client-VERSION-HOST.tar.gz
peardock-client-VERSION-HOST.tar.gz.sha256
# HOST examples: linux-x64, darwin-arm64, win32-x64
# Example: peardock-server-2.0.1-linux-x64.tar.gz</code></pre>
<p>
The one-line installer resolves <code>VERSION</code> from the release API (or
<code>PEARDOCK_VERSION</code>) and downloads the matching host triple.
</p>
<h2>Gitea CI rolling release</h2>
<p>Workflow: <code>.gitea/workflows/release-rolling.yml</code><br />
Script: <code>scripts/gitea-rolling-release.sh</code></p>
<div class="table-wrap">
<table>
<thead>
<tr><th>Secret</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td><code>RELEASE_TOKEN</code></td><td>Gitea PAT with release write</td></tr>
<tr><td><code>GITEA_URL</code></td><td>Optional forge base URL</td></tr>
</tbody>
</table>
</div>
<p>On every push to <code>main</code> (and <code>workflow_dispatch</code>), Ubuntu CI:</p>
<ol>
<li>Installs deps (lifecycle scripts skipped for hang safety)</li>
<li>Installs vendored <code>rcodesign</code> for macOS ad-hoc signing</li>
<li>Cross-builds all six server hosts</li>
<li>Cross-packages all six Electron clients (target prebuilds only)</li>
<li>Stages tarballs + SHA-256 under <code>dist/release/</code></li>
<li>Recreates tag/release <strong>rolling</strong> and uploads assets</li>
</ol>
<pre><code>DRY_RUN=1 bash scripts/gitea-rolling-release.sh</code></pre>
<p>
Install / upgrade production hosts:
<code>curl -fsSL https://install.peardock.boats | bash -s -- --server --yes</code>
Live asset list: <a href="/releases">peardock.boats/releases</a>
(proxied from
<a href="https://git.ssh.surf/snxraven/peardock/releases" target="_blank" rel="noopener">Gitea</a>).
</p>
<h2>Notes</h2>
<ul>
<li><strong>macOS clients</strong> are ad-hoc codesigned after package so Gatekeeper does not show “damaged”. Re-sign: <code>npm run sign:macos -- out/peardock-darwin-arm64/peardock.app</code></li>
<li><strong>Native modules</strong> ship prebuilds per target; rebuild-from-source is disabled for cross packages.</li>
<li><strong>Server</strong> cross-compile uses bare-runtime platform prebuilds (no Docker-in-Docker required to build).</li>
</ul>
<h2>Checksums &amp; SBOM</h2>
<pre><code>./scripts/release-checksums.sh dist/
# SBOM notes: docs/SBOM.md in the main repo
npx @cyclonedx/cyclonedx-npm --output-file peardock-sbom.json</code></pre>
<div class="docs-pager">
<a href="/docs/api"><span>Previous</span><strong>← RPC &amp; API</strong></a>
<a class="next" href="/docs/faq"><span>Next</span><strong>FAQ →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+120
View File
@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Security · PearDock Docs</title>
<meta name="description" content="PearDock threat model, trust boundaries, controls, residual risks, hardening checklist." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout">
<aside class="docs-sidebar" data-docs-nav data-prefix="../"></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Docs menu"></button>
<article class="docs-main">
<h1>Security &amp; threat model</h1>
<p class="docs-lead">
Audience: operators deploying PearDock in multi-operator or semi-trusted environments.
Scope: HyperDHT P2P control plane + local Docker Engine socket.
</p>
<h2>1. Assets</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Asset</th><th>Sensitivity</th></tr>
</thead>
<tbody>
<tr><td><code>SERVER_SEED</code></td><td>Critical: identity and vault key derivation</td></tr>
<tr><td>Docker socket access</td><td>Critical: full host container control</td></tr>
<tr><td>Registry passwords (vault)</td><td>High: encrypted at rest</td></tr>
<tr><td>Peer invite tokens</td><td>Medium: short-lived capabilities</td></tr>
<tr><td>Audit log</td><td>Medium: forensic integrity</td></tr>
<tr><td>Container data / env secrets</td><td>High: via inspect, logs, exec</td></tr>
</tbody>
</table>
</div>
<h2>2. Trust boundaries</h2>
<pre><code>[Client] --Noise/HyperDHT--> [peardock server] --unix socket--> [dockerd]
|
+-- peardock-vault.json (AES-GCM)
+-- peardock-peers.json
+-- peardock-audit.log</code></pre>
<ul>
<li><strong>Anyone with the server public key</strong> can attempt a DHT connection.</li>
<li><strong>Default role is admin</strong> unless default role / admin keys / peer policy tighten it.</li>
<li>Swarm / plugins are off unless feature flags enable them.</li>
<li>Holesail tunnels are on by default. Each <code>hs://</code> URL is a port capability.</li>
<li>Host FS browse is default-deny unless browse roots / open mode are set.</li>
</ul>
<h2>3. Adversaries</h2>
<ol>
<li><strong>Remote peer with public key only:</strong> should not get Docker control if allowlist and non-admin default are set.</li>
<li><strong>Stolen invite token:</strong> limited by TTL and max uses. Rotate after use.</li>
<li><strong>Compromised client:</strong> can use any role the peer holds until revoke.</li>
<li><strong>Local host attacker with filesystem:</strong> can steal seed and vault if file perms are wrong.</li>
<li><strong>Malicious container:</strong> out of scope for PearDock. Engine isolation applies.</li>
</ol>
<h2>4. Controls (implemented)</h2>
<div class="table-wrap">
<table>
<thead>
<tr><th>Control</th><th>Mechanism</th></tr>
</thead>
<tbody>
<tr><td>Transport E2E</td><td>HyperDHT Noise</td></tr>
<tr><td>Capability ACL</td><td>viewer / operator / admin + MethodRoles</td></tr>
<tr><td>Peer policy</td><td>Invite, register, revoke, optional allowlist</td></tr>
<tr><td>Audit</td><td>Append-only log for privileged methods</td></tr>
<tr><td>Rate limit</td><td>Per-peer limiter on RPC</td></tr>
<tr><td>Registry secrets</td><td>AES-256-GCM vault</td></tr>
<tr><td>Browse FS</td><td>Root allowlist / default deny</td></tr>
<tr><td>Tunnel targets</td><td>Loopback / allowlisted hosts only</td></tr>
</tbody>
</table>
</div>
<h2>5. Residual risks</h2>
<ul>
<li><strong>Default admin</strong> is intentional for single-operator setup. Change it for multi-tenant fleets.</li>
<li>Binary image/export streams are size-capped but still large; DoS via memory if many concurrent transfers.</li>
<li>JSON-over-RPC relies on handler validation (not full hyperschema end-to-end).</li>
<li>Swarm secrets/configs once enabled are highly privileged.</li>
</ul>
<h2>6. Operator hardening checklist</h2>
<ul>
<li>Generate unique <code>SERVER_SEED</code>; back up offline; never commit <code>.env</code></li>
<li>Set <code>PEARDOCK_DEFAULT_ROLE=viewer</code> or <code>operator</code></li>
<li>Set <code>PEARDOCK_ADMIN_KEYS=&lt;your client public key hex&gt;</code></li>
<li>Enable <code>PEARDOCK_PEER_ALLOWLIST=1</code> after registering operators</li>
<li>Set <code>PEARDOCK_BROWSE_ROOTS</code> only if host path pickers are needed</li>
<li>Enable <code>PEARDOCK_AUDIT=1</code></li>
<li>Treat <code>hs://</code> tunnel URLs as secrets; set <code>ENABLE_HOLESAIL=0</code> if unused</li>
<li>Restrict filesystem permissions on vault, peers, and seed files</li>
</ul>
<div class="docs-pager">
<a href="/docs/holesail"><span>Previous</span><strong>← Holesail</strong></a>
<a class="next" href="/docs/api"><span>Next</span><strong>RPC &amp; API →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+178
View File
@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Download &amp; install · PearDock</title>
<meta name="description" content="Install PearDock: peer-to-peer Docker management with no open ports or VPN. One-line installer or rolling binaries for Linux, macOS, and Windows." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/assets/favicons/apple-touch-icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix=""></div>
<section class="download-hero">
<div class="container">
<div class="badge mb-4">rolling · 64-bit only</div>
<h1>Download &amp; install</h1>
<p class="lead">
Remote Docker without opening ports, exposing the Engine, or maintaining a VPN.
The recommended path is the interactive <strong>install.sh</strong> script.
It detects OS and arch, installs a Linux <strong>systemd</strong> server (with the docker group) or a desktop client, and pulls artifacts from the <strong>rolling</strong> release.
</p>
<div class="flex flex-wrap gap-3" style="justify-content: center;">
<a class="btn btn-primary btn-lg" href="#installer">Install with curl</a>
<a class="btn btn-secondary btn-lg" href="/releases">Browse releases</a>
<a class="btn btn-ghost btn-lg" href="https://git.ssh.surf/snxraven/peardock/releases" target="_blank" rel="noopener">Gitea</a>
</div>
</div>
</section>
<div class="container install-wide" style="margin: 0 auto 3rem;" id="installer">
<div class="card">
<h3 class="mb-4">One-line installer (recommended)</h3>
<p class="text-muted mb-4" style="font-size: 0.95rem;">
Interactive by default. Asks <strong>server</strong> vs <strong>client</strong> (or both).
Server path is Linux-only for systemd; client works on Linux and macOS.
Windows: download a <code>.tar.gz</code> from the rolling release.
</p>
<pre><code>curl -fsSL https://install.peardock.boats | bash</code></pre>
<pre><code># Non-interactive
curl -fsSL https://install.peardock.boats | bash -s -- --server --yes
curl -fsSL https://install.peardock.boats | bash -s -- --client --yes
curl -fsSL https://install.peardock.boats | bash -s -- --both --yes</code></pre>
<div class="table-wrap mt-4">
<table>
<thead>
<tr><th>Flag / env</th><th>Meaning</th></tr>
</thead>
<tbody>
<tr><td><code>--server</code> / <code>-s</code></td><td>Install peardock-server (Linux → <code>/opt/peardock</code> + systemd)</td></tr>
<tr><td><code>--client</code> / <code>-c</code></td><td>Install desktop client</td></tr>
<tr><td><code>--both</code> / <code>-b</code></td><td>Install both</td></tr>
<tr><td><code>--yes</code> / <code>-y</code></td><td>Non-interactive defaults</td></tr>
<tr><td><code>--tag rolling</code></td><td>Release tag (default: <code>rolling</code>)</td></tr>
<tr><td><code>PEARDOCK_SERVER_DIR</code></td><td>Server install path (default <code>/opt/peardock</code>)</td></tr>
<tr><td><code>PEARDOCK_VERSION</code></td><td>Pin asset version (default: auto from release)</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="container install-wide" style="margin: 0 auto 3rem;">
<div class="card">
<h3 class="mb-4">What the installer does</h3>
<h4 class="mt-4">Server (Linux)</h4>
<ul class="text-muted" style="font-size: 0.95rem; line-height: 1.7;">
<li>Downloads <code>peardock-server-VERSION-linux-ARCH.tar.gz</code> from the rolling release</li>
<li>Installs binary to <code>/opt/peardock/peardock-server</code></li>
<li>Creates system user <code>peardock</code> and adds it to the <strong>docker</strong> group (for <code>/var/run/docker.sock</code>)</li>
<li>Writes <code>/etc/systemd/system/peardock.service</code> and optionally enables it</li>
<li>State / seed in <code>/opt/peardock/.env</code> (mode 600)</li>
</ul>
<pre><code>sudo systemctl status peardock
sudo journalctl -u peardock -f
# Public key is printed on first start. Share it with clients.</code></pre>
<h4 class="mt-4">Client</h4>
<ul class="text-muted" style="font-size: 0.95rem; line-height: 1.7;">
<li>Downloads <code>peardock-client-VERSION-HOST.tar.gz</code></li>
<li><strong>macOS:</strong> <code>peardock.app</code> under <code>~/Applications</code> (clears quarantine via <code>xattr</code>)</li>
<li><strong>Linux:</strong> binary under <code>~/.local/share/peardock</code>, symlink in <code>~/.local/bin</code>, desktop entry</li>
</ul>
</div>
</div>
<div class="container">
<div class="download-grid">
<div class="download-card">
<h3>🖥 peardock-server</h3>
<p>Bare standalone HyperDHT control plane. Run on the host with Docker. Identity from <code>SERVER_SEED</code> / <code>.env</code>.</p>
<p class="text-muted" style="font-size: 0.85rem;">Asset pattern: <code>peardock-server-VERSION-HOST.tar.gz</code></p>
<div class="platform-list">
<div class="platform-row"><span>linux-x64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>linux-arm64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>darwin-x64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>darwin-arm64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>win32-x64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>win32-arm64</span><span class="badge-muted badge">.tar.gz</span></div>
</div>
</div>
<div class="download-card">
<h3>⌘ peardock-client</h3>
<p>Desktop GUI (Electron). Connect with the server public key. Multi-peer fleet, tunnels, terminals, deploy wizard.</p>
<p class="text-muted" style="font-size: 0.85rem;">Asset pattern: <code>peardock-client-VERSION-HOST.tar.gz</code></p>
<div class="platform-list">
<div class="platform-row"><span>linux-x64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>linux-arm64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>darwin-x64</span><span class="badge-muted badge">.app in tar</span></div>
<div class="platform-row"><span>darwin-arm64</span><span class="badge-muted badge">.app in tar</span></div>
<div class="platform-row"><span>win32-x64</span><span class="badge-muted badge">.tar.gz</span></div>
<div class="platform-row"><span>win32-arm64</span><span class="badge-muted badge">.tar.gz</span></div>
</div>
</div>
</div>
<div class="card" style="max-width: 900px; margin: 0 auto 4rem;">
<h3 class="mb-4">Manual download</h3>
<p class="text-muted mb-4" style="font-size: 0.95rem;">
Browse all assets on the
<a href="https://git.ssh.surf/snxraven/peardock/releases/tag/rolling" target="_blank" rel="noopener">rolling</a>
release. Each tarball has a sibling <code>.sha256</code> checksum.
</p>
<pre><code># Example: Linux x64 server
curl -fLO https://git.ssh.surf/snxraven/peardock/releases/download/rolling/peardock-server-2.0.1-linux-x64.tar.gz
curl -fLO https://git.ssh.surf/snxraven/peardock/releases/download/rolling/peardock-server-2.0.1-linux-x64.tar.gz.sha256
sha256sum -c peardock-server-*.sha256
tar -xzf peardock-server-*.tar.gz
# → peardock-server binary</code></pre>
</div>
<div class="card" style="max-width: 900px; margin: 0 auto 4rem;">
<h3 class="mb-4">Build from source</h3>
<pre><code>git clone https://git.ssh.surf/snxraven/peardock.git
cd peardock
npm ci
npm run server # control plane (needs Docker socket)
npm run start:client # Electron GUI
# or
npm run make # all 64-bit server + client hosts → out/</code></pre>
<p class="mt-4 text-muted" style="font-size: 0.92rem;">
Node.js ≥ 20 required. See <a href="docs/quickstart.html">Quick start</a> and
<a href="docs/operator.html">Operator guide</a> for production.
</p>
</div>
<div class="callout warn" style="max-width: 900px; margin: 0 auto 4rem;">
<div class="callout-icon">!</div>
<div class="callout-body">
<strong>macOS Gatekeeper.</strong> Darwin clients from Linux CI are ad-hoc signed.
If the app is blocked: right-click → Open, or
<code>xattr -cr ~/Applications/peardock.app</code>.
Developer ID re-sign: <code>npm run sign:macos -- out/peardock-darwin-arm64/peardock.app</code>
</div>
</div>
<div class="callout info" style="max-width: 900px; margin: 0 auto 4rem;">
<div class="callout-icon">i</div>
<div class="callout-body">
<strong>Docker access on the server.</strong> The service user <code>peardock</code> must reach
<code>/var/run/docker.sock</code> (usually group <code>docker</code>, mode 660).
The installer adds <code>peardock</code> to that group. If stats/start fail with permission errors:
<code>sudo usermod -aG docker peardock && sudo systemctl restart peardock</code>
(and restart Docker if the group was just created).
</div>
</div>
</div>
<div data-footer data-prefix=""></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+371
View File
@@ -0,0 +1,371 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>PearDock · Decentralized Docker management</title>
<meta name="description" content="PearDock is peer-to-peer Docker management on HyperDHT. Never open ports, expose Docker, or wrestle with remote access again. Noise-encrypted RPC, desktop client, and Holesail tunnels." />
<meta name="theme-color" content="#2dd4bf" />
<meta property="og:title" content="PearDock · Decentralized Docker management" />
<meta property="og:description" content="Docker ops without opening ports or VPN. Share a public key. Manage containers. Tunnel services. No SaaS, no inbound firewall rules." />
<meta property="og:image" content="/assets/brand/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicons/favicon-32x32.png" />
<link rel="apple-touch-icon" href="/assets/favicons/apple-touch-icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/home.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div class="bg-grid" aria-hidden="true"></div>
<div data-nav data-prefix=""></div>
<main>
<!-- HERO -->
<section class="hero">
<div class="container-wide hero-inner">
<div class="reveal">
<div class="hero-badge">
<span class="dot"></span>
v2 · HyperDHT · AGPL-3.0
</div>
<h1>
Docker ops without<br />
<span class="gradient-text">opening ports</span>
</h1>
<p class="lead">
Peer-to-peer Docker management on the Holepunch stack.
Share a public key, then manage containers, stacks, and tunnels over encrypted P2P.
<strong>Never open ports, expose Docker to the internet, or worry about remote access again.</strong>
No SaaS. No VPN. No inbound firewall rules.
</p>
<div class="hero-actions">
<a class="btn btn-primary btn-lg" href="/download">Install &amp; download</a>
<a class="btn btn-secondary btn-lg" href="/learn/">How it works</a>
<a class="btn btn-ghost btn-lg" href="/docs/quickstart">Quick start</a>
</div>
<div class="hero-meta">
<div class="hero-meta-item">
<strong>HyperDHT</strong>
<span>Transport</span>
</div>
<div class="hero-meta-item">
<strong>protomux-rpc</strong>
<span>RPC protocol</span>
</div>
<div class="hero-meta-item">
<strong>6 platforms</strong>
<span>Server + client</span>
</div>
<div class="hero-meta-item">
<strong>Holesail</strong>
<span>Port tunnels</span>
</div>
</div>
</div>
<div class="hero-visual reveal">
<div class="hero-glow" aria-hidden="true"></div>
<div class="hero-frame" data-lightbox="/assets/screenshots/screenshot-0.png">
<img src="/assets/screenshots/screenshot-0.png" alt="PearDock desktop client" width="960" height="600" />
</div>
<ul class="hero-status-bar" aria-label="Product highlights">
<li class="hero-status-item">
<span class="chip-dot" aria-hidden="true"></span>
Peer online
</li>
<li class="hero-status-item">
<span class="status-ico" aria-hidden="true">🔐</span>
Noise · E2E encrypted
</li>
<li class="hero-status-item">
<span class="status-ico" aria-hidden="true"></span>
No ports to open
</li>
</ul>
</div>
</div>
</section>
<!-- TECH STRIP -->
<div class="tech-strip">
<div class="container tech-strip-inner">
<div class="tech-item"><span></span> HyperDHT</div>
<div class="tech-item"><span></span> protomux-rpc</div>
<div class="tech-item"><span></span> compact-encoding</div>
<div class="tech-item"><span></span> dockerode</div>
<div class="tech-item"><span></span> Holesail</div>
<div class="tech-item"><span></span> Pear / Electron</div>
<div class="tech-item"><span></span> Bare runtime</div>
</div>
</div>
<!-- FEATURES -->
<section class="section" id="features">
<div class="container">
<div class="reveal">
<div class="section-label">Features</div>
<h2>Full Engine coverage.<br />Zero ports to open.</h2>
<p class="lead mt-4">
Containers, images, volumes, networks, stacks, Swarm, terminals, logs, GitOps,
registry vault, roles, and Holesail tunnels in a desktop console built for real ops.
Remote Docker without port forwards, public <code>dockerd</code>, or a permanent VPN.
</p>
</div>
<div class="features-grid">
<article class="card reveal">
<div class="card-icon"></div>
<h3>No ports. No VPN.</h3>
<p>Server identity is a HyperDHT keypair. Share the public key; peers holepunch. Nothing phones home, nothing listens on a public Docker port.</p>
</article>
<article class="card reveal">
<div class="card-icon">🔐</div>
<h3>Noise encryption</h3>
<p>Every control connection is end-to-end encrypted. Roles (viewer, operator, admin), allowlists, invites, and audit logging help multi-operator fleets stay safe.</p>
</article>
<article class="card reveal">
<div class="card-icon">🚢</div>
<h3>Docker ops console</h3>
<p>Deploy, recreate, prune, edit resources, browse volumes, manage stack env files, search Hub, handle secrets and configs, and run schedules. Built for real operators.</p>
</article>
<article class="card reveal">
<div class="card-icon"></div>
<h3>Holesail tunnels</h3>
<p>Reach published services over <code>hs://</code> keys with no inbound firewall rules. This is a separate data plane next to the RPC control plane.</p>
</article>
<article class="card reveal">
<div class="card-icon">🖥️</div>
<h3>Desktop client</h3>
<p>Pear and Electron shell with multi-peer fleet, job tray, connection health, live stats, terminals, and OTA-ready packaging.</p>
</article>
<article class="card reveal">
<div class="card-icon">📦</div>
<h3>Standalone binaries</h3>
<p>Cross-built server (Bare) and client (Electron) for linux, darwin, and win32 on x64 and arm64. Rolling releases come from CI.</p>
</article>
</div>
<div class="stats-row reveal">
<div class="stat"><strong>6</strong><span>64-bit host targets</span></div>
<div class="stat"><strong>v2</strong><span>HyperDHT + protomux-rpc</span></div>
<div class="stat"><strong>3</strong><span>ACL roles</span></div>
<div class="stat"><strong>0</strong><span>Ports to open</span></div>
</div>
</div>
</section>
<!-- ARCHITECTURE -->
<section class="section arch-section" id="architecture">
<div class="container">
<div class="reveal text-center" style="max-width: 640px; margin: 0 auto;">
<div class="section-label" style="justify-content: center;">Architecture</div>
<h2>Two planes. Still zero open ports.</h2>
<p class="lead" style="margin: 1rem auto 0;">
Control traffic uses HyperDHT and protomux-rpc. Service access uses Holesail.
Both are peer-to-peer. No inbound firewall rules. No public Docker API.
</p>
</div>
<div class="arch-diagram reveal">
<div class="arch-node">
<div class="node-icon"></div>
<h3>Desktop client</h3>
<p>Pear / Electron · multi-peer · jobs · terminals</p>
</div>
<div class="arch-arrow">
<svg viewBox="0 0 48 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h40M34 4l10 8-10 8"/></svg>
RPC
</div>
<div class="arch-node">
<div class="node-icon"></div>
<h3>peardock server</h3>
<p>HyperDHT listen · ACL · handlers · vault</p>
</div>
<div class="arch-arrow">
<svg viewBox="0 0 48 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h40M34 4l10 8-10 8"/></svg>
Socket
</div>
<div class="arch-node">
<div class="node-icon">🐳</div>
<h3>Docker Engine</h3>
<p>dockerode · compose · swarm</p>
</div>
</div>
<div class="arch-details reveal">
<div class="arch-detail">
<h4>Control plane</h4>
<p>Handshake, list/start/stop containers, deploy stacks, logs, stats pushes, terminal streams, registry vault, and peer policy. Protocol version is negotiated on connect.</p>
</div>
<div class="arch-detail">
<h4>Data / tunnels</h4>
<p>Holesail L4 proxy binds published container or host ports to <code>hs://</code> capabilities. Opt out with <code>ENABLE_HOLESAIL=0</code>.</p>
</div>
</div>
<div class="text-center mt-6 reveal">
<a class="btn btn-secondary" href="/learn/">How PearDock works →</a>
<a class="btn btn-ghost" href="/docs/architecture">Architecture reference →</a>
</div>
</div>
</section>
<!-- DEEP FEATURES -->
<section class="section">
<div class="container">
<div class="feature-row reveal">
<div class="feature-copy">
<div class="section-label">Fleet</div>
<h2>Multi-peer. Multi-host. One sidebar.</h2>
<p class="lead">Connect to many servers at once. Tag environments (prod / staging / lab). Switch contexts without losing job history or health signals.</p>
<ul class="feature-list">
<li>Peer disk cache under <code>~/.config/peardock</code></li>
<li>Connection health badge: latency, Docker status, role</li>
<li>Invites, revoke, allowlist for multi-operator setups</li>
<li>Registry credentials vault (AES-GCM from server seed)</li>
</ul>
</div>
<div class="feature-visual" data-lightbox="/assets/screenshots/screenshot-1.png">
<img src="/assets/screenshots/screenshot-1.png" alt="Fleet and peers UI" />
</div>
</div>
<div class="feature-row reverse reveal">
<div class="feature-copy">
<div class="section-label">Operations</div>
<h2>Deploy like you mean it.</h2>
<p class="lead">Compose stacks with env files and overrides. Recreate containers. Live resource editor. System prune. Scheduled maintenance. Volume file browser.</p>
<ul class="feature-list">
<li>GitOps-friendly stack deploy paths</li>
<li>Swarm services, nodes, secrets, configs, scale</li>
<li>Job tray with step logs for long-running ops</li>
<li>Destructive confirms and role-gated methods</li>
</ul>
</div>
<div class="feature-visual" data-lightbox="/assets/screenshots/screenshot-2.png">
<img src="/assets/screenshots/screenshot-2.png" alt="Deploy and operations UI" />
</div>
</div>
<div class="feature-row reveal">
<div class="feature-copy">
<div class="section-label">Tunnels</div>
<h2>Share a service, never open a port.</h2>
<p class="lead">
Create a Holesail tunnel from a container or host port. Copy the <code>hs://</code> URL.
Remote peers bind locally. You never punch a firewall hole or expose Docker to the internet.
</p>
<ul class="feature-list">
<li>No inbound port forwards. P2P hole punching instead.</li>
<li>Secure mode by default (capability ≠ raw listen key)</li>
<li>SSRF guard: loopback / allowlisted hosts only</li>
<li>Max concurrent tunnels · audited create/close</li>
</ul>
<a class="btn btn-ghost mt-4" href="docs/holesail.html">Holesail guide →</a>
</div>
<div class="feature-visual" data-lightbox="/assets/screenshots/screenshot-3.png">
<img src="/assets/screenshots/screenshot-3.png" alt="Holesail tunnels UI" />
</div>
</div>
</div>
</section>
<!-- SCREENSHOTS -->
<section class="section" id="screenshots" style="background: var(--bg-secondary); border-top: 1px solid var(--border);">
<div class="container-wide">
<div class="reveal">
<div class="section-label">Product</div>
<h2>See it in action</h2>
<p class="lead mt-4">Dark, dense, operator-first UI. Built for long sessions, not demos. Click any shot for a full-size view.</p>
</div>
<div class="shots-grid">
<div class="shot reveal" data-lightbox="/assets/screenshots/screenshot-0.png" role="button" tabindex="0" aria-label="Open PearDock overview screenshot">
<img src="/assets/screenshots/screenshot-0.png" alt="PearDock overview: dashboard and containers" loading="lazy" />
</div>
<div class="shot reveal" data-lightbox="/assets/screenshots/screenshot-4.png" role="button" tabindex="0" aria-label="Open PearDock detail view screenshot">
<img src="/assets/screenshots/screenshot-4.png" alt="PearDock detail view" loading="lazy" />
</div>
<div class="shot reveal" data-lightbox="/assets/screenshots/screenshot-5.png" role="button" tabindex="0" aria-label="Open PearDock logs and terminal screenshot">
<img src="/assets/screenshots/screenshot-5.png" alt="PearDock logs and terminal" loading="lazy" />
</div>
<div class="shot reveal" data-lightbox="/assets/screenshots/screenshot-2.png" role="button" tabindex="0" aria-label="Open PearDock deploy screenshot">
<img src="/assets/screenshots/screenshot-2.png" alt="PearDock deploy" loading="lazy" />
</div>
<div class="shot reveal" data-lightbox="/assets/screenshots/screenshot-1.png" role="button" tabindex="0" aria-label="Open PearDock peers screenshot">
<img src="/assets/screenshots/screenshot-1.png" alt="PearDock peers" loading="lazy" />
</div>
</div>
</div>
</section>
<!-- QUICK START TEASER -->
<section class="section" id="quickstart">
<div class="container">
<div class="reveal">
<div class="section-label">Quick start</div>
<h2>Install in one line. Connect in three steps.</h2>
<p class="lead mt-4">
No public IP gymnastics, no open ports, no VPN checklist.
Interactive installer pulls rolling binaries, wires Linux systemd + docker group for the server, or installs a desktop client.
</p>
</div>
<div class="card reveal install-card">
<pre><code>curl -fsSL https://install.peardock.boats | bash</code></pre>
<pre><code># Non-interactive
curl -fsSL https://install.peardock.boats | bash -s -- --server --yes
curl -fsSL https://install.peardock.boats | bash -s -- --client --yes</code></pre>
</div>
<div class="features-grid features-grid--steps">
<article class="card reveal">
<span class="badge mb-4">01</span>
<h3>Server online</h3>
<p class="mt-4">Run <code>systemctl status peardock</code>, then copy the <strong>public key</strong> from the journal.</p>
</article>
<article class="card reveal">
<span class="badge mb-4">02</span>
<h3>Launch the client</h3>
<p class="mt-4">Open <code>peardock.app</code> (macOS) or <code>peardock-client</code> (Linux). Or download from <a href="download.html">Download</a>.</p>
</article>
<article class="card reveal">
<span class="badge mb-4">03</span>
<h3>Connect</h3>
<p class="mt-4">Paste the 64-character public key. Peers auto-reconnect every 5s if the link drops.</p>
</article>
</div>
<div class="text-center mt-6 reveal">
<a class="btn btn-primary" href="docs/quickstart.html">Full quick start guide</a>
<a class="btn btn-secondary" href="download.html">Install &amp; download</a>
</div>
</div>
</section>
<!-- CTA -->
<section class="cta-band">
<div class="container">
<div class="cta-card reveal">
<div class="badge mb-4">Open source · AGPL-3.0</div>
<h2>Remote Docker without remote-access drama.</h2>
<p class="lead">
Never open ports or expose your Engine again. Just keys and peers.
Grab rolling binaries for six hosts, or build from source.
</p>
<div class="cta-actions">
<a class="btn btn-primary btn-lg" href="download.html">Download peardock</a>
<a class="btn btn-secondary btn-lg" href="docs/index.html">Browse docs</a>
</div>
</div>
</div>
</section>
</main>
<div data-footer data-prefix=""></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+496
View File
@@ -0,0 +1,496 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>How PearDock Works · P2P Docker control</title>
<meta name="description" content="Learn how PearDock manages Docker over peer-to-peer HyperDHT. No open ports, no VPN, no public Docker API. Identity, hole punching, control plane, Holesail tunnels, and security." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/learn.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<header class="learn-hero">
<div class="learn-hero-inner">
<div class="badge mb-4">learn · how it works</div>
<h1>Understanding peer-to-peer Docker control</h1>
<p class="lead">
PearDock is decentralized Docker management on the Holepunch stack.
<strong>You never need to open ports, expose Docker, or worry about remote access again.</strong>
You only need keys, peers, and the Engine you already run.
</p>
<div class="learn-key">
<div>
<strong>Key concept</strong>
Your laptop and a remote Docker host talk directly over an encrypted P2P stream.
No inbound firewall rules. No public <code>dockerd</code>. No always-on VPN.
Discovery and NAT traversal use HyperDHT. Docker commands ride protomux-rpc on that stream.
</div>
</div>
</div>
</header>
<div class="container learn-page">
<ul class="learn-toc">
<li><a href="#why">Why P2P?</a></li>
<li><a href="#problem">Old model</a></li>
<li><a href="#identity">Identity</a></li>
<li><a href="#connect">Connecting</a></li>
<li><a href="#nat">NAT &amp; hole punching</a></li>
<li><a href="#control">Control plane</a></li>
<li><a href="#data">Data plane</a></li>
<li><a href="#security">Security</a></li>
<li><a href="#fleet">Fleet</a></li>
<li><a href="#flow">End-to-end</a></li>
<li><a href="#glossary">Glossary</a></li>
</ul>
<!-- WHY -->
<section class="learn-section" id="why">
<h2>Why peer-to-peer for Docker?</h2>
<p class="learn-lead">
Docker Engine is powerful locally. Remotely, people usually open a TLS API port, maintain a VPN, or enroll hosts in a hosted panel.
PearDock takes another path: treat the host as a peer you reach with a public key.
<strong>No open ports, no exposed Engine, no remote-access checklist that never ends.</strong>
</p>
<p>
You can manage a home lab behind CGNAT, a VPS without punching firewall holes, or a fleet of edge machines.
Peers need to complete HyperDHT hole punching (or a relay fallback), and you need the right key material.
</p>
</section>
<!-- PROBLEM -->
<section class="learn-section" id="problem">
<h2>The traditional remote-Docker model</h2>
<p class="learn-lead">Most “remote Docker” setups trade convenience for a large attack surface or a permanent middleman.</p>
<div class="learn-compare">
<div class="learn-card learn-card--old">
<h4>✕ Centralized / exposed</h4>
<ul>
<li>Expose <code>dockerd</code> TCP + TLS to the internet</li>
<li>Always-on VPN just to run <code>docker ps</code></li>
<li>Hosted panel with your hosts enrolled forever</li>
<li>Single vendor outage = no ops UI</li>
</ul>
</div>
<div class="learn-card learn-card--new">
<h4>✓ PearDock peer model</h4>
<ul>
<li>Never open inbound ports for Docker or the control UI</li>
<li>Clients dial the <strong>public key</strong> over HyperDHT</li>
<li>Noise-encrypted RPC end to end. No VPN required.</li>
<li>You keep Docker and state on your machines</li>
</ul>
</div>
</div>
<pre class="mermaid">
flowchart LR
subgraph Traditional["Traditional"]
A1[Laptop] --> VPN[VPN / open :2376 / SaaS]
VPN --> D1[dockerd]
end
subgraph PearDock["PearDock"]
A2[Desktop client] -- "HyperDHT + Noise + protomux-rpc" --> S2[peardock-server]
S2 --> D2[dockerd]
end
</pre>
</section>
<!-- IDENTITY -->
<section class="learn-section" id="identity">
<h2>Identity: seeds and public keys</h2>
<p class="learn-lead">
PearDock v2 identity is a HyperDHT keypair derived from a secret seed.
It is not a shared “topic string” like some older swarm designs.
</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>Material</th><th>Who has it</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>SERVER_SEED</code></td>
<td>Server only (secret)</td>
<td>32-byte seed; generates the listening keypair. Never share.</td>
</tr>
<tr>
<td><code>SERVER_PUBLIC_KEY</code></td>
<td>Operators / clients</td>
<td>64 hex chars. Paste into the client to dial this host.</td>
</tr>
<tr>
<td>Client keys</td>
<td>Each desktop install</td>
<td>Peer identity for ACL, invites, and reconnect cache.</td>
</tr>
</tbody>
</table>
</div>
<p>
On first start the server prints the public key (also in the journal when using systemd).
Rotating the seed changes the public key. All clients must re-add the peer.
Back up <code>/opt/peardock/.env</code> (or your dev <code>.env</code>) like any other root secret.
</p>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<strong>Not the same as a Holesail <code>hs://</code> URL.</strong>
The server public key is for PearDock <em>control</em> RPC.
Tunnel capabilities are separate secrets created per published port.
</div>
</div>
</section>
<!-- CONNECT -->
<section class="learn-section" id="connect">
<h2>How a client finds and connects to a server</h2>
<p class="learn-lead">
Under the hood this is classic Holepunch connectivity: announce, look up, hole-punch (or relay), then encrypt.
PearDocks application layer starts only after that secure stream exists.
</p>
<div class="learn-peers">
<div class="learn-peer">
<h4>Client</h4>
<p>Desktop app holds public key + optional invite</p>
</div>
<div class="learn-arrow"><span></span>lookup</div>
<div class="learn-peer learn-peer--hub">
<h4>HyperDHT</h4>
<p>Distributed peer discovery &amp; signaling</p>
</div>
<div class="learn-arrow"><span></span>announce</div>
<div class="learn-peer">
<h4>Server</h4>
<p>Listens on keypair next to Docker</p>
</div>
</div>
<div class="learn-steps">
<div class="learn-step">
<h4>Announce</h4>
<p>The server registers its presence on HyperDHT under its cryptographic identity so dialers can find a path.</p>
</div>
<div class="learn-step">
<h4>Lookup &amp; punch</h4>
<p>The client queries the DHT, then both sides attempt coordinated UDP probes so NAT mappings open (“hole punching”).</p>
</div>
<div class="learn-step">
<h4>Encrypt &amp; RPC</h4>
<p>A Noise-encrypted stream carries protomux-rpc methods and server→client pushes (stats, logs, events).</p>
</div>
</div>
<pre class="mermaid">
sequenceDiagram
participant C as Client
participant DHT as HyperDHT
participant S as peardock-server
participant D as dockerd
S->>DHT: announce identity
C->>DHT: lookup public key
DHT-->>C: candidate endpoints
C->>S: hole punch / connect
C->>S: Noise handshake
C->>S: protomux-rpc handshake
S-->>C: role · protocol version
C->>S: listContainers / deploy / …
S->>D: dockerode
D-->>S: result
S-->>C: response + optional pushes
</pre>
</section>
<!-- NAT -->
<section class="learn-section" id="nat">
<h2>NAT traversal &amp; hole punching</h2>
<p class="learn-lead">
Most devices sit behind Network Address Translation. Routers map private IPs to public ports and block unsolicited inbound traffic.
That is why “just open Docker on the internet” is a bad default, and why P2P needs help to dial home.
</p>
<h3>What hole punching does</h3>
<ol>
<li><strong>Both peers learn public endpoints</strong> via DHT-assisted signaling (not a single app-specific rendezvous you host).</li>
<li><strong>Simultaneous outbound probes</strong> create temporary NAT mappings toward each other.</li>
<li><strong>Direct path preferred</strong> when mappings cooperate (common on home NAT).</li>
<li><strong>Relay fallback</strong> may apply when both sides are hard symmetric NAT. Traffic stays encrypted. Relays do not get plaintext Docker commands.</li>
</ol>
<div class="learn-compare">
<div class="learn-card">
<h4>Easier NAT</h4>
<p>Open / consistent mappings → hole punching usually succeeds quickly. Typical home routers often work well.</p>
</div>
<div class="learn-card">
<h4>Harder NAT / CGNAT</h4>
<p>Symmetric NAT or carrier-grade NAT can block direct paths. The stack may need more probes or a relay. Still no need to expose dockerd publicly.</p>
</div>
</div>
<p>
PearDock benefits from the same Holepunch/libudx generation of tools that power Hyperswarm and Holesail.
You dont configure port forwards for the control channel. You share a key.
That is the whole remote-access story: no open ports, no exposed Engine, no perpetual VPN.
</p>
</section>
<!-- CONTROL PLANE -->
<section class="learn-section" id="control">
<h2>Control plane: protomux-rpc over HyperDHT</h2>
<p class="learn-lead">
Once the encrypted stream is up, PearDock speaks a structured RPC protocol, not ad-hoc JSON lines.
That is how the UI stays rich: typed methods, roles, and live pushes.
</p>
<div class="learn-stack">
<div class="learn-layer">
<div class="learn-layer-name">UI</div>
<p>Desktop client (Electron / Pear) for containers, stacks, Swarm, tunnels, terminals, and jobs.</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">protomux-rpc</div>
<p>Request/response methods + push channels. <code>PROTOCOL_VERSION</code> negotiated on connect.</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">HyperDHT</div>
<p>Peer discovery, hole punch coordination, Noise transport under the app protocol.</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">handlers</div>
<p>Server modules for containers, images, volumes, networks, stacks, Swarm, vault, tunnels…</p>
</div>
<div class="learn-layer">
<div class="learn-layer-name">dockerode</div>
<p>Talks to local <code>dockerd</code> (usually the unix socket; server user in the <code>docker</code> group).</p>
</div>
</div>
<h3>Client → server (examples)</h3>
<ul>
<li><code>handshake</code>, <code>ping</code>: session setup and liveness</li>
<li><code>listContainers</code>, <code>startContainer</code>, <code>stopContainer</code>, <code>recreateContainer</code></li>
<li><code>deployContainer</code>, <code>deployStack</code>, <code>syncStackFromGit</code></li>
<li><code>getContainerLogs</code>, <code>startTerminal</code>, <code>containerStats</code></li>
<li><code>systemPrune</code>, Swarm methods, vault, schedules, tunnels</li>
</ul>
<h3>Server → client pushes</h3>
<ul>
<li><code>push:containers</code>, <code>push:allStats</code>: live fleet view without spam polling</li>
<li><code>push:logs</code>, <code>push:terminalOutput</code>: streaming operator surfaces</li>
<li><code>push:dockerEvent</code> and pull/build progress: keep the UI honest when the Engine changes</li>
</ul>
<p>
Method ↔ role maps live in the shared protocol. Viewers can observe; operators mutate day-to-day;
admins get destructive and policy-level actions. See
<a href="/docs/api">RPC &amp; API</a> and <a href="/docs/security">Security</a>.
</p>
</section>
<!-- DATA PLANE -->
<section class="learn-section" id="data">
<h2>Data plane: Holesail tunnels (optional traffic path)</h2>
<p class="learn-lead">
Managing Docker is not the same as reaching a published web app or TCP service.
PearDock keeps a second plane for that: Holesail L4 tunnels.
Still <strong>no open host ports</strong>. Share an <code>hs://</code> key instead of a firewall rule.
</p>
<pre class="mermaid">
flowchart LR
subgraph Control["Control plane"]
UI[Client RPC]
SRV[Server handlers]
UI --> SRV
SRV --> DK[Docker Engine]
end
subgraph Data["Data plane"]
HC[Holesail client]
HS[HolesailServer]
HC -- "hs:// capability" --> HS
HS --> Port["host:published-port"]
end
Control -.->|"orthogonal"| Data
</pre>
<div class="table-wrap">
<table>
<thead>
<tr><th>Plane</th><th>Technology</th><th>Carries</th></tr>
</thead>
<tbody>
<tr>
<td><strong>Control</strong></td>
<td>HyperDHT + protomux-rpc</td>
<td>Docker ops, ACL, logs stream control, deploy, stats</td>
</tr>
<tr>
<td><strong>Data / tunnels</strong></td>
<td>Holesail</td>
<td>Raw TCP/UDP bytes to a published host port via <code>hs://</code></td>
</tr>
</tbody>
</table>
</div>
<p>
<strong>Why not use only Holesail?</strong>
Holesail moves bytes between sockets. It is not an RPC framework, role system, or audit log.
PearDock needs structured methods and live UI channels, so control stays on protomux-rpc.
</p>
<p>
Tunnels are <strong>on by default</strong> (<code>ENABLE_HOLESAIL=0</code> to disable).
Treat every <code>hs://</code> URL as a capability secret. Details:
<a href="/docs/holesail">Holesail guide</a>.
</p>
</section>
<!-- SECURITY -->
<section class="learn-section" id="security">
<h2>Security model (operator mental model)</h2>
<p class="learn-lead">P2P does not mean “open to the world.” It means “reachable by those who hold the right cryptographic handles.”</p>
<ul>
<li><strong>Transport encryption:</strong> Noise on the HyperDHT stream, not cleartext Docker API on :2375.</li>
<li><strong>Roles:</strong> viewer, operator, and admin gate methods server-side.</li>
<li><strong>Peer policy:</strong> optional allowlists, invites, and revoke for multi-operator fleets without sharing root SSH.</li>
<li><strong>Rate limits and audit:</strong> blunt force protection and accountability for privileged calls.</li>
<li><strong>Registry vault:</strong> credentials encrypted at rest (AES-GCM keyed from server material).</li>
<li><strong>Tunnel SSRF guard:</strong> default allowlist for tunnel targets (loopback and published binds).</li>
<li><strong>Browse roots:</strong> host filesystem browse is default-deny and constrained.</li>
</ul>
<p>
You still must secure Docker itself: who is in the <code>docker</code> group, what images you run, and how secrets enter containers.
PearDock is a control surface. It inherits the power of the Engine socket it uses.
</p>
<p>Deep dive: <a href="/docs/security">Threat model &amp; hardening</a> · <a href="/legal/security">Vulnerability reporting</a></p>
</section>
<!-- FLEET -->
<section class="learn-section" id="fleet">
<h2>Multi-peer fleet UX</h2>
<p class="learn-lead">
One desktop client can keep many servers. Each peer is a separate HyperDHT identity with its own role and health.
</p>
<ul>
<li>Disk-backed peer cache under the client config directory</li>
<li>Auto-reconnect (retry cadence when a peer drops)</li>
<li>Environment tags (prod / staging / lab) for mental models</li>
<li>Job tray and notifications scoped to operator workflows</li>
</ul>
<p>
There is no “log into PearDock cloud.” There is only: which public keys did you add, and which of those peers are online.
</p>
</section>
<!-- END TO END -->
<section class="learn-section" id="flow">
<h2>End-to-end: first session</h2>
<p class="learn-lead">From zero to a live containers list. This is the path most operators take.</p>
<pre class="mermaid">
flowchart TB
A[Install peardock-server on Docker host] --> B[Read public key from journal]
B --> C[Install desktop client]
C --> D[Add peer · paste public key]
D --> E[Handshake · role · protocol version]
E --> F[listContainers · stats pushes · manage]
F --> G{Need app traffic?}
G -->|Yes| H[Create Holesail tunnel · share hs://]
G -->|No| I[Stay on control plane only]
</pre>
<ol>
<li>Install the server (one-liner or binary) on a machine with Docker.</li>
<li>Copy the <strong>public key</strong> from logs / journal.</li>
<li>Install the client; add the peer; wait for health (latency, Docker ok, role).</li>
<li>Operate: deploy, logs, terminal, stacks, Swarm, prune, schedules…</li>
<li>Optionally tunnel a published port with Holesail for users outside the control path.</li>
</ol>
<p>
Install: <a href="/download">Download &amp; install</a> ·
Guide: <a href="/docs/quickstart">Quick start</a> ·
Binaries: <a href="/releases">Releases</a>
</p>
</section>
<!-- GLOSSARY -->
<section class="learn-section" id="glossary">
<h2>Glossary</h2>
<div class="learn-glossary">
<details>
<summary>HyperDHT</summary>
<p>Distributed hash table used for peer announcements, lookups, and hole-punch coordination. Peers find each other by cryptographic identity without PearDock hosting a central directory of your fleet.</p>
</details>
<details>
<summary>Noise / Secret Stream</summary>
<p>Encrypted, mutually authenticated transport under HyperDHT connections. Application data (RPC) is not sent in the clear across the peer path.</p>
</details>
<details>
<summary>protomux-rpc</summary>
<p>RPC framework PearDock uses for named methods and server-initiated push channels over a multiplexed stream.</p>
</details>
<details>
<summary>Hole punching</summary>
<p>Technique where two NATed peers send coordinated outbound packets so temporary firewall mappings allow a direct path. If both sides are hostile NAT, encrypted relays may assist.</p>
</details>
<details>
<summary>NAT / CGNAT</summary>
<p>Network Address Translation (and carrier-grade NAT) share public IPs and block unsolicited inbound connections. That is why “open port 2376” is painful and unsafe as a default.</p>
</details>
<details>
<summary>Public key vs SERVER_SEED</summary>
<p>The seed is secret server identity material. The public key is what clients dial. Sharing the seed is equivalent to handing over the server identity.</p>
</details>
<details>
<summary>Holesail / hs://</summary>
<p>Peer-to-peer L4 reverse proxy. An <code>hs://</code> URL is a capability to reach a specific tunnel endpoint. It is separate from PearDocks Docker control key.</p>
</details>
<details>
<summary>dockerode</summary>
<p>Node.js Docker client used by the PearDock server to talk to the local Docker Engine API (typically via the unix socket).</p>
</details>
<details>
<summary>Role (viewer / operator / admin)</summary>
<p>Server-enforced permission tier for RPC methods. UI may hide actions, but the server is the authority.</p>
</details>
<details>
<summary>Rolling release</summary>
<p>Continuously updated binary channel used by the installer and <a href="/releases">releases</a> page. Recommended for current peardock-server and peardock-client builds.</p>
</details>
</div>
</section>
<div class="learn-cta">
<h2>Ready to try it?</h2>
<p>Install a server next to Docker, paste the public key into the client, and operate without a central control plane.</p>
<div class="learn-cta-actions">
<a class="btn btn-primary" href="/download">Install PearDock</a>
<a class="btn btn-secondary" href="/docs/architecture">Architecture docs</a>
<a class="btn btn-ghost" href="/docs/">All documentation</a>
</div>
</div>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+88
View File
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Acceptable Use Policy · PearDock Legal</title>
<meta name="description" content="Acceptable Use Policy for PearDock and HoneyPeer, LLC services." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>Acceptable Use Policy</h1>
<p class="docs-lead">
This Acceptable Use Policy (“<strong>AUP</strong>”) applies to use of HoneyPeer Sites, installers, and PearDock software
in connection with HoneyPeer-operated infrastructure or distribution channels.
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<h2 id="purpose">1. Purpose</h2>
<p>
PearDock is a powerful operations tool for Docker. With power comes responsibility.
This AUP describes prohibited uses so that operators, networks, and the open-source community remain safe.
</p>
<h2 id="prohibited">2. Prohibited activities</h2>
<p>You may not use the Sites, installers, or Software to:</p>
<ul>
<li><strong>Violate law</strong>, including computer crime, fraud, sanctions evasion, or unlawful surveillance;</li>
<li><strong>Attack systems</strong> by scanning, exploiting, or accessing systems, networks, or accounts without authorization;</li>
<li><strong>Distribute malware</strong> such as ransomware, trojans, botnet C2, or other malicious code via containers, images, or tunnels;</li>
<li><strong>Abuse tunnels</strong> by using Holesail or similar tunnels to conceal attacks, spam, phishing, or copyright infringement at scale;</li>
<li><strong>Interfere with HoneyPeer infrastructure</strong> through DDoS, credential stuffing, scraping that degrades service, or bypassing rate limits;</li>
<li><strong>Misrepresent identity</strong> by impersonating HoneyPeer, PearDock, or another person or entity;</li>
<li><strong>Infringe IP</strong> by hosting or distributing content you do not have rights to through systems managed with PearDock, in a way that unlawfully uses our distribution channels;</li>
<li><strong>Mine cryptocurrency on third-party systems</strong> without authorization from the system owner;</li>
<li><strong>Harass or harm</strong> by threatening, stalking, or encouraging violence.</li>
</ul>
<h2 id="operator">3. Self-hosted operator duties</h2>
<p>
When you self-host PearDock, you are the operator of that instance.
You must secure peer keys, Docker sockets, and network exposure.
HoneyPeer is not responsible for how you configure or expose your infrastructure.
</p>
<h2 id="enforcement">4. Enforcement</h2>
<p>
HoneyPeer may investigate suspected violations involving our Sites or distribution systems and may suspend access,
block IPs, remove materials, or report to law enforcement.
For self-hosted deployments, enforcement may be limited to denying access to HoneyPeer-operated download or documentation properties.
</p>
<h2 id="reporting">5. Reporting abuse</h2>
<p>
Report abuse of HoneyPeer Sites or brand misuse to
<a href="mailto:[email protected]">[email protected]</a>.
Copyright notices: see <a href="/legal/copyright">Copyright &amp; DMCA</a>.
</p>
<h2 id="changes">6. Changes</h2>
<p>We may update this AUP by posting a revised version with a new “Last updated” date.</p>
<div class="docs-pager">
<a href="/legal/license"><span>Previous</span><strong>← Open Source</strong></a>
<a class="next" href="/legal/copyright"><span>Next</span><strong>Copyright &amp; DMCA →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Cookie Policy → Privacy Policy · PearDock Legal</title>
<meta name="description" content="Cookie information is part of the PearDock Privacy Policy." />
<meta name="robots" content="noindex" />
<meta http-equiv="refresh" content="0;url=/legal/privacy#cookies" />
<link rel="canonical" href="/legal/privacy#cookies" />
<script>location.replace('/legal/privacy#cookies')</script>
</head>
<body>
<p>
Cookie information is included in the
<a href="/legal/privacy#cookies">Privacy Policy</a>.
</p>
</body>
</html>
+93
View File
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Copyright &amp; DMCA · PearDock Legal</title>
<meta name="description" content="Copyright ownership and DMCA notice procedure for PearDock / HoneyPeer, LLC." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>Copyright &amp; DMCA</h1>
<p class="docs-lead">
Copyright in PearDock product materials and HoneyPeer-operated Sites is held by
<strong>HoneyPeer, LLC</strong>, except open-source code licensed under AGPL-3.0 and third-party works.
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<h2 id="ownership">1. Copyright ownership</h2>
<p>
© 2024-<span data-year></span> HoneyPeer, LLC and contributors.
Website content, documentation layout, original graphics, and brand materials are protected by copyright.
Software source is additionally licensed under AGPL-3.0 as described in
<a href="/legal/license">Open Source Notices</a>.
</p>
<h2 id="permission">2. Permission</h2>
<p>
You may quote short excerpts of documentation with attribution for non-misleading technical purposes.
For republication of substantial documentation or brand assets, contact
<a href="mailto:[email protected]">[email protected]</a>.
</p>
<h2 id="dmca">3. DMCA / copyright complaints</h2>
<p>
If you believe content on a HoneyPeer-operated Site infringes your copyright, send a notice that includes:
</p>
<ol>
<li>Your physical or electronic signature;</li>
<li>Identification of the copyrighted work claimed to be infringed;</li>
<li>Identification of the material that is claimed to be infringing and information reasonably sufficient to locate it (URL);</li>
<li>Your contact information (address, telephone, email);</li>
<li>A statement that you have a good-faith belief that use of the material is not authorized by the copyright owner, its agent, or the law;</li>
<li>A statement that the information in the notification is accurate, and under penalty of perjury, that you are authorized to act on behalf of the owner.</li>
</ol>
<p>
<strong>Designated agent (email):</strong>
<a href="mailto:[email protected]">[email protected]</a><br />
<strong>Entity:</strong> HoneyPeer, LLC, DeKalb County, Georgia, USA
</p>
<p>
Upon valid notice, HoneyPeer may remove or disable access to the material and, where applicable, notify the poster.
Counter-notification procedures under the DMCA may apply.
</p>
<h2 id="repeat">4. Repeat infringers</h2>
<p>
HoneyPeer may terminate access to Sites or distribution channels for users who are repeat infringers, where accounts or access controls exist.
</p>
<h2 id="selfhost">5. Self-hosted content</h2>
<p>
Content hosted on systems you operate with PearDock is your responsibility.
HoneyPeer generally cannot remove content from infrastructure we do not control.
Send notices about HoneyPeer-operated Sites only to the address above.
</p>
<div class="docs-pager">
<a href="/legal/aup"><span>Previous</span><strong>← Acceptable Use</strong></a>
<a class="next" href="/legal/disclaimer"><span>Next</span><strong>Disclaimer →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Disclaimer &amp; Liability · PearDock Legal</title>
<meta name="description" content="Warranty disclaimer and limitation of liability for PearDock by HoneyPeer, LLC." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>Disclaimer &amp; Limitation of Liability</h1>
<p class="docs-lead">
Important risk disclosures for PearDock software and documentation published by HoneyPeer, LLC.
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<div class="callout warn">
<div class="callout-icon">!</div>
<div class="callout-body">
<strong>Infrastructure software.</strong>
PearDock can control Docker engines, open peer connections, and expose ports via tunnels.
Misconfiguration can cause data loss, downtime, or unauthorized access. Use at your own risk, especially in production.
</div>
</div>
<h2 id="as-is">1. “AS IS” software and documentation</h2>
<p>
PEARDOCK SOFTWARE, DOCUMENTATION, INSTALLERS, WEBSITES, AND RELATED MATERIALS ARE PROVIDED BY HONEYPEER, LLC
AND CONTRIBUTORS “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY,
INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, QUIET ENJOYMENT,
ACCURACY, AND NON-INFRINGEMENT.
</p>
<p>
This is consistent with the AGPL-3.0 disclaimer for open-source distributions and applies to the fullest extent permitted by law for HoneyPeers Sites and binary distributions.
</p>
<h2 id="no-guarantee">2. No operational guarantees</h2>
<p>Without limiting the foregoing, HoneyPeer does not warrant that:</p>
<ul>
<li>PearDock will meet your requirements or be compatible with every Docker version or platform;</li>
<li>peer connectivity, HyperDHT routing, or Holesail tunnels will be available, confidential, or uninterrupted;</li>
<li>defects will be corrected within any timeframe;</li>
<li>documentation is complete or error-free.</li>
</ul>
<h2 id="your-risk">3. Your risk allocation</h2>
<p>You are solely responsible for:</p>
<ul>
<li>testing in non-production environments before production use;</li>
<li>access control, key management, backups, and monitoring;</li>
<li>compliance with your organizations security policies and applicable law;</li>
<li>consequences of granting peers Docker-level access via PearDock roles.</li>
</ul>
<h2 id="liability">4. Limitation of liability</h2>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT WILL HONEYPEER, LLC, ITS MEMBERS, OFFICERS, EMPLOYEES, AGENTS, OR CONTRIBUTORS BE LIABLE FOR ANY
INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES; OR ANY LOSS OF PROFITS, REVENUE, DATA, GOODWILL, OR BUSINESS INTERRUPTION;
OR COST OF SUBSTITUTE GOODS OR SERVICES; ARISING OUT OF OR RELATED TO PEARDOCK OR HONEYPEER SITES, WHETHER BASED IN CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, OR OTHERWISE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
</p>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, HONEYPEERS TOTAL AGGREGATE LIABILITY FOR ALL CLAIMS RELATING TO PEARDOCK OR THE SITES SHALL NOT EXCEED THE GREATER OF
(A) THE AMOUNTS YOU PAID TO HONEYPEER FOR THE SPECIFIC PRODUCT OR SERVICE GIVING RISE TO THE CLAIM DURING THE TWELVE (12) MONTHS PRIOR TO THE CLAIM, OR
(B) ONE HUNDRED U.S. DOLLARS (US&nbsp;$100).
</p>
<p>
Because some jurisdictions do not allow certain exclusions, some of the above may not apply to you; in that case, liability is limited to the minimum extent required by law.
</p>
<h2 id="high-risk">5. High-risk activities</h2>
<p>
The Software is not designed for use in hazardous environments requiring fail-safe performance (for example nuclear facilities, air traffic control, or life-support systems)
where failure could lead to death, personal injury, or severe environmental damage. You assume all risk of such use.
</p>
<h2 id="related">6. Related documents</h2>
<ul>
<li><a href="/legal/eula">End User License Agreement</a></li>
<li><a href="/legal/terms">Terms of Service</a></li>
<li><a href="/legal/license">Open Source Notices (AGPL-3.0)</a></li>
</ul>
<h2 id="contact">7. Contact</h2>
<p>
HoneyPeer, LLC · DeKalb County, Georgia, USA<br />
<a href="mailto:[email protected]">[email protected]</a>
</p>
<div class="docs-pager">
<a href="/legal/copyright"><span>Previous</span><strong>← Copyright</strong></a>
<a class="next" href="/legal/security"><span>Next</span><strong>Security Policy →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+182
View File
@@ -0,0 +1,182 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>End User License Agreement · PearDock Legal</title>
<meta name="description" content="End User License Agreement for PearDock software distributed by HoneyPeer, LLC." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>End User License Agreement</h1>
<p class="docs-lead">
This End User License Agreement (“<strong>EULA</strong>”) is a legal agreement between you and
<strong>HoneyPeer, LLC</strong> (“HoneyPeer”) for PearDock software products distributed by HoneyPeer,
including server binaries, desktop clients, installers, and related documentation (the “<strong>Software</strong>”).
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<nav class="toc">
<strong>On this page</strong>
<ol>
<li><a href="#acceptance">Acceptance</a></li>
<li><a href="#license">License grant</a></li>
<li><a href="#oss">Open-source components</a></li>
<li><a href="#restrictions">Restrictions</a></li>
<li><a href="#ownership">Ownership</a></li>
<li><a href="#updates">Updates</a></li>
<li><a href="#data">Your data &amp; infrastructure</a></li>
<li><a href="#third">Third-party software</a></li>
<li><a href="#support">Support</a></li>
<li><a href="#term">Term &amp; termination</a></li>
<li><a href="#export">Export</a></li>
<li><a href="#warranty">Warranty disclaimer</a></li>
<li><a href="#liability">Limitation of liability</a></li>
<li><a href="#law">Governing law</a></li>
<li><a href="#contact">Contact</a></li>
</ol>
</nav>
<h2 id="acceptance">1. Acceptance</h2>
<p>
By downloading, installing, copying, or using the Software, you agree to this EULA.
If you do not agree, do not install or use the Software.
If you accept on behalf of an entity, you represent that you are authorized to bind that entity.
</p>
<h2 id="license">2. License grant</h2>
<p>
Subject to this EULA, HoneyPeer grants you a limited, non-exclusive, non-transferable (except as permitted below),
revocable license to install and use the Software on systems you own or control, for lawful purposes,
including managing Docker environments over peer-to-peer connections as documented.
</p>
<p>
Where the Software (or portions of it) is also provided under AGPL-3.0 in source form,
your rights under AGPL-3.0 for that source remain available and are not narrowed by this EULA.
This EULA primarily governs HoneyPeers distribution of binaries, installers, trademarks, and packaging.
</p>
<h2 id="oss">3. Open-source components</h2>
<p>
PearDock is engineered as open-source software under the
<strong>GNU Affero General Public License, Version 3 (AGPL-3.0)</strong>, matching Holesail.
The Software may include third-party open-source components under their own licenses
(see <a href="/legal/license">Open Source Notices</a>). Those licenses govern those components.
Holesail-related tunnel functionality is also <strong>AGPL-3.0</strong>.
</p>
<h2 id="restrictions">4. Restrictions</h2>
<p>Except as allowed by applicable open-source licenses or mandatory law, you may not:</p>
<ul>
<li>rent, lease, sell, sublicense, or provide the Software as a hosted multi-tenant service that conceals HoneyPeers trademarks in a misleading way;</li>
<li>remove or alter proprietary notices, copyright legends, or attribution required by licenses;</li>
<li>use the Software to violate law, third-party rights, or the <a href="/legal/aup">Acceptable Use Policy</a>;</li>
<li>attempt to bypass security, authentication, or access controls of systems you do not own or administer;</li>
<li>misrepresent the Software as officially endorsed by another party, or misuse HoneyPeer/PearDock marks in a misleading way.</li>
</ul>
<h2 id="ownership">5. Ownership</h2>
<p>
HoneyPeer and its licensors retain all right, title, and interest in and to the Software,
including intellectual property rights, subject to open-source licenses applicable to specific components.
This EULA does not transfer ownership of the Software to you.
</p>
<h2 id="updates">6. Updates and versions</h2>
<p>
HoneyPeer may provide updates, rolling releases, or patches. Updates may be required for continued compatibility.
HoneyPeer is not obligated to provide any particular update schedule.
Pre-release, experimental, or “rolling” builds may be unstable.
</p>
<h2 id="data">7. Your data and infrastructure</h2>
<p>
The Software operates on infrastructure and networks you control or connect to.
You are solely responsible for Docker hosts, containers, secrets, peer keys, tunnels, logs, and any data processed by systems you manage with PearDock.
HoneyPeer does not claim ownership of your content or container workloads.
</p>
<p>
Unless you separately enable a HoneyPeer online service that collects data (described in the
<a href="/legal/privacy">Privacy Policy</a>), local use of the Software does not require sending operational Docker data to HoneyPeer.
</p>
<h2 id="third">8. Third-party software and services</h2>
<p>
The Software may interact with Docker Engine, operating systems, network services, and optional tunnel libraries.
Those products are governed by their own terms and licenses. HoneyPeer is not responsible for third-party software behavior.
</p>
<h2 id="support">9. Support</h2>
<p>
Unless you have a separate written support agreement with HoneyPeer, the Software is provided without guaranteed support, SLA, or uptime commitment.
Community documentation may be available on the Sites.
</p>
<h2 id="term">10. Term and termination</h2>
<p>
This EULA remains in effect until terminated. Your rights terminate automatically if you materially breach this EULA and fail to cure where cure is reasonably possible.
Upon termination, you must stop using HoneyPeer-distributed proprietary packaging that is not independently licensed to you under open source, and destroy copies as required.
Open-source licenses that have already been granted for source code survive according to their terms.
</p>
<h2 id="export">11. Export and sanctions</h2>
<p>
You must comply with U.S. and other applicable export control and sanctions laws.
You may not use or export the Software in violation of those laws.
</p>
<h2 id="warranty">12. Warranty disclaimer</h2>
<p>
THE SOFTWARE IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND.
TO THE MAXIMUM EXTENT PERMITTED BY LAW, HONEYPEER DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED,
INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
YOU ASSUME ALL RISK FOR USE OF THE SOFTWARE IN PRODUCTION OR CRITICAL ENVIRONMENTS.
</p>
<h2 id="liability">13. Limitation of liability</h2>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, HONEYPEER WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES,
OR LOSS OF PROFITS, DATA, OR BUSINESS, ARISING FROM THE SOFTWARE OR THIS EULA.
HONEYPEERS TOTAL LIABILITY WILL NOT EXCEED THE GREATER OF FEES YOU PAID TO HONEYPEER FOR THE SOFTWARE IN THE TWELVE (12) MONTHS BEFORE THE CLAIM OR US&nbsp;$100.
</p>
<p>See also the <a href="/legal/disclaimer">Disclaimer &amp; Liability</a> statement.</p>
<h2 id="law">14. Governing law</h2>
<p>
This EULA is governed by the laws of the State of Georgia, USA.
Venue lies in Georgia courts as described in the <a href="/legal/terms">Terms of Service</a>, except where prohibited.
</p>
<h2 id="contact">15. Contact</h2>
<p>
HoneyPeer, LLC · DeKalb County, Georgia, USA<br />
<a href="mailto:[email protected]">[email protected]</a>
</p>
<div class="docs-pager">
<a href="/legal/terms"><span>Previous</span><strong>← Terms of Service</strong></a>
<a class="next" href="/legal/privacy"><span>Next</span><strong>Privacy Policy →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Legal · PearDock</title>
<meta name="description" content="Legal documents for PearDock by HoneyPeer, LLC: Terms, EULA, Privacy, AGPL-3.0 license notices, and more." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">HoneyPeer, LLC</p>
<h1>Legal</h1>
<p class="docs-lead">
PearDock is owned, operated, and engineered by <strong>HoneyPeer, LLC</strong>,
a limited liability company organized under the laws of the State of Georgia
(DeKalb County), United States. The PearDock software is made available as
open source under the <strong>GNU Affero General Public License v3.0 (AGPL-3.0)</strong>.
</p>
<div class="legal-notice callout info">
<div class="callout-icon">i</div>
<div class="callout-body">
<strong>Two layers of rights.</strong>
The <em>source code</em> is licensed under AGPL-3.0 (and third-party licenses noted in
<a href="/legal/license">Open Source Notices</a>).
Use of the <em>website, hosted materials, trademarks, and binary distributions we publish</em>
is also governed by the Terms, EULA, and policies linked below.
</div>
</div>
<div class="docs-cards legal-cards">
<a class="docs-card" href="/legal/terms">
<span class="card-tag">Website &amp; services</span>
<h3>Terms of Service</h3>
<p>Rules for using peardock.boats, install.peardock.boats, and related HoneyPeer online properties.</p>
</a>
<a class="docs-card" href="/legal/eula">
<span class="card-tag">Software</span>
<h3>End User License Agreement</h3>
<p>License terms for PearDock client/server binaries and desktop applications distributed by HoneyPeer.</p>
</a>
<a class="docs-card" href="/legal/privacy">
<span class="card-tag">Data</span>
<h3>Privacy Policy</h3>
<p>How HoneyPeer handles information on our sites, including cookies and similar technologies.</p>
</a>
<a class="docs-card" href="/legal/license">
<span class="card-tag">Open source</span>
<h3>Open Source &amp; Notices</h3>
<p>AGPL-3.0 license summary, copyright, and third-party components (including Holesail).</p>
</a>
<a class="docs-card" href="/legal/aup">
<span class="card-tag">Conduct</span>
<h3>Acceptable Use Policy</h3>
<p>Prohibited misuse of PearDock, installers, tunnels, and HoneyPeer infrastructure.</p>
</a>
<a class="docs-card" href="/legal/copyright">
<span class="card-tag">IP</span>
<h3>Copyright &amp; DMCA</h3>
<p>Copyright ownership, notice-and-takedown, and reporting infringement.</p>
</a>
<a class="docs-card" href="/legal/disclaimer">
<span class="card-tag">Risk</span>
<h3>Disclaimer &amp; Liability</h3>
<p>Warranty disclaimers and limitation of liability for open-source infrastructure software.</p>
</a>
<a class="docs-card" href="/legal/security">
<span class="card-tag">Safety</span>
<h3>Security Policy</h3>
<p>Responsible vulnerability disclosure and security contact for PearDock.</p>
</a>
</div>
<h2>Entity</h2>
<div class="table-wrap">
<table>
<tbody>
<tr><th scope="row">Product</th><td>PearDock</td></tr>
<tr><th scope="row">Owner / operator / engineer</th><td>HoneyPeer, LLC</td></tr>
<tr><th scope="row">Jurisdiction</th><td>State of Georgia, United States (DeKalb County)</td></tr>
<tr><th scope="row">Software license</th><td>GNU Affero General Public License, Version 3 (AGPL-3.0)</td></tr>
<tr><th scope="row">Primary sites</th><td>peardock.boats · install.peardock.boats</td></tr>
<tr><th scope="row">Contact</th><td><a href="mailto:[email protected]">[email protected]</a> (legal, privacy, security, brand, DMCA)</td></tr>
</tbody>
</table>
</div>
<p class="legal-updated">Documents effective as of <strong>July 11, 2026</strong>, unless a page states otherwise. HoneyPeer may update these documents; the “Last updated” line on each page controls.</p>
<div class="docs-pager">
<span></span>
<a class="next" href="/legal/terms"><span>Start with</span><strong>Terms of Service →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+136
View File
@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Open Source Notices · PearDock Legal</title>
<meta name="description" content="AGPL-3.0 license and third-party open source notices for PearDock by HoneyPeer, LLC." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>Open Source &amp; Third-Party Notices</h1>
<p class="docs-lead">
PearDock is open-source software engineered by <strong>HoneyPeer, LLC</strong> and licensed under the
<strong>GNU Affero General Public License, Version 3 (AGPL-3.0)</strong>,
matching <a href="https://github.com/holesail/holesail" target="_blank" rel="noopener">Holesail</a>,
with third-party components under their respective licenses.
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<nav class="toc">
<strong>On this page</strong>
<ol>
<li><a href="#copyright">Copyright</a></li>
<li><a href="#agpl">AGPL-3.0</a></li>
<li><a href="#network">Network use &amp; source offer</a></li>
<li><a href="#third">Third-party components</a></li>
<li><a href="#holesail">Holesail</a></li>
<li><a href="#source">Source availability</a></li>
<li><a href="#contact">Contact</a></li>
</ol>
</nav>
<h2 id="copyright">1. Copyright</h2>
<p>
Copyright © 2024-<span data-year></span> HoneyPeer, LLC and PearDock contributors.<br />
PearDock is owned, operated, and engineered by HoneyPeer, LLC (DeKalb County, Georgia, USA).
</p>
<h2 id="agpl">2. GNU Affero General Public License v3.0</h2>
<p>
PearDock is licensed under the GNU Affero General Public License, Version 3 (the “License”).
You may not use this file except in compliance with the License.
You may obtain a copy of the License at:
</p>
<p><a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">https://www.gnu.org/licenses/agpl-3.0.html</a></p>
<p>
This program is free software: you can redistribute it and/or modify it under the terms of the
GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License,
or (at your option) any later version.
</p>
<p>
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
</p>
<div class="callout info">
<div class="callout-icon">i</div>
<div class="callout-body">
<strong>Full text.</strong> The complete AGPL-3.0 license text is included in the PearDock source repository
as <code>LICENSE</code>. In case of conflict between this summary page and the repository license file, the repository license file controls for the source code.
</div>
</div>
<h2 id="network">3. Network use and source offer</h2>
<p>
AGPL-3.0 is a strong copyleft license designed for network server software.
If you modify PearDock and let others interact with it over a computer network, AGPL requires that you offer
those users the corresponding source code of your modified version under AGPL-3.0 (see AGPL §13).
Operators running modified servers or combined distributions should review the full License carefully.
</p>
<p>
Redistributions must preserve copyright notices, license notices, and disclaimers as required by AGPL-3.0.
</p>
<h2 id="third">4. Third-party components</h2>
<p>
PearDock depends on third-party libraries and tools (for example Holepunch stack components, Docker client libraries, Electron, and others).
Each dependency is governed by its own license as declared in its package metadata or upstream repository.
Operators redistributing combined builds should review dependency licenses and generate an SBOM as appropriate for their compliance process.
</p>
<p>
Project documentation may reference an SBOM or dependency inventory (for example <code>docs/SBOM.md</code> in the source tree) for operator convenience; that inventory may evolve over time.
</p>
<h2 id="holesail">5. Holesail</h2>
<p>
Peer-to-peer port tunneling uses <strong>Holesail</strong>, which is also licensed under
<strong>GNU Affero General Public License v3.0 (AGPL-3.0)</strong>.
PearDocks primary license matches Holesail so the project and its required tunnel stack share the same AGPL-3.0 family.
</p>
<ul>
<li>PearDock source and Holesail are both AGPL-3.0.</li>
<li>Operators can disable tunnel creation with <code>ENABLE_HOLESAIL=0</code> where supported; the Holesail package may still be present as a dependency.</li>
</ul>
<p>See product docs: <a href="/docs/holesail">Holesail tunnels</a>.</p>
<h2 id="source">6. Source availability</h2>
<p>
Source code is published at the project repository (currently
<a href="https://git.ssh.surf/snxraven/peardock" target="_blank" rel="noopener">git.ssh.surf/snxraven/peardock</a>).
Installers at <a href="https://install.peardock.boats">install.peardock.boats</a> may distribute prebuilt binaries corresponding to published releases (including the rolling channel).
</p>
<h2 id="contact">7. Contact</h2>
<p>
License questions: <a href="mailto:[email protected]">[email protected]</a><br />
HoneyPeer, LLC · DeKalb County, Georgia, USA
</p>
<div class="docs-pager">
<a href="/legal/privacy"><span>Previous</span><strong>← Privacy</strong></a>
<a class="next" href="/legal/aup"><span>Next</span><strong>Acceptable Use →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+214
View File
@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Privacy Policy · PearDock Legal</title>
<meta name="description" content="Privacy Policy for PearDock and HoneyPeer, LLC websites and services, including cookies and similar technologies." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>Privacy Policy</h1>
<p class="docs-lead">
This Privacy Policy explains how <strong>HoneyPeer, LLC</strong> (“HoneyPeer,” “we,” “us”) handles information
in connection with PearDock websites and related online properties (the “<strong>Sites</strong>”),
including cookies and similar technologies.
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<nav class="toc">
<strong>On this page</strong>
<ol>
<li><a href="#scope">Scope</a></li>
<li><a href="#controller">Controller</a></li>
<li><a href="#collect">Information we collect</a></li>
<li><a href="#local">Local PearDock software</a></li>
<li><a href="#use">How we use information</a></li>
<li><a href="#share">Sharing</a></li>
<li><a href="#cookies">Cookies &amp; similar technologies</a></li>
<li><a href="#retention">Retention</a></li>
<li><a href="#security">Security</a></li>
<li><a href="#rights">Your rights</a></li>
<li><a href="#children">Children</a></li>
<li><a href="#intl">International transfers</a></li>
<li><a href="#changes">Changes</a></li>
<li><a href="#contact">Contact</a></li>
</ol>
</nav>
<h2 id="scope">1. Scope</h2>
<p>
This Policy covers the Sites (including peardock.boats and install.peardock.boats) and communications with HoneyPeer about PearDock.
It does <strong>not</strong> cover data processed solely on your own servers when you self-host PearDock, except to the extent that data is sent to us (for example email or issue reports you choose to send).
</p>
<h2 id="controller">2. Data controller</h2>
<p>
HoneyPeer, LLC<br />
DeKalb County, Georgia, United States<br />
Contact: <a href="mailto:[email protected]">[email protected]</a>
</p>
<h2 id="collect">3. Information we collect</h2>
<h3>3.1 Information you provide</h3>
<ul>
<li>Contact details if you email us (name, email address, message content);</li>
<li>Security vulnerability reports or legal notices you send;</li>
<li>Any other information you voluntarily submit via forms or email.</li>
</ul>
<h3>3.2 Information collected automatically</h3>
<ul>
<li>Standard server logs (IP address, user agent, request path, timestamps, referrer);</li>
<li>Approximate location derived from IP (city/region level at most, if used);</li>
<li>Diagnostic data if you opt into telemetry in a future product feature that discloses it at collection time.</li>
</ul>
<h3>3.3 We do not sell personal data</h3>
<p>HoneyPeer does not sell your personal information.</p>
<h2 id="local">4. Local PearDock software</h2>
<p>
When you run PearDock server or client on your machines, operational data (containers, logs, peer keys, Docker API traffic)
generally stays on systems and networks you control.
HoneyPeer does not receive that operational data unless you intentionally transmit it to us or to a third party.
</p>
<p>
Peer-to-peer connections and optional Holesail tunnels exchange data between peers you configure; HoneyPeer is not a party to that traffic.
</p>
<h2 id="use">5. How we use information</h2>
<ul>
<li>Operate, secure, and improve the Sites and installers;</li>
<li>Respond to support, legal, and security requests;</li>
<li>Prevent abuse, fraud, and violations of our <a href="/legal/aup">Acceptable Use Policy</a>;</li>
<li>Comply with law and enforce our agreements;</li>
<li>Communicate about PearDock if you contact us or subscribe to updates (where offered).</li>
</ul>
<h2 id="share">6. Sharing</h2>
<p>We may share information with:</p>
<ul>
<li><strong>Service providers</strong> who host the Sites, DNS, CDN, email, or analytics under contractual confidentiality obligations;</li>
<li><strong>Professional advisors</strong> (legal, accounting) as needed;</li>
<li><strong>Authorities</strong> when required by law or to protect rights, safety, and security;</li>
<li><strong>Business transfers</strong> in connection with a merger, acquisition, or asset sale, subject to appropriate protections.</li>
</ul>
<h2 id="cookies">7. Cookies and similar technologies</h2>
<p>
Cookies are small text files stored on your device. Similar technologies include local storage, session storage, and pixels.
We use “cookies” to refer to these technologies collectively.
</p>
<h3 id="cookies-how">7.1 How we use cookies</h3>
<div class="table-wrap">
<table>
<thead>
<tr><th>Category</th><th>Purpose</th><th>Examples</th></tr>
</thead>
<tbody>
<tr>
<td><strong>Essential</strong></td>
<td>Required for basic site operation, security, and preference storage you request</td>
<td>Load balancing, security tokens, UI preferences if stored locally</td>
</tr>
<tr>
<td><strong>Analytics</strong> (if enabled)</td>
<td>Understand aggregate traffic and improve documentation</td>
<td>Privacy-respecting analytics configured by HoneyPeer (disclosed if active)</td>
</tr>
<tr>
<td><strong>Functional</strong></td>
<td>Remember choices such as dismissed banners</td>
<td>Local storage flags</td>
</tr>
</tbody>
</table>
</div>
<p>
As of the Last updated date, the marketing and documentation Sites prioritize static content delivery.
If we enable non-essential analytics cookies, we will update this Policy and, where required, provide consent controls.
</p>
<h3 id="cookies-choices">7.2 Your choices</h3>
<ul>
<li>Browser settings can block or delete cookies; some features may not work if essential cookies are blocked.</li>
<li>Where consent is required for non-essential cookies, we will request it before setting them.</li>
</ul>
<h3 id="cookies-third">7.3 Third parties</h3>
<p>
Embedded third-party content (fonts, CDNs, linked repositories) may set their own cookies under their policies.
We do not control third-party cookies on external sites you navigate to.
</p>
<h2 id="retention">8. Retention</h2>
<p>
We retain personal information only as long as needed for the purposes above, including legal, security, and accounting requirements.
Server logs are typically rotated on a limited schedule determined by our hosting configuration.
</p>
<h2 id="security">9. Security</h2>
<p>
We implement reasonable administrative and technical measures appropriate to the nature of the Sites.
No method of transmission or storage is perfectly secure. Report suspected site or product vulnerabilities to
<a href="mailto:[email protected]">[email protected]</a> (see <a href="/legal/security">Security Policy</a>).
</p>
<h2 id="rights">10. Your rights</h2>
<p>
Depending on your location, you may have rights to access, correct, delete, or restrict processing of personal data, or to object to certain processing.
To exercise rights, contact <a href="mailto:[email protected]">[email protected]</a>.
We may need to verify your request. You may also lodge a complaint with a supervisory authority where applicable.
</p>
<p>
Residents of certain U.S. states may have additional rights under state privacy laws; contact us to exercise them.
We do not use sensitive personal information for “sale” or “sharing” as those terms are defined under the CCPA/CPRA.
</p>
<h2 id="children">11. Children</h2>
<p>
The Sites and Software are directed to technical operators and businesses, not children under 16 (or the age of digital consent in your jurisdiction).
We do not knowingly collect personal information from children.
</p>
<h2 id="intl">12. International transfers</h2>
<p>
HoneyPeer is based in the United States. If you access the Sites from outside the U.S., your information may be processed in the U.S. or other countries where our providers operate.
</p>
<h2 id="changes">13. Changes</h2>
<p>
We may update this Policy by posting a new version with a revised “Last updated” date.
Material changes will be indicated on this page.
</p>
<h2 id="contact">14. Contact</h2>
<p>
HoneyPeer, LLC · DeKalb County, Georgia, USA<br />
<a href="mailto:[email protected]">[email protected]</a>
</p>
<div class="docs-pager">
<a href="/legal/eula"><span>Previous</span><strong>← EULA</strong></a>
<a class="next" href="/legal/license"><span>Next</span><strong>Open Source Notices →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Security Policy · PearDock Legal</title>
<meta name="description" content="Security vulnerability disclosure policy for PearDock by HoneyPeer, LLC." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>Security Policy</h1>
<p class="docs-lead">
HoneyPeer, LLC takes the security of PearDock seriously.
This policy describes how to report vulnerabilities responsibly.
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<h2 id="scope">1. Scope</h2>
<p>In scope:</p>
<ul>
<li>PearDock server and client software as published by HoneyPeer;</li>
<li>Official installers and websites (peardock.boats, install.peardock.boats);</li>
<li>Cryptographic handling of peer identity and RPC transport where implemented by PearDock.</li>
</ul>
<p>Generally out of scope:</p>
<ul>
<li>Issues solely in third-party dependencies without a demonstrated impact on PearDock (report upstream when appropriate);</li>
<li>Social engineering of HoneyPeer staff;</li>
<li>Denial-of-service volume testing against production Sites without prior coordination;</li>
<li>Findings that require physical access or already-compromised admin credentials on the target host.</li>
</ul>
<h2 id="report">2. How to report</h2>
<p>
Email <a href="mailto:[email protected]">[email protected]</a> with:
</p>
<ul>
<li>description of the issue and potential impact;</li>
<li>steps to reproduce or proof-of-concept (non-destructive);</li>
<li>affected versions / commit / platform if known;</li>
<li>your contact details and any preferred credit name.</li>
</ul>
<p>Please use English if possible. Do not include sensitive personal data of third parties.</p>
<h2 id="safe">3. Safe harbor</h2>
<p>
If you make a good-faith effort to follow this policy, avoid privacy violations, service disruption, and data destruction,
and do not exploit the issue beyond what is necessary to demonstrate it, HoneyPeer will not pursue legal action against you
for that research under laws applicable to unauthorized access, to the extent we can bind ourselves.
This does not authorize attacks on third-party systems or users self-hosted instances without their permission.
</p>
<h2 id="process">4. Our process</h2>
<ul>
<li>We will acknowledge receipt when practicable;</li>
<li>We will investigate and may request more detail;</li>
<li>We aim to remediate or mitigate high-severity issues in a reasonable timeframe;</li>
<li>We may publicly credit reporters who wish to be credited after a fix is available.</li>
</ul>
<p>We do not guarantee a bug bounty payment unless a separate program is announced.</p>
<h2 id="operators">5. For operators</h2>
<p>
Production hardening guidance is available in the product docs:
<a href="/docs/security">Security &amp; threat model</a> and
<a href="/docs/operator">Operator guide</a>.
Keep PearDock and Docker updated; protect <code>SERVER_SEED</code> and peer allowlists.
</p>
<h2 id="contact">6. Contact</h2>
<p>
Contact: <a href="mailto:[email protected]">[email protected]</a><br />
HoneyPeer, LLC · DeKalb County, Georgia, USA
</p>
<div class="docs-pager">
<a href="/legal/disclaimer"><span>Previous</span><strong>← Disclaimer</strong></a>
<a class="next" href="/legal/"><span>Hub</span><strong>All legal docs →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+189
View File
@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Terms of Service · PearDock Legal</title>
<meta name="description" content="Terms of Service for PearDock websites and online properties operated by HoneyPeer, LLC." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/legal.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix="../"></div>
<div class="docs-layout legal-layout">
<aside class="docs-sidebar" data-legal-nav></aside>
<button type="button" class="docs-sidebar-toggle" aria-label="Legal menu"></button>
<article class="docs-main legal-main">
<p class="legal-kicker">Legal</p>
<h1>Terms of Service</h1>
<p class="docs-lead">
These Terms of Service (“<strong>Terms</strong>”) govern your access to and use of websites,
documentation, download portals, and related online properties operated by HoneyPeer, LLC
in connection with PearDock (collectively, the “<strong>Sites</strong>”).
</p>
<p class="legal-updated">Last updated: <strong>July 11, 2026</strong></p>
<nav class="toc">
<strong>On this page</strong>
<ol>
<li><a href="#agreement">Agreement</a></li>
<li><a href="#entity">Who we are</a></li>
<li><a href="#sites">The Sites</a></li>
<li><a href="#oss">Relationship to open source</a></li>
<li><a href="#accounts">Accounts &amp; keys</a></li>
<li><a href="#acceptable">Acceptable use</a></li>
<li><a href="#ip">Intellectual property</a></li>
<li><a href="#third">Third-party services</a></li>
<li><a href="#disclaimers">Disclaimers</a></li>
<li><a href="#liability">Limitation of liability</a></li>
<li><a href="#indemnity">Indemnity</a></li>
<li><a href="#law">Governing law</a></li>
<li><a href="#changes">Changes</a></li>
<li><a href="#contact">Contact</a></li>
</ol>
</nav>
<h2 id="agreement">1. Agreement to these Terms</h2>
<p>
By accessing or using the Sites, you agree to these Terms and to our
<a href="/legal/privacy">Privacy Policy</a>
(including cookies and similar technologies) and
<a href="/legal/aup">Acceptable Use Policy</a>.
If you do not agree, do not use the Sites.
</p>
<p>
If you use the Sites on behalf of an organization, you represent that you have authority to bind that organization, and “you” includes that organization.
</p>
<h2 id="entity">2. Who we are</h2>
<p>
The Sites are owned and operated by <strong>HoneyPeer, LLC</strong> (“<strong>HoneyPeer</strong>,” “<strong>we</strong>,” “<strong>us</strong>”),
a Georgia limited liability company with its principal place of business in DeKalb County, Georgia, USA.
PearDock is a product owned, operated, and engineered by HoneyPeer.
</p>
<h2 id="sites">3. The Sites</h2>
<p>The Sites include, without limitation:</p>
<ul>
<li>https://peardock.boats and related subdomains (including documentation and marketing pages);</li>
<li>https://install.peardock.boats and other installers or download endpoints we operate;</li>
<li>any related web properties, APIs, or portals we designate as part of PearDock online services.</li>
</ul>
<p>
The Sites provide information, documentation, and software distribution for PearDock.
HoneyPeer may modify, suspend, or discontinue any part of the Sites at any time.
</p>
<h2 id="oss">4. Relationship to open-source software</h2>
<p>
PearDock <strong>source code</strong> is generally available under the
<a href="/legal/license">GNU Affero General Public License v3.0 (AGPL-3.0)</a> (and any additional notices in the repository).
Nothing in these Terms limits rights you already have under a valid open-source license to code you obtained under that license.
</p>
<p>
These Terms apply to the Sites and to materials and services HoneyPeer provides online.
Your use of compiled binaries, desktop applications, or packages we distribute may also be subject to the
<a href="/legal/eula">End User License Agreement</a>.
</p>
<h2 id="accounts">5. Accounts, keys, and operator responsibility</h2>
<p>
PearDock is designed as a peer-to-peer Docker operations tool. HoneyPeer does not host your Docker workloads as a multi-tenant SaaS control plane through the Sites by default.
You are solely responsible for:
</p>
<ul>
<li>securing server seeds, public keys, invite tokens, and access credentials;</li>
<li>configuring roles, allowlists, firewalls, and Docker permissions on systems you control;</li>
<li>compliance with laws applicable to your infrastructure and data;</li>
<li>backing up identity material (for example <code>SERVER_SEED</code> / <code>.env</code> files).</li>
</ul>
<h2 id="acceptable">6. Acceptable use</h2>
<p>
You must comply with our <a href="/legal/aup">Acceptable Use Policy</a>.
You may not misuse the Sites, installers, or HoneyPeer infrastructure, including attempts to disrupt, scrape abusively, distribute malware, or violate law.
</p>
<h2 id="ip">7. Intellectual property</h2>
<p>
Except for open-source software licensed separately, the Sites (including text, graphics, logos, and arrangement) are owned by HoneyPeer or its licensors and protected by intellectual property laws.
See our <a href="/legal/copyright">Copyright &amp; DMCA</a> page for intellectual property notices.
</p>
<p>
You may not use HoneyPeer or PearDock branding in a way that implies endorsement, partnership, or official status without prior written permission, except as allowed by fair use or other applicable law.
</p>
<h2 id="third">8. Third-party services and links</h2>
<p>
The Sites may link to third-party sites (for example source hosting or dependency projects).
HoneyPeer is not responsible for third-party content, licenses, or practices.
Some PearDock features integrate third-party components (see <a href="/legal/license">Open Source Notices</a>).
</p>
<h2 id="disclaimers">9. Disclaimers</h2>
<p>
THE SITES AND ALL MATERIALS ARE PROVIDED “AS IS” AND “AS AVAILABLE.”
TO THE MAXIMUM EXTENT PERMITTED BY LAW, HONEYPEER DISCLAIMS ALL WARRANTIES, WHETHER EXPRESS, IMPLIED, OR STATUTORY,
INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
WE DO NOT WARRANT THAT THE SITES WILL BE UNINTERRUPTED, SECURE, OR ERROR-FREE.
</p>
<p>Additional product-level disclaimers appear in the <a href="/legal/disclaimer">Disclaimer &amp; Liability</a> document and the EULA.</p>
<h2 id="liability">10. Limitation of liability</h2>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, HONEYPEER AND ITS OFFICERS, MEMBERS, EMPLOYEES, AND AGENTS WILL NOT BE LIABLE FOR
ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS, DATA, GOODWILL,
OR BUSINESS INTERRUPTION, ARISING OUT OF OR RELATED TO YOUR USE OF THE SITES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
</p>
<p>
TO THE MAXIMUM EXTENT PERMITTED BY LAW, HONEYPEERS TOTAL LIABILITY FOR ANY CLAIM ARISING OUT OF OR RELATING TO THE SITES
WILL NOT EXCEED THE GREATER OF (A) AMOUNTS YOU PAID TO HONEYPEER FOR THE SITES IN THE TWELVE (12) MONTHS BEFORE THE CLAIM, OR (B) ONE HUNDRED U.S. DOLLARS (US&nbsp;$100).
</p>
<p>Some jurisdictions do not allow certain limitations; in those cases, our liability is limited to the fullest extent permitted.</p>
<h2 id="indemnity">11. Indemnification</h2>
<p>
You will defend, indemnify, and hold harmless HoneyPeer and its members, officers, and agents from and against any claims, damages, losses, and expenses
(including reasonable attorneys fees) arising out of or related to your misuse of the Sites, your infrastructure, your violation of these Terms, or your violation of law or third-party rights.
</p>
<h2 id="law">12. Governing law and venue</h2>
<p>
These Terms are governed by the laws of the State of Georgia, USA, without regard to conflict-of-law principles.
Exclusive venue for disputes lies in the state or federal courts located in Georgia (including courts serving DeKalb County, Georgia),
and you consent to personal jurisdiction there, except where prohibited by law.
</p>
<h2 id="changes">13. Changes</h2>
<p>
We may update these Terms by posting a revised version on the Sites with a new “Last updated” date.
Continued use after changes become effective constitutes acceptance of the revised Terms.
</p>
<h2 id="contact">14. Contact</h2>
<p>
HoneyPeer, LLC<br />
DeKalb County, Georgia, United States<br />
Legal: <a href="mailto:[email protected]">[email protected]</a>
</p>
<div class="docs-pager">
<a href="/legal/"><span>Legal hub</span><strong>← All documents</strong></a>
<a class="next" href="/legal/eula"><span>Next</span><strong>EULA →</strong></a>
</div>
</article>
</div>
<div data-footer data-prefix="../"></div>
<script type="module" src="/src/js/site.js"></script>
</body>
</html>
+3243
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "peardock-website",
"version": "1.0.0",
"private": true,
"description": "Official PearDock marketing site and documentation",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"start": "node server.mjs",
"serve": "node server.mjs"
},
"devDependencies": {
"serve": "^14.2.6",
"vite": "^6.3.5"
},
"dependencies": {
"mermaid": "^11.16.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#0f1117</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+13
View File
@@ -0,0 +1,13 @@
{
"name": "PearDock",
"short_name": "PearDock",
"description": "Decentralized Docker management on the Holepunch stack",
"start_url": "/",
"display": "standalone",
"background_color": "#0f1117",
"theme_color": "#2dd4bf",
"icons": [
{ "src": "/assets/favicons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/assets/favicons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 60" width="280" height="60">
<text x="0" y="45" style="font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-weight: 600; font-size: 42px; fill: #f4f7fb; letter-spacing: -0.02em;">peardock</text>
</svg>

After

Width:  |  Height:  |  Size: 292 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 60" width="280" height="60">
<text x="0" y="45" style="font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-weight: 600; font-size: 42px; fill: #1e3a5f; letter-spacing: -0.02em;">peardock</text>
</svg>

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Releases · PearDock</title>
<meta name="description" content="Live PearDock releases and downloadable assets from git.ssh.surf/snxraven/peardock." />
<meta name="theme-color" content="#2dd4bf" />
<link rel="icon" href="/assets/favicons/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/assets/favicons/apple-touch-icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/src/styles/global.css" />
<link rel="stylesheet" href="/src/styles/docs.css" />
<link rel="stylesheet" href="/src/styles/releases.css" />
</head>
<body>
<div class="bg-mesh" aria-hidden="true"></div>
<div data-nav data-prefix=""></div>
<section class="releases-hero">
<div class="container releases-hero-inner">
<div class="badge mb-4">live from Gitea</div>
<h1>Releases</h1>
<p class="lead">
Binaries and notes published at
<a href="https://git.ssh.surf/snxraven/peardock/releases" target="_blank" rel="noopener">git.ssh.surf/snxraven/peardock/releases</a>.
The <strong>rolling</strong> tag is the recommended install channel.
</p>
<div class="flex flex-wrap gap-3 releases-hero-actions">
<a class="btn btn-primary" href="/download">Install guide</a>
<a class="btn btn-secondary" href="https://git.ssh.surf/snxraven/peardock/releases" target="_blank" rel="noopener">Open on Gitea</a>
<button type="button" class="btn btn-ghost" id="releases-refresh">Refresh</button>
</div>
</div>
</section>
<div class="container releases-page">
<div class="releases-toolbar">
<div class="releases-filters" role="group" aria-label="Filter assets">
<button type="button" class="releases-filter is-active" data-filter="all">All assets</button>
<button type="button" class="releases-filter" data-filter="server">Server</button>
<button type="button" class="releases-filter" data-filter="client">Client</button>
<button type="button" class="releases-filter" data-filter="checksum">Checksums</button>
</div>
<p class="releases-meta" id="releases-meta" aria-live="polite">Loading releases…</p>
</div>
<div id="releases-error" class="releases-error hidden" role="alert" hidden></div>
<div id="releases-list" class="releases-list" aria-busy="true">
<div class="releases-skeleton" aria-hidden="true"></div>
<div class="releases-skeleton" aria-hidden="true"></div>
</div>
</div>
<div data-footer data-prefix=""></div>
<script type="module" src="/src/js/site.js"></script>
<script type="module" src="/src/js/releases.js"></script>
</body>
</html>
+256
View File
@@ -0,0 +1,256 @@
/**
* PearDock website server: static dist/ + Gitea releases API proxy.
*
* Proxies:
* GET /api/releases list releases
* GET /api/releases/tags/:tag single release by tag
* GET /api/releases/latest latest non-draft (or rolling if present)
*
* Env:
* PORT (default 4173)
* DIST (default ./dist)
* GITEA_API (default https://git.ssh.surf/api/v1/repos/snxraven/peardock)
*/
import http from 'node:http'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const PORT = Number(process.env.PORT || 4173)
const DIST = path.resolve(process.env.DIST || path.join(__dirname, 'dist'))
const GITEA_API =
process.env.GITEA_API || 'https://git.ssh.surf/api/v1/repos/snxraven/peardock'
const GITEA_WEB =
process.env.GITEA_WEB || 'https://git.ssh.surf/snxraven/peardock'
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.map': 'application/json',
'.txt': 'text/plain; charset=utf-8',
'.webmanifest': 'application/manifest+json',
}
function send(res, status, body, headers = {}) {
const buf = Buffer.isBuffer(body) ? body : Buffer.from(body ?? '')
res.writeHead(status, {
'Content-Length': buf.length,
...headers,
})
res.end(buf)
}
function sendJson(res, status, obj) {
send(res, status, JSON.stringify(obj), {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'public, max-age=60',
})
}
async function proxyGitea(apiPath, search = '') {
const url = `${GITEA_API}${apiPath}${search || ''}`
const r = await fetch(url, {
headers: {
Accept: 'application/json',
'User-Agent': 'peardock-website-releases/1.0',
},
})
const text = await r.text()
let data
try {
data = text ? JSON.parse(text) : null
} catch {
data = { message: text || 'Invalid JSON from Gitea' }
}
return { status: r.status, data, url }
}
async function handleApi(req, res, url) {
if (req.method === 'OPTIONS') {
send(res, 204, '', {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
})
return
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
sendJson(res, 405, { error: 'Method not allowed' })
return
}
try {
// GET /api/releases
if (url.pathname === '/api/releases' || url.pathname === '/api/releases/') {
const qs = new URLSearchParams(url.search)
if (!qs.has('limit')) qs.set('limit', '25')
const { status, data } = await proxyGitea('/releases', `?${qs}`)
// Annotate with canonical web links
if (Array.isArray(data)) {
for (const rel of data) {
if (rel && !rel.html_url && rel.tag_name) {
rel.html_url = `${GITEA_WEB}/releases/tag/${encodeURIComponent(rel.tag_name)}`
}
}
}
sendJson(res, status, {
source: GITEA_WEB + '/releases',
api: GITEA_API + '/releases',
releases: data,
})
return
}
// GET /api/releases/latest
if (url.pathname === '/api/releases/latest') {
// Prefer rolling tag if present, else first non-draft
const list = await proxyGitea('/releases', '?limit=50')
if (!Array.isArray(list.data)) {
sendJson(res, list.status, list.data)
return
}
const rolling = list.data.find((r) => r.tag_name === 'rolling' && !r.draft)
const first = list.data.find((r) => !r.draft) || list.data[0]
const pick = rolling || first
if (!pick) {
sendJson(res, 404, { error: 'No releases found' })
return
}
sendJson(res, 200, {
source: GITEA_WEB + '/releases',
release: pick,
})
return
}
// GET /api/releases/tags/:tag
const tagMatch = url.pathname.match(/^\/api\/releases\/tags\/([^/]+)\/?$/)
if (tagMatch) {
const tag = decodeURIComponent(tagMatch[1])
const { status, data } = await proxyGitea(
`/releases/tags/${encodeURIComponent(tag)}`
)
sendJson(res, status, {
source: `${GITEA_WEB}/releases/tag/${encodeURIComponent(tag)}`,
release: data,
})
return
}
sendJson(res, 404, {
error: 'Unknown API route',
routes: [
'GET /api/releases',
'GET /api/releases/latest',
'GET /api/releases/tags/:tag',
],
})
} catch (err) {
console.error('[api]', err)
sendJson(res, 502, {
error: 'Failed to reach Gitea releases API',
message: err?.message || String(err),
upstream: GITEA_API,
})
}
}
function safeJoin(root, reqPath) {
const decoded = decodeURIComponent(reqPath.split('?')[0])
const cleaned = path.normalize(decoded).replace(/^(\.\.(\/|\\|$))+/, '')
const full = path.join(root, cleaned)
if (!full.startsWith(root)) return null
return full
}
function resolveStatic(urlPath) {
let p = urlPath
if (p.endsWith('/')) p += 'index.html'
let file = safeJoin(DIST, p)
if (!file) return null
if (existsFile(file)) return file
// clean URLs: /releases → releases.html, /docs/foo → docs/foo.html
if (!path.extname(file)) {
const asHtml = file + '.html'
if (existsFile(asHtml)) return asHtml
const asIndex = path.join(file, 'index.html')
if (existsFile(asIndex)) return asIndex
}
return null
}
function existsFile(f) {
try {
return fs.statSync(f).isFile()
} catch {
return false
}
}
function serveStatic(req, res, url) {
const file = resolveStatic(url.pathname === '/' ? '/index.html' : url.pathname)
if (!file) {
send(res, 404, 'Not found', { 'Content-Type': 'text/plain; charset=utf-8' })
return
}
const ext = path.extname(file).toLowerCase()
const type = MIME[ext] || 'application/octet-stream'
const stream = fs.createReadStream(file)
res.writeHead(200, {
'Content-Type': type,
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600',
})
if (req.method === 'HEAD') {
res.end()
return
}
stream.pipe(res)
stream.on('error', () => {
if (!res.headersSent) send(res, 500, 'Read error')
else res.end()
})
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
// Health
if (url.pathname === '/api/health') {
sendJson(res, 200, { ok: true, gitea: GITEA_API })
return
}
if (url.pathname.startsWith('/api/')) {
await handleApi(req, res, url)
return
}
if (req.method !== 'GET' && req.method !== 'HEAD') {
send(res, 405, 'Method not allowed', { 'Content-Type': 'text/plain' })
return
}
serveStatic(req, res, url)
})
server.listen(PORT, () => {
console.log(`[peardock-website] http://127.0.0.1:${PORT}`)
console.log(`[peardock-website] dist=${DIST}`)
console.log(`[peardock-website] gitea=${GITEA_API}`)
console.log(`[peardock-website] releases UI=/releases api=/api/releases`)
})
+345
View File
@@ -0,0 +1,345 @@
/**
* Live releases UI. Fetches /api/releases (proxied to Gitea).
*/
const GITEA_RELEASES_WEB = 'https://git.ssh.surf/snxraven/peardock/releases'
const API = '/api/releases'
/** @type {'all'|'server'|'client'|'checksum'} */
let assetFilter = 'all'
function $(id) {
return document.getElementById(id)
}
function escapeHtml(s) {
return String(s ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function formatBytes(n) {
const v = Number(n)
if (!Number.isFinite(v) || v < 0) return '-'
if (v < 1024) return `${v} B`
if (v < 1024 ** 2) return `${(v / 1024).toFixed(1)} KiB`
if (v < 1024 ** 3) return `${(v / 1024 ** 2).toFixed(1)} MiB`
return `${(v / 1024 ** 3).toFixed(2)} GiB`
}
function formatDate(iso) {
if (!iso) return '-'
try {
return new Date(iso).toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return iso
}
}
function classifyAsset(name) {
const n = String(name || '').toLowerCase()
if (n.endsWith('.sha256') || n.includes('checksum') || n === 'sha256sums.txt') return 'checksum'
if (n.includes('peardock-server') || n.includes('server-')) return 'server'
if (n.includes('peardock-client') || n.includes('client-') || n.endsWith('.dmg') || n.endsWith('.appimage'))
return 'client'
if (n.includes('RELEASE_NOTES') || n.endsWith('.md')) return 'notes'
return 'other'
}
function hostFromName(name) {
const m = String(name).match(
/(linux|darwin|win32|windows)-(x64|arm64|amd64|aarch64)/i
)
if (!m) return ''
const os = m[1].toLowerCase().replace('windows', 'win32')
let arch = m[2].toLowerCase()
if (arch === 'amd64') arch = 'x64'
if (arch === 'aarch64') arch = 'arm64'
return `${os}-${arch}`
}
/**
* Safe release-notes renderer.
* Avoids brittle markdownHTML that can break the page (nested pre/p, bad lists).
*/
function renderBody(md) {
if (!md || !String(md).trim()) {
return '<p class="text-muted">No release notes.</p>'
}
// Escape everything, then apply only line-oriented, non-nesting transforms
const raw = String(md).replace(/\r\n/g, '\n')
const lines = raw.split('\n')
/** @type {string[]} */
const out = []
let inCode = false
/** @type {string[]} */
const codeBuf = []
const flushCode = () => {
if (!codeBuf.length && !inCode) return
out.push(`<pre class="release-notes-pre"><code>${codeBuf.join('\n')}</code></pre>`)
codeBuf.length = 0
}
for (const line of lines) {
const fence = line.match(/^```/)
if (fence) {
if (inCode) {
flushCode()
inCode = false
} else {
inCode = true
}
continue
}
if (inCode) {
codeBuf.push(escapeHtml(line))
continue
}
const heading = line.match(/^(#{1,3})\s+(.+)$/)
if (heading) {
const level = Math.min(heading[1].length + 2, 4) // h3-h4
out.push(`<h${level}>${inlineFormat(heading[2])}</h${level}>`)
continue
}
const bullet = line.match(/^\s*[-*]\s+(.+)$/)
if (bullet) {
out.push(`<div class="release-notes-li">• ${inlineFormat(bullet[1])}</div>`)
continue
}
if (!line.trim()) {
out.push('<div class="release-notes-gap"></div>')
continue
}
out.push(`<p>${inlineFormat(line)}</p>`)
}
if (inCode) flushCode()
return out.join('\n') || '<p class="text-muted">No release notes.</p>'
}
function inlineFormat(s) {
let t = escapeHtml(s)
t = t.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
t = t.replace(/`([^`]+)`/g, '<code>$1</code>')
t = t.replace(
/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,
'<a href="$2" target="_blank" rel="noopener">$1</a>'
)
return t
}
function assetMatchesFilter(kind) {
if (assetFilter === 'all') return kind !== 'notes'
return kind === assetFilter
}
function renderAsset(a) {
const name = a.name || 'asset'
const kind = classifyAsset(name)
if (!assetMatchesFilter(kind)) return ''
const host = hostFromName(name)
const url = a.browser_download_url || '#'
const size = formatBytes(a.size)
return `
<a class="release-asset release-asset--${escapeHtml(kind)}" href="${escapeHtml(url)}" target="_blank" rel="noopener">
<span class="release-asset-name"><code>${escapeHtml(name)}</code></span>
<span class="release-asset-meta">
${host ? `<span class="badge badge-muted">${escapeHtml(host)}</span>` : ''}
<span>${escapeHtml(size)}</span>
</span>
</a>`
}
function renderRelease(rel) {
const tag = rel.tag_name || 'unknown'
const name = rel.name || tag
const isRolling = tag === 'rolling'
const isPre = Boolean(rel.prerelease)
const isDraft = Boolean(rel.draft)
const htmlUrl = rel.html_url || `${GITEA_RELEASES_WEB}/tag/${encodeURIComponent(tag)}`
const published = formatDate(rel.published_at || rel.created_at)
const assets = Array.isArray(rel.assets) ? [...rel.assets] : []
assets.sort((a, b) => String(a.name).localeCompare(String(b.name)))
const assetHtml = assets.map(renderAsset).filter(Boolean).join('')
const visibleCount = assets.filter((a) => assetMatchesFilter(classifyAsset(a.name))).length
return `
<article class="release-card${isRolling ? ' release-card--rolling' : ''}" data-tag="${escapeHtml(tag)}">
<header class="release-card-header">
<div class="release-card-titles">
<div class="release-card-badges">
${isRolling ? '<span class="badge">rolling</span>' : ''}
${isPre ? '<span class="badge badge-muted">pre-release</span>' : ''}
${isDraft ? '<span class="badge badge-muted">draft</span>' : ''}
</div>
<h2 class="release-card-title">
<a href="${escapeHtml(htmlUrl)}" target="_blank" rel="noopener">${escapeHtml(name)}</a>
</h2>
<p class="release-card-sub">
<code class="release-tag">${escapeHtml(tag)}</code>
<span class="text-muted">·</span>
<time datetime="${escapeHtml(rel.published_at || rel.created_at || '')}">${escapeHtml(published)}</time>
${rel.target_commitish ? `<span class="text-muted">·</span> <code class="release-sha">${escapeHtml(String(rel.target_commitish).slice(0, 12))}</code>` : ''}
</p>
</div>
<div class="release-card-actions">
<a class="btn btn-sm btn-secondary" href="${escapeHtml(htmlUrl)}" target="_blank" rel="noopener">View on Gitea</a>
</div>
</header>
<div class="release-body">${renderBody(rel.body)}</div>
<section class="release-assets">
<h3 class="release-assets-title">Assets <span class="text-muted">(${visibleCount})</span></h3>
${
assetHtml
? `<div class="release-asset-list">${assetHtml}</div>`
: '<p class="text-muted">No assets match this filter.</p>'
}
</section>
</article>`
}
function setError(msg, detail) {
const el = $('releases-error')
if (!el) return
el.hidden = false
el.classList.remove('hidden')
el.innerHTML = `
<strong>Could not load releases.</strong>
<p>${escapeHtml(msg)}</p>
${detail ? `<pre class="releases-error-detail">${escapeHtml(detail)}</pre>` : ''}
<p><a href="${GITEA_RELEASES_WEB}" target="_blank" rel="noopener">Open releases on Gitea </a></p>`
}
function clearError() {
const el = $('releases-error')
if (!el) return
el.innerHTML = ''
el.hidden = true
el.classList.add('hidden')
}
async function loadReleases() {
const list = $('releases-list')
const meta = $('releases-meta')
if (list) {
list.setAttribute('aria-busy', 'true')
list.innerHTML = `
<div class="releases-skeleton" aria-hidden="true"></div>
<div class="releases-skeleton" aria-hidden="true"></div>`
}
clearError()
if (meta) meta.textContent = 'Fetching from Gitea…'
try {
const res = await fetch(`${API}?limit=30`, {
headers: { Accept: 'application/json' },
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
throw new Error(data.error || data.message || `HTTP ${res.status}`)
}
const releases = Array.isArray(data.releases)
? data.releases
: Array.isArray(data)
? data
: []
if (!releases.length) {
if (list) list.innerHTML = '<p class="text-muted">No releases published yet.</p>'
if (meta) meta.textContent = 'No releases'
return
}
// rolling first, then by date
releases.sort((a, b) => {
if (a.tag_name === 'rolling') return -1
if (b.tag_name === 'rolling') return 1
const da = new Date(a.published_at || a.created_at || 0).getTime()
const db = new Date(b.published_at || b.created_at || 0).getTime()
return db - da
})
if (list) {
list.innerHTML = releases.map(renderRelease).join('')
list.setAttribute('aria-busy', 'false')
}
const totalAssets = releases.reduce((n, r) => n + (r.assets?.length || 0), 0)
if (meta) {
meta.innerHTML = `<span>${releases.length} release${releases.length === 1 ? '' : 's'}</span>
<span class="text-muted">·</span>
<span>${totalAssets} assets</span>
<span class="text-muted">·</span>
<a href="${escapeHtml(data.source || GITEA_RELEASES_WEB)}" target="_blank" rel="noopener">source</a>`
}
} catch (err) {
console.error('[releases]', err)
if (list) {
list.innerHTML = ''
list.setAttribute('aria-busy', 'false')
}
if (meta) meta.textContent = 'Failed to load'
setError(err.message || String(err))
}
}
function initFilters() {
document.querySelectorAll('.releases-filter').forEach((btn) => {
btn.addEventListener('click', () => {
document.querySelectorAll('.releases-filter').forEach((b) => b.classList.remove('is-active'))
btn.classList.add('is-active')
assetFilter = btn.getAttribute('data-filter') || 'all'
// re-render from last data would need cache; simplest: reload (fast, cached 60s)
// better: re-filter DOM
document.querySelectorAll('.release-card').forEach((card) => {
// re-fetch not needed; hide assets by class
card.querySelectorAll('.release-asset').forEach((a) => {
const kind = [...a.classList]
.find((c) => c.startsWith('release-asset--'))
?.replace('release-asset--', '')
const show =
assetFilter === 'all' ? kind !== 'notes' : kind === assetFilter
a.hidden = !show
})
const visible = card.querySelectorAll('.release-asset:not([hidden])').length
const title = card.querySelector('.release-assets-title .text-muted')
if (title) title.textContent = `(${visible})`
const empty = card.querySelector('.release-assets > .text-muted')
const list = card.querySelector('.release-asset-list')
if (list && !visible) {
if (!empty) {
const p = document.createElement('p')
p.className = 'text-muted release-filter-empty'
p.textContent = 'No assets match this filter.'
list.after(p)
}
} else {
card.querySelectorAll('.release-filter-empty').forEach((el) => el.remove())
}
})
})
})
}
function init() {
if (!$('releases-list')) return
initFilters()
$('releases-refresh')?.addEventListener('click', () => loadReleases())
loadReleases()
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}
+701
View File
@@ -0,0 +1,701 @@
/**
* PearDock site: shared chrome, nav, reveal, code copy, lightbox, docs
*/
const REPO = 'https://git.ssh.surf/snxraven/peardock'
const RELEASE = 'https://git.ssh.surf/snxraven/peardock/releases/tag/rolling'
const RELEASES_PAGE = '/releases'
const RELEASES_GITEA = 'https://git.ssh.surf/snxraven/peardock/releases'
const DISCORD_LINUX = 'https://join.discord-linux.com'
const COMMUNITY_PAGE = '/community'
function injectChrome() {
const navHost = document.querySelector('[data-nav]')
if (navHost) {
navHost.outerHTML = `
<header class="site-nav" id="site-nav">
<div class="nav-inner">
<a class="nav-brand" href="/">
<img src="/assets/logo/peardock-icon-64.png" alt="" width="32" height="32" />
peardock
</a>
<button type="button" class="nav-toggle" aria-label="Menu" aria-expanded="false"></button>
<ul class="nav-links">
<li><a href="/#features">Features</a></li>
<li><a href="/learn/">How it works</a></li>
<li><a href="/docs/">Docs</a></li>
<li><a href="/download">Download</a></li>
<li><a href="${RELEASES_PAGE}">Releases</a></li>
<li><a href="${COMMUNITY_PAGE}">Community</a></li>
<li><a href="/legal/">Legal</a></li>
<li><a href="${REPO}" target="_blank" rel="noopener">Source</a></li>
</ul>
<div class="nav-cta">
<a class="btn btn-ghost btn-sm" href="/learn/">Learn</a>
<a class="btn btn-primary btn-sm" href="/download">Install</a>
</div>
</div>
</header>`
}
const footHost = document.querySelector('[data-footer]')
if (footHost) {
footHost.outerHTML = `
<footer class="site-footer">
<div class="container-wide">
<div class="footer-grid">
<div class="footer-brand">
<img src="/assets/logo/peardock-icon-64.png" alt="PearDock" width="40" height="40" />
<p>Decentralized Docker management on HyperDHT. Owned &amp; engineered by HoneyPeer, LLC.</p>
<span class="badge">AGPL-3.0</span>
</div>
<div class="footer-col">
<h4>Product</h4>
<a href="/#features">Features</a>
<a href="/download">Download</a>
<a href="${RELEASES_PAGE}">Releases</a>
<a href="${COMMUNITY_PAGE}">Community</a>
<a href="/#screenshots">Screenshots</a>
<a href="${REPO}" target="_blank" rel="noopener">Source code</a>
</div>
<div class="footer-col">
<h4>Documentation</h4>
<a href="/learn/">How PearDock works</a>
<a href="/docs/quickstart">Quick start</a>
<a href="/docs/architecture">Architecture</a>
<a href="/docs/operator">Operator guide</a>
<a href="/docs/security">Security</a>
</div>
<div class="footer-col">
<h4>Legal</h4>
<a href="/legal/">Legal hub</a>
<a href="/legal/terms">Terms of Service</a>
<a href="/legal/privacy">Privacy Policy</a>
<a href="/legal/eula">EULA</a>
<a href="/legal/license">Open Source (AGPL-3.0)</a>
</div>
</div>
<div class="footer-bottom">
<span>© <span data-year></span> HoneyPeer, LLC · PearDock · DeKalb County, Georgia, USA</span>
<span>
<a href="${COMMUNITY_PAGE}">Community</a> ·
<a href="${DISCORD_LINUX}" target="_blank" rel="noopener">Discord-Linux</a> ·
<a href="/legal/">Legal</a> ·
<a href="/legal/privacy">Privacy</a>
</span>
</div>
</div>
</footer>`
}
const sideHost = document.querySelector('[data-docs-nav]')
if (sideHost) {
// Always root-absolute so /docs (no trailing slash) does not break relative links
sideHost.innerHTML = `
<a class="docs-sidebar-brand" href="/docs/">
<img src="/assets/logo/peardock-icon-64.png" alt="" width="28" height="28" />
<span class="docs-sidebar-brand-text">
<strong>peardock</strong>
<span>Documentation</span>
</span>
</a>
<p class="docs-sidebar-title">Guides</p>
<ul class="docs-nav">
<li><a href="/docs/"><span class="nav-ico"></span> Overview</a></li>
<li><a href="/learn/"><span class="nav-ico"></span> How it works</a></li>
<li><a href="/docs/quickstart"><span class="nav-ico"></span> Quick start</a></li>
<li><a href="/docs/architecture"><span class="nav-ico"></span> Architecture</a></li>
<li><a href="/docs/operator"><span class="nav-ico"></span> Operator guide</a></li>
<li><a href="/docs/holesail"><span class="nav-ico"></span> Holesail tunnels</a></li>
<li><a href="/docs/security"><span class="nav-ico"></span> Security</a></li>
</ul>
<p class="docs-sidebar-title">Reference</p>
<ul class="docs-nav">
<li><a href="/docs/api"><span class="nav-ico">{ }</span> RPC &amp; API</a></li>
<li><a href="/docs/release"><span class="nav-ico"></span> Releases</a></li>
<li><a href="/docs/faq"><span class="nav-ico">?</span> FAQ</a></li>
</ul>
<p class="docs-sidebar-title">Project</p>
<ul class="docs-nav">
<li><a href="/download"><span class="nav-ico"></span> Download</a></li>
<li><a href="${RELEASES_PAGE}"><span class="nav-ico"></span> Releases</a></li>
<li><a href="${COMMUNITY_PAGE}"><span class="nav-ico"></span> Community</a></li>
<li><a href="${DISCORD_LINUX}" target="_blank" rel="noopener"><span class="nav-ico">💬</span> Discord-Linux</a></li>
<li><a href="/legal/"><span class="nav-ico">§</span> Legal</a></li>
<li><a href="${REPO}" target="_blank" rel="noopener"><span class="nav-ico"></span> Source</a></li>
<li><a href="${RELEASE}" target="_blank" rel="noopener"><span class="nav-ico"></span> Rolling on Gitea</a></li>
</ul>`
}
const legalHost = document.querySelector('[data-legal-nav]')
if (legalHost) {
legalHost.innerHTML = `
<a class="docs-sidebar-brand" href="/legal/">
<img src="/assets/logo/peardock-icon-64.png" alt="" width="28" height="28" />
<span class="docs-sidebar-brand-text">
<strong>HoneyPeer, LLC</strong>
<span>Legal</span>
</span>
</a>
<p class="docs-sidebar-title">Agreements</p>
<ul class="docs-nav">
<li><a href="/legal/"><span class="nav-ico"></span> Overview</a></li>
<li><a href="/legal/terms"><span class="nav-ico">§</span> Terms of Service</a></li>
<li><a href="/legal/eula"><span class="nav-ico"></span> EULA</a></li>
<li><a href="/legal/aup"><span class="nav-ico"></span> Acceptable Use</a></li>
</ul>
<p class="docs-sidebar-title">Privacy &amp; data</p>
<ul class="docs-nav">
<li><a href="/legal/privacy"><span class="nav-ico"></span> Privacy &amp; cookies</a></li>
</ul>
<p class="docs-sidebar-title">IP &amp; open source</p>
<ul class="docs-nav">
<li><a href="/legal/license"><span class="nav-ico"></span> AGPL-3.0 &amp; notices</a></li>
<li><a href="/legal/copyright"><span class="nav-ico">©</span> Copyright &amp; DMCA</a></li>
</ul>
<p class="docs-sidebar-title">Risk &amp; safety</p>
<ul class="docs-nav">
<li><a href="/legal/disclaimer"><span class="nav-ico">!</span> Disclaimer</a></li>
<li><a href="/legal/security"><span class="nav-ico"></span> Security policy</a></li>
</ul>
<p class="docs-sidebar-title">Product</p>
<ul class="docs-nav">
<li><a href="/learn/"><span class="nav-ico"></span> How it works</a></li>
<li><a href="/docs/"><span class="nav-ico"></span> Documentation</a></li>
<li><a href="/community"><span class="nav-ico"></span> Community</a></li>
<li><a href="https://join.discord-linux.com" target="_blank" rel="noopener"><span class="nav-ico">💬</span> Discord-Linux</a></li>
<li><a href="/download"><span class="nav-ico"></span> Download</a></li>
</ul>`
}
}
function ensureBackdrop(className) {
let el = document.querySelector(`.${className}`)
if (!el) {
el = document.createElement('button')
el.type = 'button'
el.className = className
el.setAttribute('aria-label', 'Close menu')
document.body.appendChild(el)
}
return el
}
function setNavOpen(nav, toggle, open) {
nav.classList.toggle('open', open)
toggle?.setAttribute('aria-expanded', open ? 'true' : 'false')
document.body.classList.toggle('nav-open', open)
const backdrop = ensureBackdrop('nav-backdrop')
backdrop.classList.toggle('is-visible', open)
}
function initNav() {
const nav = document.querySelector('.site-nav')
const toggle = document.querySelector('.nav-toggle')
if (!nav) return
const onScroll = () => {
nav.classList.toggle('scrolled', window.scrollY > 12 || nav.classList.contains('open'))
}
onScroll()
window.addEventListener('scroll', onScroll, { passive: true })
const backdrop = ensureBackdrop('nav-backdrop')
backdrop.addEventListener('click', () => setNavOpen(nav, toggle, false))
if (toggle) {
toggle.addEventListener('click', () => {
const open = !nav.classList.contains('open')
setNavOpen(nav, toggle, open)
onScroll()
})
}
// Close mobile menu when a link is followed
nav.querySelectorAll('a').forEach((a) => {
a.addEventListener('click', () => setNavOpen(nav, toggle, false))
})
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && nav.classList.contains('open')) {
setNavOpen(nav, toggle, false)
}
})
// Active link by path (+ hash for homepage section links)
const normalize = (p) => {
let s = (p || '/').replace(/\/index\.html$/i, '').replace(/\.html$/i, '')
if (s.length > 1) s = s.replace(/\/$/, '')
return s || '/'
}
const syncActiveNav = () => {
const path = normalize(location.pathname)
const hash = location.hash || ''
document.querySelectorAll('.nav-links a, .docs-nav a, .docs-sidebar-brand').forEach((a) => {
a.classList.remove('active')
try {
const url = new URL(a.href, location.origin)
const target = normalize(url.pathname)
const linkHash = url.hash || ''
// Docs sidebar / brand: exact path only
if (a.closest('.docs-nav') || a.classList.contains('docs-sidebar-brand')) {
if (path === target) a.classList.add('active')
return
}
// Top nav section links (e.g. /#features): only when hash matches
if (linkHash) {
if (path === target && hash === linkHash) a.classList.add('active')
return
}
// Full-page links (Docs, Download, Releases, Legal, …)
// Do not mark home "/" for every section; only exact path match without hash requirement
if (path === target) {
// On homepage with a section hash, don't highlight plain "/" page links either
// (there is no plain Home item in top nav currently).
a.classList.add('active')
}
} catch {
// ignore
}
})
}
syncActiveNav()
window.addEventListener('hashchange', syncActiveNav)
}
function initReveal() {
const els = document.querySelectorAll('.reveal')
if (!els.length || !('IntersectionObserver' in window)) {
els.forEach((el) => el.classList.add('visible'))
return
}
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
e.target.classList.add('visible')
io.unobserve(e.target)
}
}
},
{ threshold: 0.12, rootMargin: '0px 0px -40px 0px' }
)
els.forEach((el) => io.observe(el))
}
function isMermaidBlock(el) {
if (!el) return false
if (el.classList?.contains('mermaid')) return true
if (el.closest?.('.mermaid-wrap, .mermaid')) return true
const code = el.tagName === 'CODE' ? el : el.querySelector?.('code')
if (code?.classList?.contains('language-mermaid') || code?.classList?.contains('mermaid')) return true
return false
}
/**
* Collect mermaid diagram sources from the page and normalize to .mermaid nodes.
* Supports:
* <pre class="mermaid"></pre>
* <div class="mermaid"></div>
* <pre><code class="language-mermaid"></code></pre>
*/
function collectMermaidNodes() {
/** @type {HTMLElement[]} */
const targets = []
document.querySelectorAll('pre > code.language-mermaid, pre > code.mermaid').forEach((code) => {
const pre = code.parentElement
if (!pre || pre.tagName !== 'PRE') return
const div = document.createElement('div')
div.className = 'mermaid'
div.textContent = code.textContent || ''
const wrap = document.createElement('div')
wrap.className = 'mermaid-wrap'
wrap.setAttribute('role', 'img')
wrap.setAttribute('aria-label', 'Diagram')
pre.replaceWith(wrap)
wrap.appendChild(div)
targets.push(div)
})
document.querySelectorAll('pre.mermaid').forEach((pre) => {
const div = document.createElement('div')
div.className = 'mermaid'
div.textContent = pre.textContent || ''
const wrap = document.createElement('div')
wrap.className = 'mermaid-wrap'
wrap.setAttribute('role', 'img')
wrap.setAttribute('aria-label', 'Diagram')
pre.replaceWith(wrap)
wrap.appendChild(div)
targets.push(div)
})
document.querySelectorAll('div.mermaid').forEach((div) => {
if (div.closest('.mermaid-wrap')) {
if (!targets.includes(div)) targets.push(div)
return
}
// already a bare .mermaid div
if (!div.parentElement?.classList.contains('mermaid-wrap')) {
const wrap = document.createElement('div')
wrap.className = 'mermaid-wrap'
wrap.setAttribute('role', 'img')
wrap.setAttribute('aria-label', 'Diagram')
div.parentNode.insertBefore(wrap, div)
wrap.appendChild(div)
}
if (!targets.includes(div)) targets.push(div)
})
return targets
}
/**
* Lazy-load mermaid and render diagrams (dark theme matched to site).
*/
async function initMermaid() {
const nodes = collectMermaidNodes()
if (!nodes.length) return
try {
const { default: mermaid } = await import('mermaid')
mermaid.initialize({
startOnLoad: false,
securityLevel: 'strict',
theme: 'dark',
fontFamily: 'Inter, system-ui, sans-serif',
themeVariables: {
darkMode: true,
background: '#0f1117',
primaryColor: '#134e4a',
primaryTextColor: '#f4f7fb',
primaryBorderColor: '#2dd4bf',
secondaryColor: '#1c2128',
secondaryTextColor: '#c8d1df',
secondaryBorderColor: '#334155',
tertiaryColor: '#161b22',
tertiaryTextColor: '#c8d1df',
tertiaryBorderColor: '#2dd4bf55',
lineColor: '#5eead4',
textColor: '#e2e8f0',
mainBkg: '#1c2128',
nodeBorder: '#2dd4bf',
clusterBkg: '#0f1117',
clusterBorder: '#2dd4bf55',
titleColor: '#f4f7fb',
edgeLabelBackground: '#0f1117',
actorBkg: '#1c2128',
actorBorder: '#2dd4bf',
actorTextColor: '#f4f7fb',
signalColor: '#5eead4',
signalTextColor: '#f4f7fb',
labelBoxBkgColor: '#1c2128',
labelBoxBorderColor: '#2dd4bf',
labelTextColor: '#f4f7fb',
loopTextColor: '#c8d1df',
noteBkgColor: '#134e4a',
noteTextColor: '#f4f7fb',
noteBorderColor: '#2dd4bf',
activationBkgColor: '#0f766e',
sequenceNumberColor: '#042f2e',
},
flowchart: {
curve: 'basis',
padding: 16,
htmlLabels: true,
useMaxWidth: true,
},
sequence: {
useMaxWidth: true,
mirrorActors: false,
bottomMarginAdj: 8,
},
})
await mermaid.run({ nodes, suppressErrors: false })
nodes.forEach((n) => {
const wrap = n.closest('.mermaid-wrap')
if (wrap) wrap.classList.add('mermaid-wrap--ready')
})
} catch (err) {
console.warn('[peardock] mermaid render failed', err)
nodes.forEach((n) => {
const wrap = n.closest('.mermaid-wrap') || n
wrap.classList?.add('mermaid-wrap--error')
if (!wrap.querySelector?.('.mermaid-error-msg')) {
const msg = document.createElement('p')
msg.className = 'mermaid-error-msg'
msg.textContent = 'Diagram failed to render. Check Mermaid syntax.'
wrap.appendChild?.(msg)
}
})
}
}
function initCodeCopy() {
document.querySelectorAll('pre').forEach((pre) => {
if (pre.closest('.code-block')) return
if (isMermaidBlock(pre)) return
const wrap = document.createElement('div')
wrap.className = 'code-block'
pre.parentNode.insertBefore(wrap, pre)
wrap.appendChild(pre)
const btn = document.createElement('button')
btn.type = 'button'
btn.className = 'copy-btn'
btn.textContent = 'Copy'
btn.addEventListener('click', async () => {
const text = pre.innerText
try {
await navigator.clipboard.writeText(text)
btn.textContent = 'Copied'
btn.classList.add('copied')
setTimeout(() => {
btn.textContent = 'Copy'
btn.classList.remove('copied')
}, 1600)
} catch {
btn.textContent = 'Failed'
}
})
wrap.appendChild(btn)
})
}
function initLightbox() {
const shots = [...document.querySelectorAll('[data-lightbox]')]
if (!shots.length) return
/** @type {{ src: string, alt: string }[]} */
const gallery = shots.map((el) => ({
src: el.getAttribute('data-lightbox') || el.querySelector('img')?.src || '',
alt: el.querySelector('img')?.getAttribute('alt') || el.getAttribute('aria-label') || 'Screenshot',
})).filter((g) => g.src)
// Dedupe by src while keeping order (hero + grid may share images)
const seen = new Set()
const items = gallery.filter((g) => {
if (seen.has(g.src)) return false
seen.add(g.src)
return true
})
let index = 0
const box = document.createElement('div')
box.className = 'lightbox'
box.setAttribute('role', 'dialog')
box.setAttribute('aria-modal', 'true')
box.setAttribute('aria-label', 'Screenshot viewer')
box.innerHTML = `
<button type="button" class="lightbox-close" aria-label="Close">×</button>
<button type="button" class="lightbox-nav lightbox-prev" aria-label="Previous screenshot"></button>
<button type="button" class="lightbox-nav lightbox-next" aria-label="Next screenshot"></button>
<div class="lightbox-stage">
<img src="" alt="Screenshot" />
</div>
<div class="lightbox-footer">
<p class="lightbox-caption"></p>
<p class="lightbox-counter" aria-live="polite"></p>
</div>
`
document.body.appendChild(box)
const img = box.querySelector('img')
const caption = box.querySelector('.lightbox-caption')
const counter = box.querySelector('.lightbox-counter')
const prevBtn = box.querySelector('.lightbox-prev')
const nextBtn = box.querySelector('.lightbox-next')
const show = (i) => {
if (!items.length) return
index = ((i % items.length) + items.length) % items.length
const item = items[index]
img.src = item.src
img.alt = item.alt
if (caption) caption.textContent = item.alt
if (counter) counter.textContent = `${index + 1} / ${items.length}`
const multi = items.length > 1
prevBtn?.classList.toggle('is-hidden', !multi)
nextBtn?.classList.toggle('is-hidden', !multi)
}
const open = (i) => {
show(i)
box.classList.add('open')
document.body.classList.add('lightbox-open')
}
const close = () => {
box.classList.remove('open')
document.body.classList.remove('lightbox-open')
}
box.querySelector('.lightbox-close')?.addEventListener('click', close)
prevBtn?.addEventListener('click', (e) => {
e.stopPropagation()
show(index - 1)
})
nextBtn?.addEventListener('click', (e) => {
e.stopPropagation()
show(index + 1)
})
box.addEventListener('click', (e) => {
if (e.target === box || e.target.classList.contains('lightbox-stage')) close()
})
document.addEventListener('keydown', (e) => {
if (!box.classList.contains('open')) return
if (e.key === 'Escape') close()
if (e.key === 'ArrowLeft') show(index - 1)
if (e.key === 'ArrowRight') show(index + 1)
})
shots.forEach((el) => {
el.addEventListener('click', () => {
const src = el.getAttribute('data-lightbox') || el.querySelector('img')?.src
if (!src) return
const i = items.findIndex((g) => g.src === src)
open(i >= 0 ? i : 0)
})
// Keyboard accessibility on shot tiles
if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0')
if (!el.getAttribute('role')) el.setAttribute('role', 'button')
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
el.click()
}
})
})
}
function setDocsSidebarOpen(sidebar, open) {
sidebar.classList.toggle('open', open)
document.body.classList.toggle('docs-sidebar-open', open)
const backdrop = ensureBackdrop('docs-sidebar-backdrop')
backdrop.classList.toggle('is-visible', open)
}
function initDocsSidebar() {
const sidebar = document.querySelector('.docs-sidebar')
const toggle = document.querySelector('.docs-sidebar-toggle')
if (!sidebar || !toggle) return
const backdrop = ensureBackdrop('docs-sidebar-backdrop')
backdrop.addEventListener('click', () => setDocsSidebarOpen(sidebar, false))
toggle.addEventListener('click', () => {
setDocsSidebarOpen(sidebar, !sidebar.classList.contains('open'))
})
sidebar.querySelectorAll('a').forEach((a) => {
a.addEventListener('click', () => setDocsSidebarOpen(sidebar, false))
})
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && sidebar.classList.contains('open')) {
setDocsSidebarOpen(sidebar, false)
}
})
}
/** Mark active legal / docs nav items (exact path). */
function initLegalActiveNav() {
const normalize = (p) => {
let s = (p || '/').replace(/\/index\.html$/i, '').replace(/\.html$/i, '')
if (s.length > 1) s = s.replace(/\/$/, '')
return s || '/'
}
const path = normalize(location.pathname)
document.querySelectorAll('.docs-nav a').forEach((a) => {
try {
const target = normalize(new URL(a.href, location.origin).pathname)
if (path === target) a.classList.add('active')
} catch {
// ignore
}
})
}
/**
* Docs / legal page polish: body class + breadcrumb.
*/
function initDocsPage() {
const main = document.querySelector('.docs-main')
const layout = document.querySelector('.docs-layout')
if (!main || !layout) return
document.body.classList.add('is-docs')
const isLegal = Boolean(layout.classList.contains('legal-layout') || location.pathname.includes('/legal'))
if (isLegal) document.body.classList.add('is-legal')
if (main.querySelector('.docs-breadcrumb')) return
const h1 = main.querySelector(':scope > h1')
if (!h1) return
const title = (h1.textContent || '').trim() || (isLegal ? 'Legal' : 'Docs')
const path = location.pathname.replace(/\/$/, '') || '/'
const isOverview = isLegal
? /\/legal\/?(index\.html)?$/.test(path) || title === 'Legal'
: /\/docs\/?(index\.html)?$/.test(path) || title === 'Documentation'
const crumb = document.createElement('nav')
crumb.className = 'docs-breadcrumb'
crumb.setAttribute('aria-label', 'Breadcrumb')
const escape = (s) =>
String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
if (isLegal) {
if (isOverview) {
crumb.innerHTML = `<span class="docs-bc-current">Legal</span>`
} else {
crumb.innerHTML = `
<a href="/legal/">Legal</a>
<span class="docs-bc-sep" aria-hidden="true">/</span>
<span class="docs-bc-current">${escape(title)}</span>`
}
} else if (isOverview) {
crumb.innerHTML = `<span class="docs-bc-current">Documentation</span>`
} else {
crumb.innerHTML = `
<a href="/docs/">Docs</a>
<span class="docs-bc-sep" aria-hidden="true">/</span>
<span class="docs-bc-current">${escape(title)}</span>`
}
main.insertBefore(crumb, main.firstChild)
}
function setYear() {
document.querySelectorAll('[data-year]').forEach((el) => {
el.textContent = String(new Date().getFullYear())
})
}
document.addEventListener('DOMContentLoaded', () => {
injectChrome()
initNav()
initReveal()
// Mermaid before code-copy so diagram blocks are not wrapped as copyable code
initMermaid()
initCodeCopy()
initLightbox()
initDocsSidebar()
initDocsPage()
initLegalActiveNav()
setYear()
})
+298
View File
@@ -0,0 +1,298 @@
/* ─── Community page ─── */
.community-hero {
padding: calc(var(--nav-h) + 3.75rem) 0 3rem;
border-bottom: 1px solid var(--border);
background:
radial-gradient(ellipse 65% 50% at 50% -10%, rgba(45, 212, 191, 0.12), transparent 55%),
radial-gradient(ellipse 40% 40% at 10% 40%, rgba(88, 101, 242, 0.08), transparent 50%),
var(--bg-primary);
text-align: center;
}
.community-hero-inner {
max-width: min(640px, 100% - 2rem);
margin-inline: auto;
}
.community-hero h1 {
margin-bottom: 0.85rem;
}
.community-hero .lead {
margin: 0 auto;
max-width: 46ch;
}
.community-page {
max-width: min(920px, 100% - 2rem) !important;
width: min(920px, 100% - 2rem);
margin: 2.5rem auto 4rem;
box-sizing: border-box;
}
.community-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1.15rem;
margin-bottom: 3rem;
}
.community-card {
border: 1px solid var(--border);
border-radius: 18px;
padding: 1.5rem 1.45rem 1.45rem;
background: linear-gradient(165deg, rgba(28, 33, 40, 0.95), rgba(12, 14, 18, 0.95));
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
min-width: 0;
}
.community-card--discord {
border-color: rgba(88, 101, 242, 0.35);
background:
radial-gradient(ellipse 80% 60% at 100% 0%, rgba(88, 101, 242, 0.14), transparent 55%),
linear-gradient(165deg, rgba(28, 33, 40, 0.95), rgba(12, 14, 18, 0.95));
}
.community-card-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: grid;
place-items: center;
background: var(--accent-dim);
border: 1px solid var(--accent-border);
font-size: 1.15rem;
margin-bottom: 1rem;
}
.community-card--discord .community-card-icon {
background: rgba(88, 101, 242, 0.15);
border-color: rgba(88, 101, 242, 0.35);
}
.community-card h2 {
font-size: 1.25rem;
margin: 0 0 0.55rem;
letter-spacing: -0.02em;
}
.community-card > p {
font-size: 0.95rem;
color: var(--text-muted);
line-height: 1.6;
margin: 0 0 1rem;
flex: 1;
}
.community-list {
list-style: none;
margin: 0 0 1.25rem;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.community-list li {
font-size: 0.9rem;
color: var(--text-secondary);
padding-left: 1.15rem;
position: relative;
line-height: 1.45;
}
.community-list li::before {
content: '';
position: absolute;
left: 0;
top: 0.55em;
width: 0.4rem;
height: 0.4rem;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.12);
}
.community-card--discord .community-list li::before {
background: #818cf8;
box-shadow: 0 0 0 3px rgba(88, 101, 242, 0.15);
}
.community-card-actions {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
}
.community-section {
margin: 0 0 2.75rem;
scroll-margin-top: calc(var(--nav-h) + 1rem);
}
.community-section h2 {
font-size: 1.5rem;
margin: 0 0 0.65rem;
letter-spacing: -0.03em;
}
.community-section h3 {
font-size: 1.05rem;
margin: 1.5rem 0 0.65rem;
}
.community-lead {
color: var(--text-muted);
margin: 0 0 0.85rem;
font-size: 1.02rem;
}
.community-bullets {
margin: 0 0 1.25rem 1.2rem;
color: var(--text-secondary);
line-height: 1.65;
}
.community-bullets li {
margin-bottom: 0.35rem;
}
.community-bullets li::marker {
color: var(--accent);
}
.community-steps {
margin: 0 0 1.25rem 1.25rem;
color: var(--text-secondary);
line-height: 1.7;
}
.community-steps li {
margin-bottom: 0.55rem;
padding-left: 0.25rem;
}
.community-steps li::marker {
color: var(--accent);
font-weight: 700;
}
.community-steps code {
font-size: 0.88em;
}
.community-support-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.85rem;
margin-top: 1rem;
}
.community-support-card {
display: block;
padding: 1.15rem 1.15rem 1.2rem;
border-radius: 14px;
border: 1px solid var(--border);
background: rgba(15, 17, 23, 0.75);
color: inherit;
text-decoration: none;
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
min-width: 0;
}
.community-support-card:hover {
border-color: rgba(45, 212, 191, 0.35);
transform: translateY(-2px);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.22);
color: inherit;
text-decoration: none;
}
.community-support-card h3 {
margin: 0 0 0.4rem;
font-size: 1rem;
color: var(--text);
}
.community-support-card p {
margin: 0;
font-size: 0.88rem;
color: var(--text-muted);
line-height: 1.5;
}
.community-cta {
margin-top: 1rem;
padding: 2rem 1.5rem;
border-radius: 18px;
border: 1px solid var(--accent-border);
background:
radial-gradient(ellipse 80% 80% at 20% 0%, rgba(45, 212, 191, 0.14), transparent 55%),
radial-gradient(ellipse 50% 50% at 90% 100%, rgba(88, 101, 242, 0.1), transparent 50%),
linear-gradient(165deg, var(--bg-elevated), var(--bg-secondary));
text-align: center;
}
.community-cta h2 {
margin: 0 0 0.5rem;
font-size: 1.5rem;
}
.community-cta p {
margin: 0 auto 1.35rem;
max-width: 40ch;
color: var(--text-muted);
}
.community-cta-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
}
@media (max-width: 900px) {
.community-grid {
grid-template-columns: 1fr;
}
.community-support-grid {
grid-template-columns: 1fr 1fr;
}
.community-page {
max-width: min(100% - 1.25rem, 920px) !important;
width: min(100% - 1.25rem, 920px) !important;
}
}
@media (max-width: 560px) {
.community-hero {
padding: calc(var(--nav-h) + 2rem) 1rem 2rem;
text-align: left;
}
.community-hero-inner {
max-width: 100%;
}
.community-hero .lead {
margin-left: 0;
}
.community-support-grid {
grid-template-columns: 1fr;
}
.community-card-actions,
.community-cta-actions {
flex-direction: column;
}
.community-card-actions .btn,
.community-cta-actions .btn {
width: 100%;
}
}
+1214
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1018
View File
File diff suppressed because it is too large Load Diff
+460
View File
@@ -0,0 +1,460 @@
/* ─── Learn / How it works educational layout ─── */
.learn-hero {
padding: calc(var(--nav-h) + 3.75rem) 0 3rem;
border-bottom: 1px solid var(--border);
background:
radial-gradient(ellipse 70% 55% at 50% -10%, rgba(45, 212, 191, 0.14), transparent 55%),
radial-gradient(ellipse 40% 40% at 90% 20%, rgba(56, 189, 248, 0.06), transparent 50%),
var(--bg-primary);
}
.learn-hero-inner {
max-width: min(760px, 100% - 2rem);
margin-inline: auto;
text-align: center;
}
.learn-hero h1 {
margin-bottom: 1rem;
font-size: clamp(2rem, 5vw, 2.85rem);
}
.learn-hero .lead {
margin: 0 auto 1.5rem;
max-width: 48ch;
}
.learn-key {
display: inline-flex;
gap: 0.65rem;
align-items: flex-start;
text-align: left;
max-width: 40rem;
margin: 0 auto;
padding: 1rem 1.2rem;
border-radius: 14px;
border: 1px solid var(--accent-border);
background: rgba(45, 212, 191, 0.08);
font-size: 0.95rem;
color: var(--text-secondary);
line-height: 1.55;
}
.learn-key strong {
color: var(--accent);
display: block;
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 0.35rem;
}
.learn-page {
max-width: min(860px, 100% - 2rem) !important;
width: min(860px, 100% - 2rem);
margin: 0 auto 4rem;
padding-top: 2.5rem;
box-sizing: border-box;
}
.learn-toc {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin: 0 0 2.5rem;
padding: 0;
list-style: none;
justify-content: center;
}
.learn-toc a {
display: inline-flex;
align-items: center;
min-height: 36px;
padding: 0.35rem 0.8rem;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--bg-elevated);
color: var(--text-muted);
font-size: 0.82rem;
font-weight: 600;
text-decoration: none;
transition: all 0.15s;
}
.learn-toc a:hover {
color: var(--accent);
border-color: var(--accent-border);
}
.learn-section {
margin: 0 0 3.25rem;
scroll-margin-top: calc(var(--nav-h) + 1.25rem);
}
.learn-section > h2 {
font-size: clamp(1.45rem, 3vw, 1.85rem);
letter-spacing: -0.03em;
margin: 0 0 0.85rem;
padding-top: 0.25rem;
border-top: 1px solid var(--border);
padding-top: 2rem;
}
.learn-section:first-of-type > h2 {
border-top: none;
padding-top: 0.25rem;
}
.learn-section > .learn-lead {
font-size: 1.05rem;
color: var(--text-muted);
line-height: 1.7;
margin: 0 0 1.35rem;
}
.learn-section p {
color: var(--text-secondary);
line-height: 1.72;
margin: 0 0 1rem;
font-size: 0.98rem;
}
.learn-section h3 {
font-size: 1.1rem;
margin: 1.75rem 0 0.65rem;
letter-spacing: -0.02em;
}
.learn-section ul,
.learn-section ol {
margin: 0 0 1.15rem 1.2rem;
color: var(--text-secondary);
line-height: 1.65;
}
.learn-section li {
margin-bottom: 0.4rem;
}
.learn-section li::marker {
color: var(--accent);
}
/* Visual comparison cards */
.learn-compare {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin: 1.25rem 0 1.5rem;
}
.learn-card {
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.25rem 1.3rem;
background: linear-gradient(165deg, rgba(28, 33, 40, 0.95), rgba(12, 14, 18, 0.92));
}
.learn-card h4 {
font-size: 0.95rem;
margin: 0 0 0.55rem;
display: flex;
align-items: center;
gap: 0.45rem;
}
.learn-card p,
.learn-card li {
font-size: 0.9rem;
margin: 0;
color: var(--text-muted);
line-height: 1.55;
}
.learn-card ul {
margin: 0.5rem 0 0 1.1rem;
}
.learn-card--old {
border-color: rgba(248, 113, 113, 0.22);
}
.learn-card--old h4 {
color: #fca5a5;
}
.learn-card--new {
border-color: rgba(45, 212, 191, 0.28);
}
.learn-card--new h4 {
color: #5eead4;
}
/* Step grid (hole punch / discovery) */
.learn-steps {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.85rem;
margin: 1.25rem 0 1.5rem;
counter-reset: learn-step;
}
.learn-step {
position: relative;
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.15rem 1.1rem 1.15rem;
background: rgba(15, 17, 23, 0.75);
counter-increment: learn-step;
}
.learn-step::before {
content: counter(learn-step, decimal-leading-zero);
display: block;
font-family: var(--mono);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--accent);
margin-bottom: 0.5rem;
}
.learn-step h4 {
font-size: 0.95rem;
margin: 0 0 0.4rem;
}
.learn-step p {
font-size: 0.88rem;
margin: 0;
color: var(--text-muted);
line-height: 1.5;
}
/* Peer / DHT visual row */
.learn-peers {
display: grid;
grid-template-columns: 1fr auto 1fr auto 1fr;
gap: 0.65rem;
align-items: stretch;
margin: 1.35rem 0 1.5rem;
}
.learn-peer {
border: 1px solid var(--border);
border-radius: 14px;
padding: 1.1rem;
text-align: center;
background: linear-gradient(180deg, rgba(28, 33, 40, 0.9), rgba(15, 17, 23, 0.95));
}
.learn-peer--hub {
border-color: rgba(45, 212, 191, 0.35);
background: linear-gradient(180deg, rgba(45, 212, 191, 0.1), rgba(15, 17, 23, 0.95));
}
.learn-peer h4 {
font-size: 0.92rem;
margin: 0 0 0.35rem;
}
.learn-peer p {
font-size: 0.8rem;
margin: 0;
color: var(--text-faint);
line-height: 1.45;
}
.learn-arrow {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--accent);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
gap: 0.2rem;
min-width: 2.5rem;
}
.learn-arrow span {
font-size: 1.25rem;
line-height: 1;
}
/* Layer stack */
.learn-stack {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 1.25rem 0 1.5rem;
}
.learn-layer {
display: grid;
grid-template-columns: minmax(7rem, 9rem) 1fr;
gap: 0.85rem;
align-items: center;
padding: 0.9rem 1.1rem;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(15, 17, 23, 0.8);
}
.learn-layer-name {
font-family: var(--mono);
font-size: 0.78rem;
font-weight: 600;
color: #99f6e4;
}
.learn-layer p {
margin: 0;
font-size: 0.9rem;
color: var(--text-muted);
}
/* Glossary */
.learn-glossary {
display: grid;
gap: 0.75rem;
margin-top: 1rem;
}
.learn-glossary details {
border: 1px solid var(--border);
border-radius: 12px;
background: rgba(15, 17, 23, 0.7);
padding: 0.85rem 1.1rem;
}
.learn-glossary summary {
cursor: pointer;
font-weight: 650;
color: var(--text);
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
min-height: 1.5rem;
}
.learn-glossary summary::-webkit-details-marker {
display: none;
}
.learn-glossary summary::after {
content: '+';
color: var(--accent);
font-weight: 700;
font-size: 1.1rem;
}
.learn-glossary details[open] summary::after {
content: '';
}
.learn-glossary details p {
margin: 0.65rem 0 0.15rem;
font-size: 0.92rem;
color: var(--text-muted);
line-height: 1.6;
}
.learn-cta {
margin: 2.5rem 0 1rem;
padding: 1.75rem 1.5rem;
border-radius: 18px;
border: 1px solid var(--accent-border);
background:
radial-gradient(ellipse 80% 80% at 20% 0%, rgba(45, 212, 191, 0.12), transparent 55%),
linear-gradient(165deg, var(--bg-elevated), var(--bg-secondary));
text-align: center;
}
.learn-cta h2 {
margin: 0 0 0.55rem;
border: none;
padding: 0;
font-size: 1.45rem;
}
.learn-cta p {
margin: 0 auto 1.25rem;
max-width: 42ch;
color: var(--text-muted);
}
.learn-cta-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
}
.learn-page .mermaid-wrap {
margin: 1.35rem 0 1.5rem;
}
.learn-page .table-wrap {
margin: 1.15rem 0 1.35rem;
}
.learn-page .callout {
margin: 1.15rem 0 1.35rem;
}
@media (max-width: 900px) {
.learn-compare {
grid-template-columns: 1fr;
}
.learn-steps {
grid-template-columns: 1fr;
}
.learn-peers {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.learn-arrow {
flex-direction: row;
transform: rotate(90deg);
min-height: 1.5rem;
margin: 0.15rem 0;
}
.learn-layer {
grid-template-columns: 1fr;
gap: 0.35rem;
}
.learn-page {
max-width: min(100% - 1.25rem, 860px) !important;
width: min(100% - 1.25rem, 860px) !important;
padding-top: 1.5rem;
}
.learn-hero {
padding: calc(var(--nav-h) + 2rem) 1rem 2rem;
}
.learn-hero-inner {
text-align: left;
}
.learn-key {
margin-left: 0;
}
.learn-toc {
justify-content: flex-start;
}
}
+55
View File
@@ -0,0 +1,55 @@
/* ─── Legal area (extends docs layout) ─── */
.legal-kicker {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.72rem;
font-weight: 650;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
margin: 0 0 0.75rem;
}
.legal-updated {
font-size: 0.88rem;
color: var(--text-faint);
margin: -0.5rem 0 1.5rem;
}
.legal-main .docs-lead {
margin-bottom: 0.75rem;
}
.legal-notice {
margin-top: 0.5rem;
}
.legal-cards {
margin-top: 1.25rem;
}
/* Entity table: first column as labels */
.legal-main .table-wrap th[scope='row'] {
width: 38%;
text-transform: none;
letter-spacing: -0.01em;
font-size: 0.88rem;
font-weight: 600;
color: var(--text-muted);
background: rgba(28, 33, 40, 0.55);
}
.legal-layout .docs-sidebar-brand-text span {
letter-spacing: 0.04em;
}
/* Compact ordered legal sections */
.legal-main h2 {
margin-top: 2.25rem;
}
.legal-main ol > li {
margin-bottom: 0.5rem;
}
+399
View File
@@ -0,0 +1,399 @@
/* ─── Releases page ─── */
.releases-hero {
padding: calc(var(--nav-h) + 3.5rem) 0 2.5rem;
border-bottom: 1px solid var(--border);
background:
radial-gradient(ellipse 60% 50% at 50% 0%, rgba(45, 212, 191, 0.1), transparent 60%),
var(--bg-primary);
}
.releases-hero-inner {
text-align: center;
max-width: min(720px, 100% - 2rem);
margin-inline: auto;
}
.releases-hero h1 {
margin-bottom: 0.75rem;
}
.releases-hero .lead {
margin: 0 auto 1.5rem;
max-width: 52ch;
}
.releases-hero-actions {
justify-content: center;
}
.releases-page {
max-width: min(920px, 100% - 2rem) !important;
width: min(920px, 100% - 2rem);
margin: 2rem auto 4rem;
box-sizing: border-box;
}
.releases-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
.releases-filters {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.releases-filter {
font-family: var(--font);
font-size: 0.82rem;
font-weight: 600;
padding: 0.4rem 0.85rem;
border-radius: var(--radius-full);
border: 1px solid var(--border);
background: var(--bg-elevated);
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
}
.releases-filter:hover {
color: var(--text);
border-color: var(--border-strong);
}
.releases-filter.is-active {
color: #042f2e;
background: linear-gradient(135deg, var(--accent), var(--accent-secondary));
border-color: transparent;
}
.releases-meta {
font-size: 0.85rem;
color: var(--text-muted);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
margin: 0;
}
.releases-meta a {
color: var(--accent);
}
.releases-list {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.releases-skeleton {
height: 180px;
border-radius: var(--radius-lg);
border: 1px solid var(--border);
background: linear-gradient(
90deg,
rgba(28, 33, 40, 0.5) 0%,
rgba(45, 212, 191, 0.06) 50%,
rgba(28, 33, 40, 0.5) 100%
);
background-size: 200% 100%;
animation: releases-shimmer 1.2s ease infinite;
}
@keyframes releases-shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
@media (prefers-reduced-motion: reduce) {
.releases-skeleton {
animation: none;
}
}
.releases-error {
padding: 1.15rem 1.25rem;
border-radius: var(--radius);
border: 1px solid rgba(248, 113, 113, 0.35);
background: rgba(248, 113, 113, 0.08);
color: var(--text-secondary);
margin-bottom: 1.25rem;
}
/* Never show an empty error shell (was rendering as a red bar under filters) */
.releases-error.hidden,
.releases-error:empty {
display: none !important;
padding: 0;
margin: 0;
border: none;
background: none;
}
.releases-error strong {
color: #fca5a5;
}
.releases-error-detail {
margin-top: 0.75rem;
font-size: 0.8rem;
max-height: 8rem;
overflow: auto;
}
.release-card {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: linear-gradient(165deg, rgba(28, 33, 40, 0.92), rgba(15, 17, 23, 0.95));
padding: 1.35rem 1.4rem 1.4rem;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
}
.release-card--rolling {
border-color: rgba(45, 212, 191, 0.35);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(45, 212, 191, 0.08);
}
.release-card-header {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border);
}
.release-card-badges {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-bottom: 0.45rem;
}
.release-card-title {
font-size: 1.2rem;
font-weight: 700;
letter-spacing: -0.025em;
margin: 0 0 0.35rem;
}
.release-card-title a {
color: var(--text);
text-decoration: none;
}
.release-card-title a:hover {
color: var(--accent);
}
.release-card-sub {
margin: 0;
font-size: 0.88rem;
color: var(--text-muted);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
}
.release-tag,
.release-sha {
font-size: 0.82em;
color: #99f6e4;
}
.release-body {
font-size: 0.92rem;
color: var(--text-secondary);
line-height: 1.65;
margin-bottom: 1.15rem;
}
.release-body h3,
.release-body h4 {
font-size: 0.98rem;
margin: 1rem 0 0.45rem;
color: var(--text);
}
.release-body p {
margin: 0 0 0.65rem;
}
.release-body ul {
margin: 0 0 0.75rem 1.15rem;
}
.release-body pre,
.release-body .release-notes-pre {
margin: 0.65rem 0 0.85rem;
font-size: 0.8rem;
}
.release-notes-li {
margin: 0.15rem 0 0.15rem 0.15rem;
line-height: 1.55;
}
.release-notes-gap {
height: 0.45rem;
}
.release-body p {
margin: 0 0 0.4rem;
}
.release-assets-title {
font-size: 0.78rem;
font-weight: 650;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-faint);
margin: 0 0 0.65rem;
}
.release-asset-list {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.release-asset {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
padding: 0.65rem 0.85rem;
border-radius: 10px;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.28);
color: inherit;
text-decoration: none;
transition: border-color 0.15s, background 0.15s;
}
.release-asset:hover {
border-color: rgba(45, 212, 191, 0.35);
background: rgba(45, 212, 191, 0.06);
color: inherit;
}
.release-asset-name code {
font-size: 0.8rem;
color: var(--text-secondary);
word-break: break-all;
background: none;
border: none;
padding: 0;
}
.release-asset:hover .release-asset-name code {
color: #99f6e4;
}
.release-asset-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
font-size: 0.78rem;
color: var(--text-muted);
}
.release-asset--checksum {
opacity: 0.85;
}
@media (max-width: 900px) {
.releases-page {
max-width: min(100% - 1.25rem, 920px) !important;
width: min(100% - 1.25rem, 920px) !important;
margin-top: 1.25rem;
}
.releases-toolbar {
flex-direction: column;
align-items: stretch;
gap: 0.85rem;
}
.releases-filters {
width: 100%;
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
padding-bottom: 0.15rem;
}
.releases-filter {
flex: 0 0 auto;
min-height: 40px;
white-space: nowrap;
}
.releases-hero-actions {
flex-direction: column;
align-items: stretch;
}
.releases-hero-actions .btn {
width: 100%;
}
}
@media (max-width: 640px) {
.releases-hero {
padding: calc(var(--nav-h) + 1.5rem) max(1rem, env(safe-area-inset-right)) 1.75rem max(1rem, env(safe-area-inset-left));
}
.releases-hero-inner {
max-width: 100%;
text-align: left;
}
.releases-hero .lead {
margin-left: 0;
margin-right: 0;
}
.release-card {
padding: 1rem 0.95rem;
}
.release-card-header {
flex-direction: column;
gap: 0.75rem;
}
.release-card-actions {
width: 100%;
}
.release-card-actions .btn {
width: 100%;
}
.release-asset {
flex-direction: column;
align-items: flex-start;
min-height: 48px;
}
.release-asset-name code {
font-size: 0.74rem;
}
}
+63
View File
@@ -0,0 +1,63 @@
import { defineConfig } from 'vite'
import { resolve } from 'path'
export default defineConfig({
root: '.',
publicDir: 'public',
build: {
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
download: resolve(__dirname, 'download.html'),
releases: resolve(__dirname, 'releases.html'),
learn: resolve(__dirname, 'learn/index.html'),
community: resolve(__dirname, 'community.html'),
docs: resolve(__dirname, 'docs/index.html'),
quickstart: resolve(__dirname, 'docs/quickstart.html'),
architecture: resolve(__dirname, 'docs/architecture.html'),
operator: resolve(__dirname, 'docs/operator.html'),
holesail: resolve(__dirname, 'docs/holesail.html'),
security: resolve(__dirname, 'docs/security.html'),
release: resolve(__dirname, 'docs/release.html'),
api: resolve(__dirname, 'docs/api.html'),
faq: resolve(__dirname, 'docs/faq.html'),
legal: resolve(__dirname, 'legal/index.html'),
legalTerms: resolve(__dirname, 'legal/terms.html'),
legalEula: resolve(__dirname, 'legal/eula.html'),
legalPrivacy: resolve(__dirname, 'legal/privacy.html'),
legalLicense: resolve(__dirname, 'legal/license.html'),
legalAup: resolve(__dirname, 'legal/aup.html'),
legalCookies: resolve(__dirname, 'legal/cookies.html'),
legalCopyright: resolve(__dirname, 'legal/copyright.html'),
legalDisclaimer: resolve(__dirname, 'legal/disclaimer.html'),
legalSecurity: resolve(__dirname, 'legal/security.html'),
},
},
},
server: {
port: 5173,
open: true,
proxy: {
// Mirror production /api/releases → Gitea (see server.mjs)
'/api/releases': {
target: 'https://git.ssh.surf',
changeOrigin: true,
rewrite: (p) => {
const base = '/api/v1/repos/snxraven/peardock'
if (p === '/api/releases' || p.startsWith('/api/releases?')) {
return p.replace('/api/releases', `${base}/releases`)
}
if (p.startsWith('/api/releases/tags/')) {
return p.replace('/api/releases/tags/', `${base}/releases/tags/`)
}
if (p.startsWith('/api/releases/latest')) {
return `${base}/releases?limit=50`
}
return p.replace(/^\/api\/releases/, `${base}/releases`)
},
},
},
},
})