diff options
| author | bndw <ben@bdw.to> | 2026-02-13 20:53:22 -0800 |
|---|---|---|
| committer | bndw <ben@bdw.to> | 2026-02-13 20:53:22 -0800 |
| commit | 98a798f0cbd64ff0f003746e8b33c6666c61de64 (patch) | |
| tree | ffc847e42673062929bf2551bf1990a709d815be /internal/handler/websocket | |
| parent | 97e6c9a0c2c32bf514d3a4218d239741f1dc26c8 (diff) | |
redesign: cyber-brutalist terminal aesthetic
Complete visual overhaul with bold conceptual direction:
AESTHETIC: Cyber-brutalist terminal interface
- JetBrains Mono monospace throughout
- Deep black (#0a0e14) with cyan/green accents
- ASCII art Nostr logo with glitch animation
- Animated grid background (scrolling terminal feel)
- Terminal-style status bar with pulse indicators
- Protocol cards with scanning line effects
- Information-dense but organized layout
MOTION & EFFECTS:
- Glitching ASCII logo animation
- Scanning line on protocol cards
- Pulsing status indicators
- Animated grid background
- Staggered fade-in on page load
- Hover effects with glow
DIFFERENTIATION:
Feels like SSH into a relay node. Unapologetically technical,
embracing Nostr's decentralized, cypherpunk ethos. Zero generic
design patterns - full commitment to terminal aesthetic.
Diffstat (limited to 'internal/handler/websocket')
| -rw-r--r-- | internal/handler/websocket/index.go | 468 |
1 files changed, 367 insertions, 101 deletions
diff --git a/internal/handler/websocket/index.go b/internal/handler/websocket/index.go index 96bcfd4..861fbb1 100644 --- a/internal/handler/websocket/index.go +++ b/internal/handler/websocket/index.go | |||
| @@ -10,173 +10,439 @@ var indexTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE html> | |||
| 10 | <head> | 10 | <head> |
| 11 | <meta charset="UTF-8"> | 11 | <meta charset="UTF-8"> |
| 12 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | 12 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 13 | <title>Nostr gRPC Relay</title> | 13 | <title>NOSTR-GRPC-RELAY/v1.0.0</title> |
| 14 | <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| 15 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| 16 | <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet"> | ||
| 14 | <style> | 17 | <style> |
| 18 | :root { | ||
| 19 | --bg-primary: #0a0e14; | ||
| 20 | --bg-secondary: #151a23; | ||
| 21 | --bg-tertiary: #1f2531; | ||
| 22 | --cyan: #00ff9f; | ||
| 23 | --green: #39ff14; | ||
| 24 | --white: #e6e6e6; | ||
| 25 | --gray: #6b7280; | ||
| 26 | --red: #ff3366; | ||
| 27 | --border: #2a3142; | ||
| 28 | } | ||
| 29 | |||
| 15 | * { | 30 | * { |
| 16 | margin: 0; | 31 | margin: 0; |
| 17 | padding: 0; | 32 | padding: 0; |
| 18 | box-sizing: border-box; | 33 | box-sizing: border-box; |
| 19 | } | 34 | } |
| 35 | |||
| 20 | body { | 36 | body { |
| 21 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | 37 | font-family: 'JetBrains Mono', monospace; |
| 22 | line-height: 1.6; | 38 | background: var(--bg-primary); |
| 23 | color: #333; | 39 | color: var(--white); |
| 24 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||
| 25 | min-height: 100vh; | 40 | min-height: 100vh; |
| 26 | padding: 2rem; | 41 | overflow-x: hidden; |
| 42 | position: relative; | ||
| 27 | } | 43 | } |
| 44 | |||
| 45 | /* Animated grid background */ | ||
| 46 | body::before { | ||
| 47 | content: ''; | ||
| 48 | position: fixed; | ||
| 49 | top: 0; | ||
| 50 | left: 0; | ||
| 51 | width: 100%; | ||
| 52 | height: 100%; | ||
| 53 | background-image: | ||
| 54 | repeating-linear-gradient(0deg, transparent, transparent 2px, var(--border) 2px, var(--border) 3px), | ||
| 55 | repeating-linear-gradient(90deg, transparent, transparent 2px, var(--border) 2px, var(--border) 3px); | ||
| 56 | background-size: 50px 50px; | ||
| 57 | opacity: 0.15; | ||
| 58 | pointer-events: none; | ||
| 59 | animation: gridScroll 20s linear infinite; | ||
| 60 | z-index: 0; | ||
| 61 | } | ||
| 62 | |||
| 63 | @keyframes gridScroll { | ||
| 64 | 0% { transform: translate(0, 0); } | ||
| 65 | 100% { transform: translate(50px, 50px); } | ||
| 66 | } | ||
| 67 | |||
| 28 | .container { | 68 | .container { |
| 29 | max-width: 800px; | 69 | position: relative; |
| 70 | z-index: 1; | ||
| 71 | max-width: 1200px; | ||
| 30 | margin: 0 auto; | 72 | margin: 0 auto; |
| 31 | background: white; | 73 | padding: 2rem; |
| 32 | border-radius: 12px; | 74 | } |
| 33 | box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | 75 | |
| 34 | overflow: hidden; | 76 | /* Status bar */ |
| 77 | .status-bar { | ||
| 78 | display: flex; | ||
| 79 | justify-content: space-between; | ||
| 80 | align-items: center; | ||
| 81 | padding: 1rem; | ||
| 82 | background: var(--bg-secondary); | ||
| 83 | border: 1px solid var(--border); | ||
| 84 | margin-bottom: 2rem; | ||
| 85 | font-size: 0.75rem; | ||
| 86 | font-weight: 300; | ||
| 87 | animation: fadeIn 0.5s ease-out; | ||
| 88 | } | ||
| 89 | |||
| 90 | .status-item { | ||
| 91 | display: flex; | ||
| 92 | align-items: center; | ||
| 93 | gap: 0.5rem; | ||
| 35 | } | 94 | } |
| 95 | |||
| 96 | .status-indicator { | ||
| 97 | width: 8px; | ||
| 98 | height: 8px; | ||
| 99 | background: var(--green); | ||
| 100 | border-radius: 50%; | ||
| 101 | animation: pulse 2s ease-in-out infinite; | ||
| 102 | } | ||
| 103 | |||
| 104 | @keyframes pulse { | ||
| 105 | 0%, 100% { opacity: 1; } | ||
| 106 | 50% { opacity: 0.4; } | ||
| 107 | } | ||
| 108 | |||
| 109 | /* Header with ASCII art */ | ||
| 36 | .header { | 110 | .header { |
| 37 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | 111 | margin-bottom: 3rem; |
| 38 | color: white; | 112 | animation: fadeIn 0.8s ease-out 0.2s both; |
| 39 | padding: 2rem; | 113 | } |
| 40 | text-align: center; | 114 | |
| 115 | .ascii-logo { | ||
| 116 | font-size: 0.5rem; | ||
| 117 | line-height: 0.5rem; | ||
| 118 | color: var(--cyan); | ||
| 119 | white-space: pre; | ||
| 120 | margin-bottom: 1.5rem; | ||
| 121 | filter: drop-shadow(0 0 10px rgba(0, 255, 159, 0.3)); | ||
| 122 | animation: glitchLogo 3s ease-in-out infinite; | ||
| 41 | } | 123 | } |
| 124 | |||
| 125 | @keyframes glitchLogo { | ||
| 126 | 0%, 90%, 100% { transform: translate(0); } | ||
| 127 | 92% { transform: translate(-2px, 1px); } | ||
| 128 | 94% { transform: translate(2px, -1px); } | ||
| 129 | 96% { transform: translate(-1px, 2px); } | ||
| 130 | } | ||
| 131 | |||
| 42 | .header h1 { | 132 | .header h1 { |
| 43 | font-size: 2.5rem; | 133 | font-size: 2rem; |
| 134 | font-weight: 700; | ||
| 135 | color: var(--cyan); | ||
| 44 | margin-bottom: 0.5rem; | 136 | margin-bottom: 0.5rem; |
| 137 | letter-spacing: 0.1em; | ||
| 45 | } | 138 | } |
| 46 | .header p { | 139 | |
| 47 | font-size: 1.1rem; | 140 | .header .subtitle { |
| 48 | opacity: 0.9; | 141 | color: var(--gray); |
| 142 | font-size: 0.875rem; | ||
| 143 | font-weight: 300; | ||
| 49 | } | 144 | } |
| 50 | .content { | 145 | |
| 51 | padding: 2rem; | 146 | .header .subtitle::before { |
| 147 | content: '> '; | ||
| 148 | color: var(--green); | ||
| 52 | } | 149 | } |
| 53 | .section { | 150 | |
| 54 | margin-bottom: 2rem; | 151 | /* Protocol blocks */ |
| 152 | .protocols { | ||
| 153 | display: grid; | ||
| 154 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | ||
| 155 | gap: 1.5rem; | ||
| 156 | margin-bottom: 3rem; | ||
| 55 | } | 157 | } |
| 56 | .section h2 { | 158 | |
| 57 | color: #667eea; | 159 | .protocol-card { |
| 58 | margin-bottom: 1rem; | 160 | background: var(--bg-secondary); |
| 59 | font-size: 1.5rem; | 161 | border: 1px solid var(--border); |
| 60 | border-bottom: 2px solid #f0f0f0; | 162 | padding: 1.5rem; |
| 61 | padding-bottom: 0.5rem; | 163 | position: relative; |
| 164 | overflow: hidden; | ||
| 165 | animation: fadeInUp 0.8s ease-out backwards; | ||
| 62 | } | 166 | } |
| 63 | .protocol { | 167 | |
| 64 | background: #f8f9fa; | 168 | .protocol-card:nth-child(1) { animation-delay: 0.3s; } |
| 65 | padding: 1rem; | 169 | .protocol-card:nth-child(2) { animation-delay: 0.4s; } |
| 66 | border-radius: 8px; | 170 | .protocol-card:nth-child(3) { animation-delay: 0.5s; } |
| 171 | |||
| 172 | .protocol-card::before { | ||
| 173 | content: ''; | ||
| 174 | position: absolute; | ||
| 175 | top: 0; | ||
| 176 | left: 0; | ||
| 177 | width: 100%; | ||
| 178 | height: 2px; | ||
| 179 | background: linear-gradient(90deg, var(--cyan), transparent); | ||
| 180 | animation: scanLine 2s ease-in-out infinite; | ||
| 181 | } | ||
| 182 | |||
| 183 | @keyframes scanLine { | ||
| 184 | 0%, 100% { transform: translateX(-100%); } | ||
| 185 | 50% { transform: translateX(100%); } | ||
| 186 | } | ||
| 187 | |||
| 188 | .protocol-card:hover { | ||
| 189 | border-color: var(--cyan); | ||
| 190 | box-shadow: 0 0 20px rgba(0, 255, 159, 0.1); | ||
| 191 | transition: all 0.3s ease; | ||
| 192 | } | ||
| 193 | |||
| 194 | .protocol-header { | ||
| 195 | display: flex; | ||
| 196 | justify-content: space-between; | ||
| 197 | align-items: center; | ||
| 67 | margin-bottom: 1rem; | 198 | margin-bottom: 1rem; |
| 68 | border-left: 4px solid #667eea; | ||
| 69 | } | 199 | } |
| 70 | .protocol h3 { | 200 | |
| 71 | color: #333; | 201 | .protocol-name { |
| 72 | margin-bottom: 0.5rem; | 202 | font-size: 0.875rem; |
| 73 | font-size: 1.2rem; | 203 | font-weight: 700; |
| 204 | color: var(--green); | ||
| 205 | letter-spacing: 0.05em; | ||
| 74 | } | 206 | } |
| 75 | .protocol code { | 207 | |
| 76 | background: #e9ecef; | 208 | .protocol-status { |
| 77 | padding: 0.2rem 0.5rem; | 209 | font-size: 0.75rem; |
| 78 | border-radius: 4px; | 210 | color: var(--gray); |
| 79 | font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; | 211 | display: flex; |
| 80 | font-size: 0.9rem; | 212 | align-items: center; |
| 81 | color: #764ba2; | 213 | gap: 0.5rem; |
| 82 | } | 214 | } |
| 83 | .protocol p { | 215 | |
| 84 | margin: 0.5rem 0; | 216 | .protocol-status::before { |
| 85 | color: #666; | 217 | content: '●'; |
| 218 | color: var(--green); | ||
| 86 | } | 219 | } |
| 87 | .nips { | 220 | |
| 221 | .protocol-endpoint { | ||
| 222 | background: var(--bg-tertiary); | ||
| 223 | padding: 0.75rem; | ||
| 224 | margin: 1rem 0; | ||
| 225 | border-left: 2px solid var(--cyan); | ||
| 226 | font-size: 0.75rem; | ||
| 227 | color: var(--cyan); | ||
| 228 | word-break: break-all; | ||
| 229 | font-weight: 400; | ||
| 230 | } | ||
| 231 | |||
| 232 | .protocol-desc { | ||
| 233 | font-size: 0.75rem; | ||
| 234 | color: var(--gray); | ||
| 235 | font-weight: 300; | ||
| 236 | line-height: 1.5; | ||
| 237 | } | ||
| 238 | |||
| 239 | /* Info section */ | ||
| 240 | .info-grid { | ||
| 241 | display: grid; | ||
| 242 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||
| 243 | gap: 1.5rem; | ||
| 244 | margin-bottom: 3rem; | ||
| 245 | } | ||
| 246 | |||
| 247 | .info-card { | ||
| 248 | background: var(--bg-secondary); | ||
| 249 | border: 1px solid var(--border); | ||
| 250 | padding: 1.5rem; | ||
| 251 | animation: fadeInUp 0.8s ease-out 0.6s backwards; | ||
| 252 | } | ||
| 253 | |||
| 254 | .info-card h3 { | ||
| 255 | font-size: 0.75rem; | ||
| 256 | color: var(--cyan); | ||
| 257 | margin-bottom: 1rem; | ||
| 258 | letter-spacing: 0.1em; | ||
| 259 | font-weight: 700; | ||
| 260 | } | ||
| 261 | |||
| 262 | .nip-list { | ||
| 88 | display: flex; | 263 | display: flex; |
| 89 | flex-wrap: wrap; | 264 | flex-wrap: wrap; |
| 90 | gap: 0.5rem; | 265 | gap: 0.5rem; |
| 91 | } | 266 | } |
| 92 | .nip { | 267 | |
| 93 | background: #667eea; | 268 | .nip-tag { |
| 94 | color: white; | 269 | background: var(--bg-tertiary); |
| 95 | padding: 0.4rem 0.8rem; | 270 | border: 1px solid var(--border); |
| 96 | border-radius: 20px; | 271 | padding: 0.25rem 0.75rem; |
| 97 | font-size: 0.9rem; | 272 | font-size: 0.75rem; |
| 273 | color: var(--green); | ||
| 98 | font-weight: 500; | 274 | font-weight: 500; |
| 275 | transition: all 0.2s ease; | ||
| 276 | } | ||
| 277 | |||
| 278 | .nip-tag:hover { | ||
| 279 | border-color: var(--green); | ||
| 280 | box-shadow: 0 0 10px rgba(57, 255, 20, 0.2); | ||
| 281 | } | ||
| 282 | |||
| 283 | .feature-list { | ||
| 284 | list-style: none; | ||
| 285 | font-size: 0.75rem; | ||
| 286 | color: var(--gray); | ||
| 287 | font-weight: 300; | ||
| 288 | } | ||
| 289 | |||
| 290 | .feature-list li { | ||
| 291 | padding: 0.5rem 0; | ||
| 292 | border-bottom: 1px solid var(--border); | ||
| 293 | } | ||
| 294 | |||
| 295 | .feature-list li:last-child { | ||
| 296 | border-bottom: none; | ||
| 99 | } | 297 | } |
| 298 | |||
| 299 | .feature-list li::before { | ||
| 300 | content: '[✓] '; | ||
| 301 | color: var(--green); | ||
| 302 | margin-right: 0.5rem; | ||
| 303 | } | ||
| 304 | |||
| 305 | /* Footer */ | ||
| 100 | .footer { | 306 | .footer { |
| 101 | background: #f8f9fa; | ||
| 102 | padding: 1.5rem; | ||
| 103 | text-align: center; | 307 | text-align: center; |
| 104 | color: #666; | 308 | padding: 2rem; |
| 105 | font-size: 0.9rem; | 309 | border-top: 1px solid var(--border); |
| 310 | margin-top: 3rem; | ||
| 311 | font-size: 0.75rem; | ||
| 312 | color: var(--gray); | ||
| 313 | animation: fadeIn 0.8s ease-out 0.8s both; | ||
| 106 | } | 314 | } |
| 107 | .footer a { | 315 | |
| 108 | color: #667eea; | 316 | .footer pre { |
| 109 | text-decoration: none; | 317 | margin: 1rem 0; |
| 318 | color: var(--cyan); | ||
| 319 | opacity: 0.5; | ||
| 320 | font-size: 0.5rem; | ||
| 321 | line-height: 0.6rem; | ||
| 110 | } | 322 | } |
| 111 | .footer a:hover { | 323 | |
| 112 | text-decoration: underline; | 324 | @keyframes fadeIn { |
| 325 | from { opacity: 0; } | ||
| 326 | to { opacity: 1; } | ||
| 113 | } | 327 | } |
| 114 | @media (max-width: 600px) { | 328 | |
| 115 | body { | 329 | @keyframes fadeInUp { |
| 330 | from { | ||
| 331 | opacity: 0; | ||
| 332 | transform: translateY(20px); | ||
| 333 | } | ||
| 334 | to { | ||
| 335 | opacity: 1; | ||
| 336 | transform: translateY(0); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | |||
| 340 | @media (max-width: 768px) { | ||
| 341 | .container { | ||
| 116 | padding: 1rem; | 342 | padding: 1rem; |
| 117 | } | 343 | } |
| 344 | .ascii-logo { | ||
| 345 | font-size: 0.35rem; | ||
| 346 | line-height: 0.4rem; | ||
| 347 | } | ||
| 118 | .header h1 { | 348 | .header h1 { |
| 119 | font-size: 2rem; | 349 | font-size: 1.5rem; |
| 120 | } | 350 | } |
| 121 | .content { | 351 | .status-bar { |
| 122 | padding: 1.5rem; | 352 | flex-direction: column; |
| 353 | gap: 0.5rem; | ||
| 354 | align-items: flex-start; | ||
| 123 | } | 355 | } |
| 124 | } | 356 | } |
| 125 | </style> | 357 | </style> |
| 126 | </head> | 358 | </head> |
| 127 | <body> | 359 | <body> |
| 128 | <div class="container"> | 360 | <div class="container"> |
| 129 | <div class="header"> | 361 | <div class="status-bar"> |
| 130 | <h1>⚡ Nostr gRPC Relay</h1> | 362 | <div class="status-item"> |
| 131 | <p>High-performance relay with multi-protocol support</p> | 363 | <span class="status-indicator"></span> |
| 364 | <span>RELAY.ONLINE</span> | ||
| 365 | </div> | ||
| 366 | <div class="status-item"> | ||
| 367 | <span>PROTOCOLS: 3</span> | ||
| 368 | </div> | ||
| 369 | <div class="status-item"> | ||
| 370 | <span>NIPs: 01/09/11</span> | ||
| 371 | </div> | ||
| 132 | </div> | 372 | </div> |
| 133 | 373 | ||
| 134 | <div class="content"> | 374 | <div class="header"> |
| 135 | <div class="section"> | 375 | <pre class="ascii-logo"> |
| 136 | <h2>Protocols</h2> | 376 | ███▄ █ ▒█████ ██████ ▄▄▄█████▓ ██▀███ |
| 377 | ██ ▀█ █ ▒██▒ ██▒▒██ ▒ ▓ ██▒ ▓▒▓██ ▒ ██▒ | ||
| 378 | ▓██ ▀█ ██▒▒██░ ██▒░ ▓██▄ ▒ ▓██░ ▒░▓██ ░▄█ ▒ | ||
| 379 | ▓██▒ ▐▌██▒▒██ ██░ ▒ ██▒░ ▓██▓ ░ ▒██▀▀█▄ | ||
| 380 | ▒██░ ▓██░░ ████▓▒░▒██████▒▒ ▒██▒ ░ ░██▓ ▒██▒ | ||
| 381 | ░ ▒░ ▒ ▒ ░ ▒░▒░▒░ ▒ ▒▓▒ ▒ ░ ▒ ░░ ░ ▒▓ ░▒▓░ | ||
| 382 | ░ ░░ ░ ▒░ ░ ▒ ▒░ ░ ░▒ ░ ░ ░ ░▒ ░ ▒░ | ||
| 383 | ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ | ||
| 384 | ░ ░ ░ ░ ░ </pre> | ||
| 385 | <h1>NOSTR-GRPC-RELAY</h1> | ||
| 386 | <p class="subtitle">decentralized protocol node // multi-transport relay</p> | ||
| 387 | </div> | ||
| 137 | 388 | ||
| 138 | <div class="protocol"> | 389 | <div class="protocols"> |
| 139 | <h3>🔌 gRPC (Native Binary)</h3> | 390 | <div class="protocol-card"> |
| 140 | <p><code>{{.GrpcAddr}}</code></p> | 391 | <div class="protocol-header"> |
| 141 | <p>High-performance binary protocol for applications</p> | 392 | <span class="protocol-name">gRPC</span> |
| 393 | <span class="protocol-status">ACTIVE</span> | ||
| 142 | </div> | 394 | </div> |
| 395 | <div class="protocol-endpoint">{{.GrpcAddr}}</div> | ||
| 396 | <p class="protocol-desc">Native binary protocol. High-throughput RPC interface for application-layer integration. Protocol Buffers over HTTP/2.</p> | ||
| 397 | </div> | ||
| 143 | 398 | ||
| 144 | <div class="protocol"> | 399 | <div class="protocol-card"> |
| 145 | <h3>🌐 Connect (HTTP/JSON)</h3> | 400 | <div class="protocol-header"> |
| 146 | <p><code>{{.HttpAddr}}/nostr.v1.NostrRelay/*</code></p> | 401 | <span class="protocol-name">CONNECT</span> |
| 147 | <p>Browser-compatible gRPC over HTTP with JSON</p> | 402 | <span class="protocol-status">ACTIVE</span> |
| 148 | </div> | 403 | </div> |
| 404 | <div class="protocol-endpoint">{{.HttpAddr}}/nostr.v1.NostrRelay/*</div> | ||
| 405 | <p class="protocol-desc">Browser-compatible HTTP/JSON interface. gRPC-Web with Connect protocol. CORS-enabled for web clients.</p> | ||
| 406 | </div> | ||
| 149 | 407 | ||
| 150 | <div class="protocol"> | 408 | <div class="protocol-card"> |
| 151 | <h3>🔗 WebSocket (Nostr Protocol)</h3> | 409 | <div class="protocol-header"> |
| 152 | <p><code>ws://{{.WsAddr}}/</code></p> | 410 | <span class="protocol-name">WEBSOCKET</span> |
| 153 | <p>Standard Nostr protocol (NIP-01) for all clients</p> | 411 | <span class="protocol-status">ACTIVE</span> |
| 154 | </div> | 412 | </div> |
| 413 | <div class="protocol-endpoint">ws://{{.WsAddr}}/</div> | ||
| 414 | <p class="protocol-desc">Standard Nostr protocol (NIP-01). Compatible with all Nostr clients. Real-time event streaming.</p> | ||
| 155 | </div> | 415 | </div> |
| 416 | </div> | ||
| 156 | 417 | ||
| 157 | <div class="section"> | 418 | <div class="info-grid"> |
| 158 | <h2>Supported NIPs</h2> | 419 | <div class="info-card"> |
| 159 | <div class="nips"> | 420 | <h3>SUPPORTED_NIPS</h3> |
| 160 | <span class="nip">NIP-01</span> | 421 | <div class="nip-list"> |
| 161 | <span class="nip">NIP-09</span> | 422 | <span class="nip-tag">NIP-01</span> |
| 162 | <span class="nip">NIP-11</span> | 423 | <span class="nip-tag">NIP-09</span> |
| 424 | <span class="nip-tag">NIP-11</span> | ||
| 163 | </div> | 425 | </div> |
| 164 | </div> | 426 | </div> |
| 165 | 427 | ||
| 166 | <div class="section"> | 428 | <div class="info-card"> |
| 167 | <h2>Features</h2> | 429 | <h3>RELAY_FEATURES</h3> |
| 168 | <ul style="list-style-position: inside; color: #666;"> | 430 | <ul class="feature-list"> |
| 169 | <li>Binary-first storage (Protocol Buffers)</li> | 431 | <li>Binary-first storage</li> |
| 170 | <li>SQLite with WAL mode</li> | 432 | <li>SQLite WAL mode</li> |
| 171 | <li>Event validation (ID & signature)</li> | 433 | <li>Event validation</li> |
| 172 | <li>Real-time subscriptions</li> | 434 | <li>Real-time subscriptions</li> |
| 173 | <li>Event deletion (NIP-09)</li> | 435 | <li>Hard deletion (NIP-09)</li> |
| 174 | </ul> | 436 | </ul> |
| 175 | </div> | 437 | </div> |
| 176 | </div> | 438 | </div> |
| 177 | 439 | ||
| 178 | <div class="footer"> | 440 | <div class="footer"> |
| 179 | <p>Built with <a href="https://github.com/anthropics/claude-code" target="_blank">Claude Code</a></p> | 441 | <pre> |
| 442 | ┌─────────────────────────────────────┐ | ||
| 443 | │ DECENTRALIZED // CENSORSHIP-RESISTANT │ | ||
| 444 | └─────────────────────────────────────┘</pre> | ||
| 445 | <p>relay.status = online // uptime.monitor = true</p> | ||
| 180 | </div> | 446 | </div> |
| 181 | </div> | 447 | </div> |
| 182 | </body> | 448 | </body> |
