| 1 | <!doctype html> |
| 2 | <html> |
| 3 | <head> |
| 4 | <title>Steve's Pi</title> |
| 5 | <meta charset="utf-8" /> |
| 6 | <meta |
| 7 | name="description" |
| 8 | content="A peek into Steve's Raspberry Pi and the services it runs" |
| 9 | /> |
| 10 | <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| 11 | |
| 12 | <meta property="og:type" content="website" /> |
| 13 | <meta property="og:title" content="Steve's Pi" /> |
| 14 | <meta |
| 15 | property="og:description" |
| 16 | content="A peek into Steve's Raspberry Pi and the services it runs" |
| 17 | /> |
| 18 | <meta property="og:url" content="https://pi.stevedylan.dev" /> |
| 19 | <meta property="og:site_name" content="Steve's Pi" /> |
| 20 | <meta property="og:image" content="https://stevedylan.dev/pi.png" /> |
| 21 | <meta property="og:image:width" content="1200" /> |
| 22 | <meta property="og:image:height" content="630" /> |
| 23 | |
| 24 | <link rel="icon" href="https://stevedylan.dev/favicon.ico" sizes="any" /> |
| 25 | <link |
| 26 | rel="icon" |
| 27 | href="https://stevedylan.dev/icon.svg" |
| 28 | type="image/svg+xml" |
| 29 | /> |
| 30 | <link rel="apple-touch-icon" href="/apple-touch-icon.png" /> |
| 31 | <style> |
| 32 | @font-face { |
| 33 | font-family: 'CommitMono'; |
| 34 | src: url('https://stevedylan.dev/CommitMono-400-Regular.otf') format('opentype'), |
| 35 | font-weight: normal; |
| 36 | font-style: normal; |
| 37 | } |
| 38 | html { |
| 39 | padding: 0; |
| 40 | margin: 0 1rem 0 1rem; |
| 41 | box-sizing: border-box; |
| 42 | background: #000000; |
| 43 | color: #FFFFFF; |
| 44 | font-family: 'CommitMono', sans-serif; |
| 45 | } |
| 46 | body { |
| 47 | display: flex; |
| 48 | justify-content: center; |
| 49 | align-items: center; |
| 50 | flex-direction: column; |
| 51 | min-height: 90vh; |
| 52 | max-width: 500px; |
| 53 | margin: auto; |
| 54 | font-size: 14px; |
| 55 | } |
| 56 | .stats-container { |
| 57 | display: flex; |
| 58 | flex-direction: column; |
| 59 | justify-content: flex-start; |
| 60 | } |
| 61 | .ipfs-div { |
| 62 | display: flex; |
| 63 | justify-content: flex-start; |
| 64 | align-items: center; |
| 65 | gap: 0.5rem; |
| 66 | } |
| 67 | .source-link { |
| 68 | padding-top: 2rem; |
| 69 | } |
| 70 | .link-container { |
| 71 | display: flex; |
| 72 | justify-content: flex-start; |
| 73 | align-items: center; |
| 74 | gap: 1rem; |
| 75 | } |
| 76 | .truncate { |
| 77 | display: inline-block; |
| 78 | max-width: calc(100% - 30px); /* Adjust based on the width of "ID: " */ |
| 79 | white-space: nowrap; |
| 80 | overflow: hidden; |
| 81 | text-overflow: ellipsis; |
| 82 | vertical-align: bottom; |
| 83 | } |
| 84 | p { |
| 85 | padding: 0; |
| 86 | margin: 0; |
| 87 | } |
| 88 | a { |
| 89 | color:#FFFFFF; |
| 90 | } |
| 91 | img { |
| 92 | height: 24px; |
| 93 | width: 24px; |
| 94 | } |
| 95 | @media (max-width: 480px) { |
| 96 | .truncate { |
| 97 | max-width: 300px; |
| 98 | } |
| 99 | } |
| 100 | </style> |
| 101 | </head> |
| 102 | <body> |
| 103 | <div class="stats-container"> |
| 104 | <h1>Steve's Pi</h1> |
| 105 | <p> |
| 106 | Welcome to a live feed of my Raspberry Pi! It sits on my desk and runs |
| 107 | multiple small services such as |
| 108 | <a href="https://ipfs.io" target="_blank">IPFS</a> and |
| 109 | <a href="https://radicle.xyz" target="_blank">Radicle</a>. |
| 110 | </p> |
| 111 | <div class="ipfs-div"> |
| 112 | <img src="https://dweb.mypinata.cloud/ipfs/QmbvEEN8zY657JC6wC2piMygmHcEKhwT5gkNWUs2qcnwKb" alt="ipfs-cube"> |
| 113 | <h3>IPFS Node</h3> |
| 114 | </div> |
| 115 | <p>RepoSize: <span id="repoSize">-</span></p> |
| 116 | <p>Objects: <span id="objects">-</span></p> |
| 117 | <p>Bandwidth In: <span id="rateIn">-</span></p> |
| 118 | <p>Bandwidth Out: <span id="rateOut">-</span></p> |
| 119 | <p>Total Data In: <span id="totalIn">-</span></p> |
| 120 | <p>Total Data Out: <span id="totalOut">-</span></p> |
| 121 | <div class="system-info"> |
| 122 | <div class="ipfs-div"> |
| 123 | <img src="https://dweb.mypinata.cloud/ipfs/QmVtXdzGAQYWYMFGCZR9XV1NFpT941F234Q4oaANTvPeqb" alt="radicle-alien"> |
| 124 | <h3>Radicle Node</h3> |
| 125 | </div> |
| 126 | <p>ID: <span class="truncate" id="radId">-</span></p> |
| 127 | <p>Agent: <span id="radAgent">-</span></p> |
| 128 | <p>Status: <span id="radStatus">-</span></p> |
| 129 | <p>Repos: <span id="radRepos">-</span></p> |
| 130 | <p>Seeding Policy: <span id="radPolicy">-</span></p> |
| 131 | </div> |
| 132 | <div class="system-info"> |
| 133 | <div class="ipfs-div"> |
| 134 | <img src="https://dweb.mypinata.cloud/ipfs/Qmaz5ih9noiMqNnAE5681cCPwGnomopKLUfF2LaSNmuV4P" alt="raspberry-pi-logo" /> |
| 135 | <h3>System Information</h3> |
| 136 | </div> |
| 137 | <p>OS: <span id="os">-</span></p> |
| 138 | <p>Kernel: <span id="kernel">-</span></p> |
| 139 | <p>Uptime: <span id="uptime">-</span></p> |
| 140 | <p>CPU Model: <span id="cpuModel">-</span></p> |
| 141 | <p>CPU Usage: <span id="cpuUsage">-</span>%</p> |
| 142 | <p> |
| 143 | Memory: <span id="memoryUsed">-</span>/<span id="memoryTotal">-</span> |
| 144 | </p> |
| 145 | </div> |
| 146 | <div class="link-container"> |
| 147 | <a |
| 148 | class="source-link" |
| 149 | href="http://github.com/stevedylandev/pi-widget" |
| 150 | target="_blank" |
| 151 | >Source Code</a |
| 152 | > |
| 153 | <a class="source-link" href="https://stevedylan.dev" target="_blank" |
| 154 | >Homepage</a |
| 155 | > |
| 156 | </div> |
| 157 | </div> |
| 158 | <script> |
| 159 | function formatBytes(bytes, decimals = 2) { |
| 160 | if (bytes === 0) return "0 Bytes"; |
| 161 | |
| 162 | const k = 1024; |
| 163 | const dm = decimals < 0 ? 0 : decimals; |
| 164 | const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; |
| 165 | |
| 166 | const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| 167 | |
| 168 | return ( |
| 169 | parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i] |
| 170 | ); |
| 171 | } |
| 172 | |
| 173 | function formatBitrate(bits) { |
| 174 | if (bits < 1000) { |
| 175 | return bits.toFixed(2) + " bps"; |
| 176 | } else if (bits < 1000000) { |
| 177 | return (bits / 1000).toFixed(2) + " Kbps"; |
| 178 | } else { |
| 179 | return (bits / 1000000).toFixed(2) + " Mbps"; |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | const evtSource = new EventSource("/events"); |
| 184 | |
| 185 | evtSource.onopen = function (event) { |
| 186 | console.log("SSE connection opened"); |
| 187 | }; |
| 188 | |
| 189 | evtSource.onerror = function (event) { |
| 190 | console.error("SSE connection error:", event); |
| 191 | }; |
| 192 | |
| 193 | evtSource.onmessage = function (event) { |
| 194 | console.log("Received data:", event.data); |
| 195 | try { |
| 196 | const data = JSON.parse(event.data); |
| 197 | // IPFS |
| 198 | document.getElementById("repoSize").textContent = formatBytes( |
| 199 | data.RepoSize, |
| 200 | ); |
| 201 | document.getElementById("objects").textContent = |
| 202 | data.NumObjects.toLocaleString(); |
| 203 | document.getElementById("rateIn").textContent = formatBitrate( |
| 204 | data.RateIn, |
| 205 | ); |
| 206 | document.getElementById("rateOut").textContent = formatBitrate( |
| 207 | data.RateOut, |
| 208 | ); |
| 209 | document.getElementById("totalIn").textContent = formatBytes( |
| 210 | data.TotalIn, |
| 211 | ); |
| 212 | document.getElementById("totalOut").textContent = formatBytes( |
| 213 | data.TotalOut, |
| 214 | ); |
| 215 | // Rad Node |
| 216 | document.getElementById("radId").textContent = data.id; |
| 217 | document.getElementById("radAgent").textContent = data.agent; |
| 218 | document.getElementById("radStatus").textContent = data.state; |
| 219 | document.getElementById("radPolicy").textContent = data.config.seedingPolicy.default; |
| 220 | document.getElementById("radRepos").textContent = data.repos.total.toString() |
| 221 | |
| 222 | // System |
| 223 | document.getElementById("os").textContent = data.os; |
| 224 | document.getElementById("kernel").textContent = data.kernel; |
| 225 | document.getElementById("uptime").textContent = data.uptime; |
| 226 | document.getElementById("cpuModel").textContent = data.cpuModel; |
| 227 | document.getElementById("cpuUsage").textContent = |
| 228 | data.cpuUsage.toFixed(2); |
| 229 | document.getElementById("memoryUsed").textContent = formatBytes( |
| 230 | data.memoryUsed, |
| 231 | ); |
| 232 | document.getElementById("memoryTotal").textContent = formatBytes( |
| 233 | data.memoryTotal, |
| 234 | ); |
| 235 | } catch (error) { |
| 236 | console.error("Error parsing or updating data:", error); |
| 237 | } |
| 238 | }; |
| 239 | </script> |
| 240 | </body> |
| 241 | </html> |