Skip to content

Commit bd5b9fa

Browse files
authored
Merge pull request #131 from keyxmakerx/claude/foundry-module-review-IrVxd
fix: API key creation double-navbar + show campaign UUID for Foundry
2 parents ff8c3cd + 828d853 commit bd5b9fa

3 files changed

Lines changed: 101 additions & 41 deletions

File tree

internal/plugins/campaigns/settings.templ

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,29 @@ templ CampaignSettingsPage(cc *CampaignContext, transfer *OwnershipTransfer, ent
156156
Install the Chronicle Sync module in Foundry VTT to sync journals, maps, tokens, and calendars
157157
with this campaign in real-time.
158158
</p>
159+
<div class="mb-4">
160+
<label class="block text-sm font-medium text-fg-body mb-1">Campaign ID</label>
161+
<div class="flex items-center gap-2">
162+
<input
163+
type="text"
164+
readonly
165+
value={ cc.Campaign.ID }
166+
class="input w-full font-mono text-sm bg-surface-alt"
167+
id="foundry-campaign-id"
168+
/>
169+
<button
170+
type="button"
171+
onclick="navigator.clipboard.writeText(document.getElementById('foundry-campaign-id').value).then(() => { this.innerHTML = '<i class=\'fa-solid fa-check text-emerald-500\'></i>'; setTimeout(() => { this.innerHTML = '<i class=\'fa-solid fa-copy\'></i>'; }, 2000); })"
172+
class="btn-secondary text-sm px-3 py-2 shrink-0"
173+
title="Copy to clipboard"
174+
>
175+
<i class="fa-solid fa-copy"></i>
176+
</button>
177+
</div>
178+
<p class="text-xs text-fg-muted mt-2">
179+
Enter this ID in the Foundry VTT module settings as the <strong>Campaign ID</strong>.
180+
</p>
181+
</div>
159182
<div>
160183
<label class="block text-sm font-medium text-fg-body mb-1">Module Install URL</label>
161184
<div class="flex items-center gap-2">

internal/plugins/syncapi/handler.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ func (h *Handler) CreateKey(c echo.Context) error {
111111
}
112112

113113
// Render the key creation result showing the plaintext key once.
114+
// For HTMX requests, return just the content fragment to avoid injecting
115+
// a full page (with duplicate navbar) inside the existing layout.
116+
if middleware.IsHTMX(c) {
117+
return middleware.Render(c, http.StatusOK, KeyCreatedFragmentTempl(cc.Campaign.ID, result))
118+
}
114119
csrfToken := middleware.GetCSRFToken(c)
115120
return middleware.Render(c, http.StatusOK, KeyCreatedTempl(cc.Campaign.ID, result, csrfToken))
116121
}

