| 1 | {{/* The page shell owns the rail, content column, contextual panel and |
| 2 | footer. Page templates render one section selected by .Tab. The body works |
| 3 | before scripts load; tiny.js and bridge.js establish shared browser services, |
| 4 | then feature modules are loaded for the current page. */}} |
| 5 | {{define "page" }}<!doctype html> |
| 6 | <html lang="en" data-js="no" data-scripts="{{scriptsVersion}}" > |
| 7 | <head> |
| 8 | <meta charset="utf-8" > |
| 9 | <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" > |
| 10 | <meta name="color-scheme" content="light dark" > |
| 11 | <meta name="theme-color" content="#f4f2ec" media="(prefers-color-scheme: light)" > |
| 12 | <meta name="theme-color" content="#1e1d1a" media="(prefers-color-scheme: dark)" > |
| 13 | <meta name="mobile-web-app-capable" content="yes" > |
| 14 | <meta name="apple-mobile-web-app-capable" content="yes" > |
| 15 | <meta name="apple-mobile-web-app-status-bar-style" content="default" > |
| 16 | <meta name="apple-mobile-web-app-title" content="{{.Slug}}" > |
| 17 | <title>{{.Title}}</title> |
| 18 | <link rel="manifest" href="/manifest.webmanifest" > |
| 19 | <link rel="icon" href="/icon.svg" type="image/svg+xml" > |
| 20 | <link rel="apple-touch-icon" href="/apple-touch-icon.png" > |
| 21 | <style>{{stylesheet}}</style> |
| 22 | <script src="/fixi.js" ></script> |
| 23 | <script src="/signer.js" ></script> |
| 24 | <script src="/webmcp.js" ></script> |
| 25 | </head> |
| 26 | <body> |
| 27 | {{$mobile := mobileHeader .}}<header id="topbar" > |
| 28 | {{if $mobile.BackURL}}<a id="mobile-back" href="{{$mobile.BackURL}}" aria-label="{{$mobile.BackLabel}}" title="{{$mobile.BackLabel}}" ><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" ><path d="m14 6-6 6 6 6" /></svg></a>{{end}} |
| 29 | <details id="context-menu" name="mobile-chrome" ><summary id="context-toggle" aria-controls="aside" title="Page details" > |
| 30 | {{if $mobile.RoomID}}<room-avatar picture="{{$mobile.Picture}}" fallback="{{roomAvatarURL .URL $mobile.RoomID}}" aria-hidden="true" ><img src="{{if $mobile.Picture}}{{$mobile.Picture}}{{else}}{{roomAvatarURL .URL $mobile.RoomID}}{{end}}" alt="" width="36" height="36" loading="lazy" ></room-avatar>{{else if $mobile.PubKey}}<nostr-avatar pubkey="{{$mobile.PubKey}}" aria-hidden="true" ><img src="{{avatarURL $mobile.PubKey}}" alt="" width="36" height="36" loading="lazy" ></nostr-avatar>{{end}} |
| 31 | <span id="mobile-heading" ><strong id="mobile-title" role="heading" aria-level="1" >{{if $mobile.PubKey}}{{template "nostrName" $mobile.PubKey}}{{else}}{{$mobile.Title}}{{end}}</strong><small id="mobile-context" >{{$mobile.Subtitle}}</small></span><svg data-chevron viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" ><path d="m8 10 4 4 4-4" /></svg> |
| 32 | </summary></details> |
| 33 | <details id="nav-menu" name="mobile-chrome" ><summary id="menu" aria-controls="rail" aria-label="Menu" title="Menu" ><svg data-menu-icon viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" ><path d="M4 6h16M4 12h16M4 18h16" /></svg><svg data-close-icon viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" ><path d="m6 6 12 12M6 18 18 6" /></svg></summary></details> |
| 34 | {{if and .Actor (not .Private)}}<signer-connection actor="{{.Actor}}" hidden></signer-connection>{{end}} |
| 35 | </header> |
| 36 | <table id="layout" aria-label="{{.Slug}}" ><tbody><tr> |
| 37 | <td id="rail" ><div id="railbox" >{{template "rail" .}}</div></td> |
| 38 | <td id="main" ><div id="content" tabindex="-1" > |
| 39 | {{if .Private}}{{template "signin" .}} |
| 40 | {{else if eq .Tab "home" }}{{template "home" .}} |
| 41 | {{else if eq .Tab "inbox" }}{{template "inbox" .}} |
| 42 | {{else if eq .Tab "outbox" }}{{template "outbox" .}} |
| 43 | {{else if eq .Tab "search" }}{{template "search" .}} |
| 44 | {{else if eq .Tab "social" }}{{template "social" .}} |
| 45 | {{else if eq .Tab "social-thread" }}{{template "social-thread" .}} |
| 46 | {{else if eq .Tab "private" }}{{template "private" .}} |
| 47 | {{else if eq .Tab "chat" }}{{template "chat" .}} |
| 48 | {{else if eq .Tab "direct" }}{{template "direct" .}} |
| 49 | {{else if eq .Tab "media" }}{{template "media" .}} |
| 50 | {{else if eq .Tab "sites" }}{{template "sites" .}} |
| 51 | {{else if eq .Tab "marmot" }}{{template "marmot" .}} |
| 52 | {{else if eq .Tab "grasp" }}{{template "grasp" .}} |
| 53 | {{else if eq .Tab "terms" }}{{template "terms" .}} |
| 54 | {{else if eq .Tab "people" }}{{template "people" .}} |
| 55 | {{else if eq .Tab "agents" }}{{template "agents" .}} |
| 56 | {{else if eq .Tab "moderation" }}{{template "moderation" .}} |
| 57 | {{else if eq .Tab "rules" }}{{template "rules" .}} |
| 58 | {{else if eq .Tab "identity" }}{{template "identity" .}} |
| 59 | {{else if eq .Tab "signin" }}{{template "signin" .}} |
| 60 | {{else if eq .Tab "account" }}{{template "account" .}} |
| 61 | {{else if eq .Tab "connect" }}{{template "connect" .}} |
| 62 | {{else if eq .Tab "data" }}{{template "data" .}} |
| 63 | {{else if eq .Tab "sync" }}{{template "sync" .}} |
| 64 | {{else if eq .Tab "views" }}{{template "views" .}} |
| 65 | {{else if eq .Tab "health" }}{{template "health" .}} |
| 66 | {{else if eq .Tab "owner" }}{{template "owner" .}} |
| 67 | {{else if eq .Tab "repos" }}{{template "repos" .}} |
| 68 | {{else if eq .Tab "repo" }}{{template "repo" .}} |
| 69 | {{else if eq .Tab "files" }}{{template "files" .}} |
| 70 | {{else if eq .Tab "wiki" }}{{template "wiki" .}} |
| 71 | {{else if eq .Tab "wikipage" }}{{template "wikipage" .}} |
| 72 | {{else if eq .Tab "file" }}{{template "file" .}} |
| 73 | {{else if eq .Tab "approvals" }}{{template "approvals" .}} |
| 74 | {{else if eq .Tab "profile" }}{{template "profile" .}} |
| 75 | {{else if eq .Tab "rooms" }}{{template "rooms" .}} |
| 76 | {{else if eq .Tab "room" }}{{template "room" .}} |
| 77 | {{else if eq .Tab "thread" }}{{template "thread" .}} |
| 78 | {{else if eq .Tab "event" }}{{template "event" .}} |
| 79 | {{else if eq .Tab "view" }}{{template "view" .}} |
| 80 | {{else if eq .Tab "invite" }}{{template "invite" .}} |
| 81 | {{else}}{{template "home" .}} |
| 82 | {{end}}</div></td> |
| 83 | <td id="aside" ><div id="panel" >{{template "panel" .}}</div></td> |
| 84 | </tr></tbody></table> |
| 85 | <footer id="footer" ><table><tr><td><span id="prompt" ><b>{{.Slug}}</b> » <page-link url="{{pageAddress .URL .Path .Query}}" title="Copy the page address" >{{prompt .Path .Query}}</page-link></span></td><td><span id="who" >{{if .Private}}private relay{{else if .Actor}}signed in as <a href="/account" >{{template "nostrName" .Actor}}</a> (<button id="session-logout" type="button" >sign out</button>){{else}}browsing as guest (<a href="{{signinURL .Base .Path .Query}}" >Sign in</a>){{end}}</span> <span id="session-status" role="status" ></span><span id="navigation-status" role="status" aria-live="polite" ></span></td></tr></table><button id="install" type="button" hidden>install app</button></footer> |
| 86 | <script src="{{scriptURL " tiny.js"}}" ></script> |
| 87 | <script src="{{scriptURL " bridge.js"}}" ></script> |
| 88 | {{if or (eq .Tab "files" ) (eq .Tab "file" )}}<script src="{{scriptURL " blossom-encryption.js"}}" ></script> |
| 89 | <script src="{{scriptURL " file-catalog.js"}}" ></script> |
| 90 | <script src="{{scriptURL " blossom-manifests.js"}}" ></script> |
| 91 | <script src="{{scriptURL " blossom-upload.js"}}" ></script> |
| 92 | <script src="{{scriptURL " file-workspace.js"}}" ></script> |
| 93 | {{end}}<script src="{{scriptURL " nostr-name.js"}}" ></script> |
| 94 | <script src="{{scriptURL " components.js"}}" ></script> |
| 95 | <script src="{{scriptURL " rooms.js"}}" ></script> |
| 96 | <script src="{{scriptURL " file-messages.js"}}" ></script> |
| 97 | <script src="{{scriptURL " direct-messages.js"}}" ></script> |
| 98 | <script src="{{scriptURL " chat-files.js"}}" ></script> |
| 99 | <script src="{{scriptURL " chat.js"}}" ></script> |
| 100 | <script src="{{scriptURL " chat-presence.js"}}" ></script> |
| 101 | <script src="{{scriptURL " chat-activity.js"}}" ></script> |
| 102 | <script src="{{scriptURL " grant-request.js"}}" ></script> |
| 103 | {{if eq .Tab "connect" }}<script src="{{scriptURL " private-services.js"}}" ></script> |
| 104 | {{end}}</body> |
| 105 | </html> |
| 106 | {{end}} |
| 107 | |
| 108 | {{/* The rail is the sitemap for the current context: the relay, one repository, or management. */}} |
| 109 | {{define "rail" }}{{$kind := railKind .Tab}}{{if .Private}}<header><span><a id="mark" href="/" ><i>t</i><i>i</i><i>n</i><i>y</i></a></span></header> |
| 110 | <nav><a href="{{signinURL .Base .Path .Query}}" aria-current="page" >/signin</a></nav> |
| 111 | <p>Private relay. Sign in with a Nostr signer to continue.</p> |
| 112 | {{else if eq $kind "repo" }}<header><span><a href="/" >{{.Slug}}</a>/<a href="/repos" title="{{.Query.Get " owner"}}" >{{shortID (npub (.Query.Get "owner" ))}}</a>/<b>{{.Query.Get "repo" }}</b></span></header> |
| 113 | <nav>{{$v := repoView .Query}}<a href="{{repoURL .Query " home" " "}}" {{if eq $v "home" }} aria-current="page" {{end}}>/home</a><a href="{{repoURL .Query " tree" " "}}" {{if or (eq $v "tree" ) (eq $v "file" )}} aria-current="page" {{end}}>/code</a><a href="{{repoURL .Query " history" " "}}" {{if or (eq $v "history" ) (eq $v "commit" )}} aria-current="page" {{end}}>/history</a><a href="{{repoURL .Query " issues" " "}}" {{if or (eq $v "issues" ) (eq $v "issue" )}} aria-current="page" {{end}}>/issues</a><a href="{{repoURL .Query " prs" " "}}" {{if or (eq $v "prs" ) (eq $v "pr" )}} aria-current="page" {{end}}>/prs</a><a href="{{repoURL .Query " activity" " "}}" {{if eq $v "activity" }} aria-current="page" {{end}}>/activity</a></nav> |
| 114 | {{else if eq $kind "manage" }}<header><span><a href="/" >{{.Slug}}</a>/<b>manage</b></span></header> |
| 115 | {{range $group := navGroups manageItems}}<nav>{{range $item := $group}}<a href="{{$item.Href}}" {{if eq $item.Tab $.Tab}} aria-current="page" {{end}}>{{$item.Label}}</a>{{end}}</nav>{{end}} |
| 116 | {{else if eq $kind "rooms" }}<header><span><a href="/" >{{.Slug}}</a>/<a href="/chat" ><b>chat</b></a></span></header> |
| 117 | <nav aria-label="Chat" ><a href="/chat" {{if eq .Tab "chat" }} aria-current="page" {{end}}>All conversations</a></nav> |
| 118 | {{if .Actor}}<h4>Direct chats</h4><direct-chats actor="{{.Actor}}" relay="{{wsURL .URL}}" compact><p>Connect your signer to see direct chats.</p></direct-chats> |
| 119 | <p><a href="/chat#new-chat" >+ new chat</a></p>{{end}} |
| 120 | <h4>Rooms</h4> |
| 121 | <nav id="room-list" >{{range .Rooms}}{{$r := asMap .}}{{$id := str (index $r "id" )}}{{$picture := socialMediaURL (str (index $r "picture" ))}}<a href="/rooms/{{$id}}" {{if eq $id $.View}} aria-current="page" {{end}}><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true" ><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="24" height="24" loading="lazy" ></room-avatar>{{or (str (index $r "name" )) $id}}{{with age (index $r "last_message_at" )}} <small>{{.}}</small>{{end}}</a>{{else}}<a href="/chat" >Browse rooms</a>{{end}}</nav> |
| 122 | <p><a href="/chat#new-room" >+ new room</a></p> |
| 123 | {{else}}<header><span><a id="mark" href="/" title="{{.Slug}}" ><i>t</i><i>i</i><i>n</i><i>y</i></a></span></header> |
| 124 | {{range $group := navGroups relayItems}}<nav>{{range $item := $group}}<a href="{{$item.Href}}" {{if eq $item.Tab $.Tab}} aria-current="page" {{end}}>{{$item.Label}}</a>{{end}}</nav>{{end}}{{if .Actor}}<p id="personal-nav" ><a href="/account" {{if eq .Tab "account" }} aria-current="page" {{end}}>account</a></p>{{end}} |
| 125 | {{end}}<div id="mobile-actions" >{{if .Actor}}<a href="/account" >Account</a>{{end}}<page-link url="{{pageAddress .URL .Path .Query}}" title="Copy the page address" ><button type="button" >Copy page link</button></page-link></div><p id="rail-tools" >{{if eq $kind "repo" }}<a href="/repos" >← all repositories</a>{{else if or (eq $kind "manage" ) (eq $kind "rooms" )}}<a href="/" >← relay</a>{{end}}<button id="theme" type="button" aria-label="Switch theme" >theme</button></p>{{end}} |
| 126 | |
| 127 | {{/* The panel holds facts and filters for the current page. */}} |
| 128 | {{define "panel" }}{{$kind := railKind .Tab}}{{if .Private}}{{template "panel-signin" .}} |
| 129 | {{else if eq .Tab "repo" }}{{template "panel-repo" .}} |
| 130 | {{else if eq .Tab "agents" }}{{template "panel-agents" .}} |
| 131 | {{else if eq $kind "manage" }}{{template "panel-manage" .}} |
| 132 | {{else if or (eq .Tab "chat" ) (eq .Tab "direct" )}}{{template "panel-chat" .}} |
| 133 | {{else if eq $kind "rooms" }}{{template "panel-rooms" .}} |
| 134 | {{else if or (eq .Tab "social" ) (eq .Tab "social-thread" )}}{{template "panel-social" .}} |
| 135 | {{else if or (eq .Tab "inbox" ) (eq .Tab "outbox" ) (eq .Tab "search" ) (eq .Tab "articles" )}}{{template "panel-feed" .}} |
| 136 | {{else if eq .Tab "repos" }}{{template "panel-repos" .}} |
| 137 | {{else if or (eq .Tab "files" ) (eq .Tab "file" )}}{{template "panel-files" .}} |
| 138 | {{else if eq .Tab "approvals" }}{{template "panel-approvals" .}} |
| 139 | {{else if or (eq .Tab "wiki" ) (eq .Tab "wikipage" )}}{{template "panel-wiki" .}} |
| 140 | {{else if eq .Tab "signin" }}{{template "panel-signin" .}} |
| 141 | {{else if eq .Tab "account" }}{{template "panel-account" .}} |
| 142 | {{else}}{{template "panel-relay" .}} |
| 143 | {{end}}{{end}} |
| 144 | |
| 145 | {{define "panel-relay" }}<section><h4>About</h4><table><tr><th>identity</th><td>{{template "nostrKey" .Identity}}</td></tr><tr><th>reads</th><td>{{.Policy.Reads}}</td></tr><tr><th>writes</th><td>{{.Policy.Writes}}</td></tr><tr><th>search</th><td>{{.Policy.Features.Search}}</td></tr>{{if .Policy.Features.Grasp}}<tr><th>git</th><td>GRASP-01</td></tr>{{end}}{{if .Policy.Features.Files}}<tr><th>files</th><td>Blossom</td></tr>{{end}}</table></section> |
| 146 | <section><h4>Actions</h4><ul>{{if .Actor}}<li><a href="/profile" >profile</a></li>{{else}}<li><a href="{{signinURL .Base .Path .Query}}" >sign in</a></li>{{end}}<li><a href="/.well-known/nostr.json" >NIP-05 names</a></li>{{if .Policy.Features.Pages}}<li><a href="/feed.xml" >Atom feed</a></li>{{end}}<li><a href="/terms" >terms</a></li></ul></section> |
| 147 | <section><h4>Relay</h4><table><tr><th>url</th><td><code>{{.URL}}</code></td></tr></table></section>{{end}} |
| 148 | |
| 149 | {{define "panel-signin" }}<section><h4>Session</h4><table><tr><th>status</th><td>{{if .Actor}}signed in{{else}}guest{{end}}</td></tr><tr><th>method</th><td>NIP-98</td></tr></table></section> |
| 150 | <section><h4>After sign in</h4><ul><li>join chats</li><li>manage members and policy</li><li>upload files</li><li>push repositories</li></ul></section>{{end}} |
| 151 | |
| 152 | {{define "panel-account" }}<section><h4>Personal</h4><table><tr><th>identity</th><td>{{template "nostrKey" .Actor}}</td></tr><tr><th>profile</th><td><a href="/profile" >edit</a></td></tr></table></section><section><h4>Relay</h4><p><small>Your relay lists and chat preferences apply to this account.</small></p></section>{{end}} |
| 153 | |
| 154 | {{define "panel-repos" }}<section><h4>Hosting</h4><table><tr><th>server</th><td><code>{{.URL}}</code></td></tr><tr><th>protocol</th><td>GRASP-01</td></tr></table></section> |
| 155 | <section><h4>Publish</h4><p><small>Announce with ngit and push with plain git. Every push needs the owner's signed state.</small></p></section>{{end}} |
| 156 | |
| 157 | {{define "panel-files" }}<section><h4>Storage</h4><table><tr><th>protocol</th><td>Blossom</td></tr>{{if .Policy.Features.Files}}<tr><th>uploads</th><td>members</td></tr>{{end}}</table></section> |
| 158 | {{if .Actor}}<section><h4>Upload</h4><file-upload pubkey="{{.Actor}}" ></file-upload><p><small>Members-only files require membership to view or download.</small></p><details><summary>Import from URL</summary><file-mirror action="/mirror" ></file-mirror></details><noscript><p>Uploads need JavaScript and a connected signer.</p></noscript></section>{{end}}{{end}} |
| 159 | |
| 160 | {{define "panel-approvals" }}{{$counts := approvalCounts .Event}}{{$data := asMap .Event}}<section><h4>Waiting</h4><table><tr><th>open</th><td>{{index $counts "open" }}</td></tr><tr><th>oldest</th><td>{{with index $data "oldest_open" }}<time datetime="{{datetime .}}" >{{when .}}</time>{{else}}none{{end}}</td></tr><tr><th>answered</th><td>{{index $counts "answered" }}</td></tr><tr><th>expired</th><td>{{index $counts "expired" }}</td></tr></table></section> |
| 161 | <section><h4>Devices</h4>{{if .Actor}}{{range $index, $device := approvalDevices .Event}}<p><i></i>device {{add $index 1}} | {{$device}}</p>{{else}}<p>No device receives notifications for this key. Enable them in Chat.</p>{{end}}<p><small>Answers from the phone need the remembered signer.</small></p>{{else}}<p>Sign in to see which devices are notified.</p>{{end}}</section>{{end}} |
| 162 | |
| 163 | {{define "panel-feed" }}<section><h4>Filters</h4><form method="get" action="/{{.Tab}}" ><label for="filter-kinds" >kinds</label><select id="filter-kinds" name="kinds" ><option value="" >notes and articles</option><option value="1" {{if eq (.Query.Get "kinds" ) "1" }} selected{{end}}>1 notes</option><option value="30023" {{if eq (.Query.Get "kinds" ) "30023" }} selected{{end}}>30023 articles</option><option value="7" {{if eq (.Query.Get "kinds" ) "7" }} selected{{end}}>7 reactions</option><option value="all" {{if eq (.Query.Get "kinds" ) "all" }} selected{{end}}>everything</option></select><label for="filter-author" >author</label><input id="filter-author" name="author" placeholder="64 hex pubkey" value="{{.Query.Get " author"}}" ><label for="filter-since" >since</label><input id="filter-since" name="since" type="date" value="{{.Query.Get " since"}}" >{{if eq .Tab "search" }}<label for="filter-q" >search</label><input id="filter-q" name="q" value="{{.Query.Get " q"}}" >{{end}}<button>apply</button></form></section> |
| 164 | {{if .Actor}}<section><h4>You</h4><table><tr><th>key</th><td>{{template "nostrKey" .Actor}}</td></tr></table></section>{{end}} |
| 165 | {{if and .Actor (eq .Tab "inbox" )}}<section><h4>Notifications</h4><push-toggle></push-toggle><p><small>Relay notices for this key, on this device only.</small></p>{{if .Owner}}<rpc-form method="notifytest" ><button>Send a test</button></rpc-form>{{end}}<noscript><p>Enabling notifications needs JavaScript.</p></noscript></section>{{end}}{{end}} |
| 166 | |
| 167 | {{define "panel-repo" }}{{$r := asMap .Event}}{{with index $r "repository" }}{{$r = asMap .}}{{end}}<section><h4>About</h4><table><tr><th>owner</th><td>{{template "nostrKey" (str (index $r "owner" ))}}</td></tr><tr><th>visibility</th><td>{{if index $r "private" }}private{{else}}public{{end}}</td></tr>{{with index $r "commit" }}<tr><th>head</th><td><code title="{{.}}" >{{shortID .}}</code></td></tr>{{end}}</table></section> |
| 168 | {{with index $r "maintainers" }}<section><h4>Maintainers</h4><ul id="maintainers" >{{range .}}{{$m := asMap .}}<li>{{template "nostrName" (str (index $m "pubkey" ))}}{{$role := str (index $m "role" )}}{{if eq $role "agent" }} <small>agent{{with str (index $m "name" )}} {{.}}{{end}}</small>{{else if eq $role "owner" }} <small>owner</small>{{end}}</li>{{end}}</ul></section> |
| 169 | {{end}}<section><h4>Clone</h4><ul>{{range index $r "clone" }}<li><code>{{.}}</code></li>{{else}}<li>no clone URL advertised</li>{{end}}</ul></section> |
| 170 | {{with index $r "refs" }}<section><h4>Refs</h4><form method="get" action="/repo" ><input type="hidden" name="owner" value="{{$.Query.Get " owner"}}" ><input type="hidden" name="repo" value="{{$.Query.Get " repo"}}" ><input type="hidden" name="path" value="{{$.Query.Get " path"}}" ><input type="hidden" name="id" value="{{$.Query.Get " id"}}" ><input type="hidden" name="view" value="{{repoView $.Query}}" ><label for="ref-select" >branch or tag</label><select id="ref-select" name="ref" ><option value="" >default branch</option>{{range $name, $oid := .}}<option value="{{$name}}" {{if eq ($.Query.Get "ref" ) $name}} selected{{end}}>{{$name}}</option>{{end}}</select><button>go</button></form></section>{{end}}{{end}} |
| 171 | |
| 172 | {{define "panel-manage" }}<section><h4>Policy</h4><table><tr><th>name</th><td>{{.Policy.Name}}</td></tr><tr><th>reads</th><td>{{.Policy.Reads}}</td></tr><tr><th>writes</th><td>{{.Policy.Writes}}</td></tr><tr><th>owner</th><td>{{template "nostrKey" .Policy.Owner}}</td></tr></table></section> |
| 173 | {{if .Review}}<section><h4>People</h4><table><tr><th>requests</th><td>{{len (pendingRequests .Requests)}} pending</td></tr></table></section> |
| 174 | {{end}}<section><h4>Session</h4><table><tr><th>you</th><td>{{if .Actor}}{{template "nostrKey" .Actor}}{{else}}guest{{end}}</td></tr><tr><th>role</th><td>{{if .Owner}}owner{{else if .Actor}}member{{else}}none{{end}}</td></tr></table></section> |
| 175 | <section><h4>Result</h4><p><small>Each form shows its result below the form. Every call is signed by your connected signer.</small></p></section>{{end}} |
| 176 | |
| 177 | {{define "panel-agents" }}{{$counts := agentCounts .Feed}}{{$callbacks := callbackCounts .Callbacks}}<section><h4>Agents</h4><table><tr><th>active</th><td>{{index $counts "active" }}</td></tr><tr><th>paused</th><td>{{index $counts "paused" }}</td></tr><tr><th>revoked</th><td>{{index $counts "revoked" }}</td></tr><tr><th>callbacks</th><td>{{index $callbacks "active" }} active, {{index $callbacks "paused" }} paused</td></tr></table></section> |
| 178 | <section><h4>Machine access</h4><table><tr><th>mcp</th><td><code>/mcp</code> 2026-07-28</td></tr><tr><th>auth</th><td>NIP-98 per request</td></tr><tr><th>rest</th><td><code>/events</code> <code>/query</code></td></tr></table></section> |
| 179 | <section><h4>Kill switch</h4><rpc-form method="pauseallagents" refresh><button>Pause all agents</button></rpc-form><rpc-form method="resumeallagents" refresh><button>Resume all agents</button></rpc-form><p><small>Pausing keeps grants and rejects writes until resumed.</small></p></section> |
| 180 | <section><h4>Session</h4><table><tr><th>you</th><td>{{if .Actor}}{{template "nostrKey" .Actor}}{{else}}guest{{end}}</td></tr><tr><th>role</th><td>{{if .Owner}}owner{{else if .Actor}}member{{else}}none{{end}}</td></tr></table></section>{{end}} |
| 181 | {{define "panel-rooms" }}{{if eq .Tab "rooms" }}<section><h4>Rooms</h4><table><tr><th>rooms</th><td>{{len .Rooms}}</td></tr><tr><th>protocol</th><td>NIP-29</td></tr></table></section> |
| 182 | <section><h4>Actions</h4><ul>{{if .Actor}}<li><a href="/chat#new-room" >new room</a></li>{{else}}<li><a href="{{signinURL .Base .Path .Query}}" >sign in</a></li>{{end}}</ul></section> |
| 183 | {{else if .Error}}<section><h4>Room</h4><table><tr><th>id</th><td><code>{{.View}}</code></td></tr></table></section> |
| 184 | {{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room" )}}{{$id := str (index $room "id" )}}{{$role := str (index $room "role" )}}<section><h4>Room</h4><table><tr><th>id</th><td><code>{{$id}}</code></td></tr><tr><th>access</th><td>{{if eq (str (index $room "access" )) "members" }}members only{{else}}open{{end}}</td></tr><tr><th>created</th><td>{{short (index $room "created_at" )}}</td></tr>{{if $role}}<tr><th>you</th><td>{{$role}}</td></tr>{{end}}</table></section> |
| 185 | <section><h4>Members</h4><ul id="members" >{{range roomMembers .Event}}{{$m := asMap .}}<li{{if index $m "agent" }} data-agent{{with str (index $m "operator" )}} data-operator="{{.}}" {{end}}{{end}}>{{template "nostrName" (str (index $m "pubkey" ))}} <small>{{if index $m "agent" }}{{if index $m "operator" }}{{template "nostrName" (str (index $m "operator" ))}}'s agent{{else}}agent{{end}}{{else}}{{str (index $m "role")}}{{end}}</small></li>{{else}}<li>{{str (index $room "members")}} members</li>{{end}}</ul></section> |
| 186 | {{if eq .Tab "thread" }}<section><h4>Actions</h4><ul><li><a href="/rooms/{{$id}}" >open room</a></li></ul></section> |
| 187 | {{else}} |
| 188 | {{if .Actor}}<section><h4>Actions</h4>{{if roomAdmin $role}}<details><summary>invite member</summary><room-action room="{{$id}}" kind="9000" ><label>pubkey <input name="pubkey" placeholder="npub or 64 hex" required></label><label>role <select name="role" ><option value="member" >member</option><option value="admin" >admin</option>{{if eq $role "owner" }}<option value="owner" >owner</option>{{end}}</select></label><button>Add member</button></room-action></details> |
| 189 | <details><summary>room settings</summary><room-action room="{{$id}}" kind="9002" ><label>name <input name="name" value="{{str (index $room " name")}}" required maxlength="200" ></label><label>about <input name="about" value="{{str (index $room " about")}}" maxlength="2000" ></label><label>picture <input name="picture" value="{{str (index $room " picture")}}" placeholder="https://" ></label><label>access <select name="access" ><option value="open" {{if ne (str (index $room "access" )) "members" }} selected{{end}}>open</option><option value="members" {{if eq (str (index $room "access" )) "members" }} selected{{end}}>members only</option></select></label><button>Save</button></room-action></details> |
| 190 | {{end}}{{if $role}}<room-action room="{{$id}}" kind="9022" ><button>Leave room</button></room-action>{{else}}<room-action room="{{$id}}" kind="9021" ><button>Join room</button></room-action>{{end}}<noscript><p>Room actions need JavaScript and a connected signer.</p></noscript></section> |
| 191 | {{end}}{{end}}{{end}}{{end}} |
| 192 | |
| 193 | {{/* Shared partials. */}} |
| 194 | |
| 195 | {{define "alert" }}{{if .}}<p role="alert" >{{.}}</p>{{end}}{{end}} |
| 196 | |
| 197 | {{define "nostrKey" }}<nostr-key hex="{{.}}" title="{{.}}" >{{shortID .}}</nostr-key>{{end}} |
| 198 | {{/* nostrName shows a person: the vendored nostr-name element swaps the short id for the profile name published on this relay. */}} |
| 199 | {{define "nostrName" }}<nostr-name pubkey="{{.}}" title="{{.}}" >{{shortID .}}</nostr-name>{{end}} |
| 200 | |
| 201 | {{define "jsonView" }}<json-view><pre>{{json .}}</pre></json-view>{{end}} |
| 202 | |
| 203 | {{define "nostrEvent" }}{{$e := asMap .}}<nostr-event id="event-{{str (index $e " id")}}" > |
| 204 | <header><a href="/e/{{str (index $e " id")}}" >{{shortID (index $e "id" )}}</a> | kind {{str (index $e "kind" )}} | {{template "nostrName" (str (index $e "pubkey" ))}}<small><time datetime="{{datetime (index $e " created_at")}}" >{{when (index $e "created_at" )}}</time></small></header> |
| 205 | {{if eq (str (index $e "kind" )) "30023" }}<article>{{markdown (str (index $e "content" ))}}</article>{{else}}<pre>{{str (index $e "content" )}}</pre>{{end}} |
| 206 | <details><summary>event JSON</summary>{{template "jsonView" .}}</details> |
| 207 | </nostr-event>{{end}} |
| 208 | |
| 209 | {{define "eventList" }}{{if .Feed}}<ol id="events" > |
| 210 | {{range .Feed}}<li>{{template "nostrEvent" .}}</li> |
| 211 | {{end}}</ol> |
| 212 | {{else}}<p>No events.</p> |
| 213 | {{end}}{{end}} |