Lexicon-driven ATProto AppView in Gleam — port of HappyView
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="utf-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <meta name="color-scheme" content="dark light" />
7 <title>GleamView</title>
8 <link rel="stylesheet" href="/static/styles.css" />
9 <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230c1210'/%3E%3Cpath d='M8 22 L16 8 L24 22' fill='none' stroke='%235eead4' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='16' cy='20' r='2' fill='%23fbbf24'/%3E%3C/svg%3E" />
10 </head>
11 <body>
12 <div class="bg-glow" aria-hidden="true"></div>
13 <div id="app" class="shell">
14 <header class="top">
15 <div class="brand">
16 <div class="logo" aria-hidden="true"></div>
17 <div>
18 <h1>GleamView</h1>
19 <p class="tagline">Lexicon-driven ATProto AppView</p>
20 </div>
21 </div>
22 <div class="top-meta">
23 <span id="status-pill" class="pill muted">connecting…</span>
24 <button type="button" class="btn ghost" id="btn-refresh" title="Refresh">↻</button>
25 </div>
26 </header>
27
28 <nav class="tabs" role="tablist" aria-label="Main">
29 <button type="button" class="tab active" data-tab="feed" role="tab" aria-selected="true">Feed</button>
30 <button type="button" class="tab" data-tab="compose" role="tab">Compose</button>
31 <button type="button" class="tab" data-tab="oauth" role="tab">Sign in</button>
32 <button type="button" class="tab" data-tab="lexicons" role="tab">Lexicons</button>
33 <button type="button" class="tab" data-tab="admin" role="tab">Admin</button>
34 <button type="button" class="tab" data-tab="settings" role="tab">Settings</button>
35 </nav>
36
37 <main class="main">
38 <!-- Feed -->
39 <section id="panel-feed" class="panel active" role="tabpanel">
40 <div class="panel-head">
41 <div>
42 <h2>Posts</h2>
43 <p class="muted">Indexed records from <code>com.example.getPosts</code></p>
44 </div>
45 <button type="button" class="btn" id="btn-load-feed">Load feed</button>
46 </div>
47 <div id="feed-empty" class="empty">
48 Loading feed…
49 </div>
50 <div id="feed-list" class="card-list" hidden></div>
51 </section>
52
53 <!-- Compose -->
54 <section id="panel-compose" class="panel" role="tabpanel" hidden>
55 <div class="panel-head">
56 <div>
57 <h2>Create post</h2>
58 <p class="muted">
59 Calls <code>com.example.createPost</code>
60 (<span id="write-mode" class="pill sm">…</span>
61 </p>
62 </div>
63 </div>
64 <form id="form-compose" class="stack">
65 <label>
66 <span>Text</span>
67 <textarea id="compose-text" rows="4" maxlength="1000" placeholder="What's happening on the network?" required></textarea>
68 </label>
69 <label>
70 <span>Author DID / handle <span class="muted">(X-Gleamview-Did)</span></span>
71 <input id="compose-did" type="text" placeholder="did:plc:… or alice.bsky.social" autocomplete="username" />
72 </label>
73 <div class="row">
74 <button type="submit" class="btn primary">Publish</button>
75 <span id="compose-status" class="muted"></span>
76 </div>
77 </form>
78 <pre id="compose-result" class="code-block" hidden></pre>
79 </section>
80
81 <!-- OAuth -->
82 <section id="panel-oauth" class="panel" role="tabpanel" hidden>
83 <div class="panel-head">
84 <div>
85 <h2>ATProto sign-in</h2>
86 <p class="muted">OAuth with PKCE + PAR + DPoP. Session is stored server-side for PDS writes.</p>
87 </div>
88 </div>
89 <form id="form-oauth" class="stack">
90 <label>
91 <span>Handle</span>
92 <input id="oauth-handle" type="text" placeholder="you.bsky.social" required autocomplete="username" />
93 </label>
94 <div class="row">
95 <button type="submit" class="btn primary">Continue with OAuth</button>
96 <button type="button" class="btn ghost" id="btn-oauth-check">Check session</button>
97 <button type="button" class="btn ghost danger" id="btn-oauth-logout">Sign out</button>
98 </div>
99 </form>
100 <div id="oauth-session" class="card" hidden></div>
101 <p class="hint muted">
102 After login, set <code>ALLOW_LOCAL_WRITES=0</code> on the server and use your DID when composing
103 so procedures proxy to your PDS.
104 </p>
105 </section>
106
107 <!-- Lexicons -->
108 <section id="panel-lexicons" class="panel" role="tabpanel" hidden>
109 <div class="panel-head">
110 <div>
111 <h2>Lexicon registry</h2>
112 <p class="muted">Requires admin API key (Settings)</p>
113 </div>
114 <button type="button" class="btn" id="btn-load-lexicons">Refresh</button>
115 </div>
116 <div id="lexicon-list" class="card-list"></div>
117 </section>
118
119 <!-- Admin -->
120 <section id="panel-admin" class="panel" role="tabpanel" hidden>
121 <div class="panel-head">
122 <div>
123 <h2>Admin</h2>
124 <p class="muted">Stats, API clients, records</p>
125 </div>
126 <button type="button" class="btn" id="btn-load-admin">Refresh</button>
127 </div>
128
129 <div class="stats" id="admin-stats">
130 <div class="stat"><span class="stat-n" id="stat-records">—</span><span class="stat-l">records</span></div>
131 <div class="stat"><span class="stat-n" id="stat-lexicons">—</span><span class="stat-l">lexicons</span></div>
132 </div>
133
134 <h3>API clients</h3>
135 <form id="form-client" class="inline-form">
136 <input id="client-name" type="text" placeholder="client name" required />
137 <button type="submit" class="btn primary">Create client</button>
138 </form>
139 <div id="client-created" class="callout" hidden></div>
140 <div id="client-list" class="card-list"></div>
141
142 <h3>Recent records</h3>
143 <div id="record-list" class="card-list"></div>
144 </section>
145
146 <!-- Settings -->
147 <section id="panel-settings" class="panel" role="tabpanel" hidden>
148 <div class="panel-head">
149 <div>
150 <h2>Settings</h2>
151 <p class="muted">Optional keys for admin tools. The web UI itself needs none.</p>
152 </div>
153 </div>
154 <form id="form-settings" class="stack">
155 <label>
156 <span>Admin bearer token <span class="muted">(only for Lexicons / Admin tabs)</span></span>
157 <input id="set-admin-key" type="password" autocomplete="off" placeholder="gva_…" />
158 </label>
159 <label class="check">
160 <input id="set-show-admin" type="checkbox" />
161 <span>Show admin key</span>
162 </label>
163 <label>
164 <span>Default author DID / handle</span>
165 <input id="set-did" type="text" placeholder="did:plc:… or handle" autocomplete="username" />
166 </label>
167 <details class="muted-card" style="margin-top:0.5rem;padding:0.85rem 1rem">
168 <summary style="cursor:pointer;font-weight:650">Advanced — third-party API client key</summary>
169 <p class="muted" style="margin:0.5rem 0 0.65rem;font-weight:500">
170 Only if you call XRPC from another app and want a named client. The built-in web UI does not use this.
171 </p>
172 <label>
173 <span>API client key <code>X-Client-Key</code></span>
174 <input id="set-client-key" type="password" autocomplete="off" placeholder="hvc_…" />
175 </label>
176 <label class="check">
177 <input id="set-show-client" type="checkbox" />
178 <span>Show client key</span>
179 </label>
180 </details>
181 <div class="row">
182 <button type="submit" class="btn primary">Save</button>
183 <button type="button" class="btn ghost danger" id="btn-clear-settings">Clear all</button>
184 <span id="settings-status" class="muted"></span>
185 </div>
186 </form>
187 <div class="card muted-card">
188 <h3>Service config</h3>
189 <pre id="config-json" class="code-block">loading…</pre>
190 </div>
191 </section>
192 </main>
193
194 <footer class="foot">
195 <span>Port of <a href="https://tangled.org/gamesgamesgamesgames.games/happyview" target="_blank" rel="noreferrer">HappyView</a></span>
196 <span class="sep">·</span>
197 <a href="/health">/health</a>
198 <span class="sep">·</span>
199 <a href="/config">/config</a>
200 </footer>
201 </div>
202
203 <div id="toast" class="toast" hidden role="status"></div>
204 <script type="module" src="/static/app.js"></script>
205 </body>
206</html>