internal/plugins/syncapi/owner_keys.templ

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,25 @@ templ OwnerKeysPageTempl(campaignID string, keys []APIKey, stats *APIStats, csrf
2626
</a>
2727
</div>
2828

29+
// Campaign ID for Foundry VTT module configuration.
30+
<div class="card p-4 flex items-center justify-between gap-4">
31+
<div class="min-w-0">
32+
<p class="text-sm font-medium text-fg">Campaign ID</p>
33+
<p class="text-xs text-fg-muted">Use this when configuring the Foundry VTT module.</p>
34+
</div>
35+
<div class="flex items-center gap-2 shrink-0">
36+
<code class="px-3 py-1.5 bg-surface-alt rounded text-sm font-mono text-fg select-all" id="campaign-id-value">{ campaignID }</code>
37+
<button
38+
type="button"
39+
onclick="navigator.clipboard.writeText(document.getElementById('campaign-id-value').textContent.trim()).then(() => { this.innerHTML = '<i class=\'fa-solid fa-check text-emerald-500\'></i>'; setTimeout(() => { this.innerHTML = '<i class=\'fa-solid fa-copy\'></i>'; }, 2000); })"
40+
class="btn-secondary text-sm px-3 py-2 shrink-0"
41+
title="Copy to clipboard"
42+
>
43+
<i class="fa-solid fa-copy"></i>
44+
</button>
45+
</div>
46+
</div>
47+
2948
// Quick stats.
3049
if stats != nil {
3150
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@@ -69,6 +88,8 @@ templ OwnerKeysPageTempl(campaignID string, keys []APIKey, stats *APIStats, csrf
6988
method="POST"
7089
action={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/api-keys", campaignID)) }
7190
hx-post={ fmt.Sprintf("/campaigns/%s/api-keys", campaignID) }
91+
hx-target="#main-content"
92+
hx-swap="innerHTML show:window:top"
7293
class="space-y-4"
7394
>
7495
<input type="hidden" name="csrf_token" value={ csrfToken }/>
@@ -193,56 +214,67 @@ templ ownerKeyRow(campaignID string, k APIKey, csrfToken string) {
193214
</div>
194215
}
195216

196-
// KeyCreatedTempl renders the one-time key display after creation.
217+
// KeyCreatedTempl renders the one-time key display after creation (full page).
197218
templ KeyCreatedTempl(campaignID string, result *CreateAPIKeyResult, csrfToken string) {
198219
@layouts.App("Key Created") {
199-
<div class="max-w-2xl mx-auto space-y-6">
200-
<div class="card p-6">
201-
<div class="flex items-center gap-3 mb-4">
202-
<span class="w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
203-
<i class="fa-solid fa-check text-emerald-600 dark:text-emerald-400"></i>
204-
</span>
205-
<div>
206-
<h1 class="text-xl font-bold text-fg">API Key Created</h1>
207-
<p class="text-sm text-fg-secondary">{ result.Key.Name }</p>
208-
</div>
209-
</div>
220+
@keyCreatedContent(campaignID, result)
221+
}
222+
}
210223

211-
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4">
212-
<p class="text-sm font-semibold text-amber-700 dark:text-amber-400 mb-2">
213-
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
214-
Copy this key now — it will not be shown again!
215-
</p>
216-
<div class="flex items-center gap-2">
217-
<code class="flex-1 p-3 bg-white dark:bg-gray-900 rounded text-sm font-mono text-fg break-all select-all" id="api-key-value">
218-
{ result.RawKey }
219-
</code>
220-
<button
221-
onclick="navigator.clipboard.writeText(document.getElementById('api-key-value').textContent.trim())"
222-
class="btn-secondary text-sm shrink-0"
223-
>
224-
<i class="fa-solid fa-copy mr-1"></i> Copy
225-
</button>
226-
</div>
227-
</div>
224+
// KeyCreatedFragmentTempl renders the one-time key display as an HTMX fragment
225+
// (no layout wrapper) for inline replacement after form submission.
226+
templ KeyCreatedFragmentTempl(campaignID string, result *CreateAPIKeyResult) {
227+
@keyCreatedContent(campaignID, result)
228+
}
228229

229-
<div class="text-sm text-fg-secondary space-y-1">
230-
<p><strong>Prefix:</strong> <code class="text-xs font-mono">{ result.Key.KeyPrefix }</code></p>
231-
<p><strong>Permissions:</strong> { formatPermissions(result.Key.Permissions) }</p>
232-
<p><strong>Rate Limit:</strong> { fmt.Sprintf("%d", result.Key.RateLimit) } req/min</p>
233-
if result.Key.ExpiresAt != nil {
234-
<p><strong>Expires:</strong> { result.Key.ExpiresAt.Format("Jan 2, 2006") }</p>
235-
}
230+
// keyCreatedContent is the inner content shared by full-page and fragment variants.
231+
templ keyCreatedContent(campaignID string, result *CreateAPIKeyResult) {
232+
<div class="max-w-2xl mx-auto space-y-6">
233+
<div class="card p-6">
234+
<div class="flex items-center gap-3 mb-4">
235+
<span class="w-10 h-10 rounded-lg bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center">
236+
<i class="fa-solid fa-check text-emerald-600 dark:text-emerald-400"></i>
237+
</span>
238+
<div>
239+
<h1 class="text-xl font-bold text-fg">API Key Created</h1>
240+
<p class="text-sm text-fg-secondary">{ result.Key.Name }</p>
236241
</div>
242+
</div>
237243

238-
<div class="mt-6">
239-
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/api-keys", campaignID)) } class="btn-primary text-sm">
240-
Back to API Keys
241-
</a>
244+
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4">
245+
<p class="text-sm font-semibold text-amber-700 dark:text-amber-400 mb-2">
246+
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
247+
Copy this key now — it will not be shown again!
248+
</p>
249+
<div class="flex items-center gap-2">
250+
<code class="flex-1 p-3 bg-white dark:bg-gray-900 rounded text-sm font-mono text-fg break-all select-all" id="api-key-value">
251+
{ result.RawKey }
252+
</code>
253+
<button
254+
onclick="navigator.clipboard.writeText(document.getElementById('api-key-value').textContent.trim())"
255+
class="btn-secondary text-sm shrink-0"
256+
>
257+
<i class="fa-solid fa-copy mr-1"></i> Copy
258+
</button>
242259
</div>
243260
</div>
261+
262+
<div class="text-sm text-fg-secondary space-y-1">
263+
<p><strong>Prefix:</strong> <code class="text-xs font-mono">{ result.Key.KeyPrefix }</code></p>
264+
<p><strong>Permissions:</strong> { formatPermissions(result.Key.Permissions) }</p>
265+
<p><strong>Rate Limit:</strong> { fmt.Sprintf("%d", result.Key.RateLimit) } req/min</p>
266+
if result.Key.ExpiresAt != nil {
267+
<p><strong>Expires:</strong> { result.Key.ExpiresAt.Format("Jan 2, 2006") }</p>
268+
}
269+
</div>
270+
271+
<div class="mt-6">
272+
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/api-keys", campaignID)) } class="btn-primary text-sm">
273+
Back to API Keys
274+
</a>
275+
</div>
244276
</div>
245-
}
277+
</div>
246278
}
247279

248280
// formatPermissions formats permission list for display.

0 commit comments

Comments
 (0)