Skip to content

Commit 5142c30

Browse files
authored
Merge pull request #125 from keyxmakerx/claude/foundry-module-review-IrVxd
Claude/foundry module review ir vxd
2 parents 758db78 + 269f359 commit 5142c30

10 files changed

Lines changed: 367 additions & 9 deletions

File tree

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ Chronicle is purpose-built for tabletop RPGs, open source, and designed to be se
8080

8181
Chronicle includes a Foundry VTT module for bidirectional sync between your Chronicle worldbuilding and your Foundry VTT game. Sync journals, maps (drawings, tokens, fog of war), and calendars in real-time.
8282

83-
**Install in Foundry VTT:**
83+
**Install in Foundry VTT** — paste this manifest URL into Foundry's module installer:
8484
```
85-
https://github.com/keyxmakerx/Chronicle/releases/latest/download/module.json
85+
https://your-chronicle-instance.com/foundry-module/module.json
8686
```
8787

88+
Replace `your-chronicle-instance.com` with your Chronicle server's URL. Chronicle serves the module manifest and zip directly — no GitHub releases needed. The manifest URLs are automatically rewritten to match your instance.
89+
8890
Supports [Calendaria](https://foundryvtt.com/packages/calendaria) and [Simple Calendar](https://foundryvtt.com/packages/foundryvtt-simple-calendar) for calendar sync.
8991

9092
## Planned Features

foundry-module/module.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
}
1616
],
1717
"url": "https://github.com/keyxmakerx/Chronicle",
18-
"manifest": "https://github.com/keyxmakerx/Chronicle/releases/latest/download/module.json",
19-
"download": "https://github.com/keyxmakerx/Chronicle/releases/latest/download/chronicle-sync.zip",
18+
"manifest": "https://raw.githubusercontent.com/keyxmakerx/Chronicle/main/foundry-module/module.json",
19+
"download": "https://github.com/keyxmakerx/Chronicle/releases/download/foundry-v0.1.0/chronicle-sync.zip",
2020
"license": "MIT",
2121
"esmodules": [
2222
"scripts/module.mjs"

internal/app/app.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
package app
55

66
import (
7+
"archive/zip"
78
"database/sql"
89
"errors"
910
"fmt"
11+
"io/fs"
1012
"log/slog"
1113
"net/http"
14+
"os"
15+
"path/filepath"
1216
"strings"
1317

1418
"github.com/labstack/echo/v4"
@@ -93,6 +97,12 @@ func New(cfg *config.Config, db *sql.DB, rdb *redis.Client, pluginHealth *databa
9397
// Serve static files (CSS, JS, vendor libs, fonts, images).
9498
e.Static("/static", "static")
9599

100+
// Serve the Foundry VTT module directory for easy installation.
101+
// module.json is served with dynamic URL injection; zip built on-the-fly.
102+
e.GET("/foundry-module/module.json", app.serveFoundryModuleManifest)
103+
e.GET("/foundry-module/chronicle-sync.zip", app.serveFoundryModuleZip)
104+
e.Static("/foundry-module", "foundry-module")
105+
96106
return app
97107
}
98108

@@ -268,6 +278,77 @@ func isHTMXRequest(c echo.Context) bool {
268278
return c.Request().Header.Get("HX-Request") == "true"
269279
}
270280

281+
// serveFoundryModuleManifest serves foundry-module/module.json with the
282+
// manifest and download URLs rewritten to use the Chronicle instance's BaseURL.
283+
// This allows Foundry VTT to install the module directly from any Chronicle
284+
// instance without needing GitHub releases.
285+
func (a *App) serveFoundryModuleManifest(c echo.Context) error {
286+
data, err := os.ReadFile("foundry-module/module.json")
287+
if err != nil {
288+
return echo.NewHTTPError(http.StatusNotFound, "module.json not found")
289+
}
290+
291+
baseURL := strings.TrimRight(a.Config.BaseURL, "/")
292+
content := string(data)
293+
294+
// Replace manifest and download URLs with this Chronicle instance's URLs.
295+
content = strings.Replace(content,
296+
`"manifest": "https://raw.githubusercontent.com/keyxmakerx/Chronicle/main/foundry-module/module.json"`,
297+
fmt.Sprintf(`"manifest": "%s/foundry-module/module.json"`, baseURL), 1)
298+
content = strings.Replace(content,
299+
`"download": "https://github.com/keyxmakerx/Chronicle/releases/download/foundry-v0.1.0/chronicle-sync.zip"`,
300+
fmt.Sprintf(`"download": "%s/foundry-module/chronicle-sync.zip"`, baseURL), 1)
301+
302+
return c.JSONBlob(http.StatusOK, []byte(content))
303+
}
304+
305+
// serveFoundryModuleZip dynamically zips the foundry-module/ directory and
306+
// serves it as chronicle-sync.zip. Foundry VTT downloads this during module
307+
// installation. The zip contains all files under a chronicle-sync/ root
308+
// directory, which is the expected structure for Foundry module archives.
309+
func (a *App) serveFoundryModuleZip(c echo.Context) error {
310+
moduleDir := "foundry-module"
311+
if _, err := os.Stat(moduleDir); err != nil {
312+
return echo.NewHTTPError(http.StatusNotFound, "foundry module directory not found")
313+
}
314+
315+
c.Response().Header().Set("Content-Type", "application/zip")
316+
c.Response().Header().Set("Content-Disposition", "attachment; filename=chronicle-sync.zip")
317+
c.Response().WriteHeader(http.StatusOK)
318+
319+
zw := zip.NewWriter(c.Response().Writer)
320+
defer zw.Close()
321+
322+
return filepath.WalkDir(moduleDir, func(path string, d fs.DirEntry, err error) error {
323+
if err != nil {
324+
return err
325+
}
326+
// Skip non-distributable files.
327+
name := d.Name()
328+
if name == ".ai.md" || name == "TESTING.md" {
329+
return nil
330+
}
331+
if d.IsDir() {
332+
return nil
333+
}
334+
// Create zip entry under chronicle-sync/ root.
335+
relPath, _ := filepath.Rel(moduleDir, path)
336+
zipPath := filepath.Join("chronicle-sync", relPath)
337+
338+
w, err := zw.Create(filepath.ToSlash(zipPath))
339+
if err != nil {
340+
return err
341+
}
342+
343+
data, err := os.ReadFile(path)
344+
if err != nil {
345+
return err
346+
}
347+
_, err = w.Write(data)
348+
return err
349+
})
350+
}
351+
271352
// Start begins listening for HTTP requests on the configured port.
272353
func (a *App) Start() error {
273354
addr := fmt.Sprintf(":%d", a.Config.Port)

internal/app/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,7 @@ func (a *App) RegisterRoutes() {
731731
campaignRepo := campaigns.NewCampaignRepository(a.DB)
732732
campaignService := campaigns.NewCampaignService(campaignRepo, userFinder, smtpService, entityService, a.Config.BaseURL)
733733
campaignHandler := campaigns.NewHandler(campaignService)
734+
campaignHandler.SetBaseURL(a.Config.BaseURL)
734735
campaignHandler.SetEntityLister(&entityTypeListerAdapter{svc: entityService})
735736
campaignHandler.SetLayoutFetcher(&entityTypeLayoutFetcherAdapter{svc: entityService})
736737
campaignHandler.SetRecentEntityLister(&recentEntityListerAdapter{svc: entityService})
@@ -810,6 +811,7 @@ func (a *App) RegisterRoutes() {
810811
// Admin plugin: site-wide management (users, campaigns, SMTP settings, storage).
811812
adminHandler := admin.NewHandler(authRepo, campaignService, smtpService)
812813
adminHandler.SetMediaDeps(mediaRepo, mediaService, a.Config.Upload.MaxSize)
814+
adminHandler.SetBaseURL(a.Config.BaseURL)
813815
adminGroup := admin.RegisterRoutes(e, adminHandler, authService, smtpHandler)
814816

815817
// Settings plugin: editable storage limits (global, per-user, per-campaign).

internal/plugins/admin/dashboard.templ

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,17 @@ templ AdminDashboardPage(userCount, campaignCount, mediaFileCount int, totalStor
145145
}
146146
<p class="text-xs text-fg-muted group-hover:text-accent transition-colors">Schema explorer &rarr;</p>
147147
</a>
148+
<!-- Foundry VTT -->
149+
<a href="/admin/foundry" class="card hover:shadow-md transition-shadow group">
150+
<div class="flex items-center justify-between">
151+
<p class="text-sm font-medium text-fg-secondary">Foundry VTT</p>
152+
<span class="w-10 h-10 rounded-lg bg-violet-50 dark:bg-violet-900/30 flex items-center justify-center">
153+
<i class="fa-solid fa-dice-d20 text-violet-500"></i>
154+
</span>
155+
</div>
156+
<p class="text-lg font-semibold text-fg mt-2">Module Manager</p>
157+
<p class="text-xs text-fg-muted mt-1 group-hover:text-accent transition-colors">Manage module &rarr;</p>
158+
</a>
148159
<!-- SMTP -->
149160
<a href="/admin/smtp" class="card hover:shadow-md transition-shadow group">
150161
<div class="flex items-center justify-between">
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// foundry.templ renders the admin Foundry VTT module management page.
2+
// Allows admins to view and update the module version and see the install URL.
3+
4+
package admin
5+
6+
import (
7+
"github.com/keyxmakerx/chronicle/internal/templates/layouts"
8+
)
9+
10+
// AdminFoundryModulePage renders the Foundry VTT module management page.
11+
templ AdminFoundryModulePage(data FoundryModuleData) {
12+
@layouts.App("Foundry VTT Module - Admin") {
13+
<div class="max-w-2xl mx-auto space-y-6">
14+
<div class="flex items-center justify-between">
15+
<div>
16+
<h1 class="text-2xl font-bold text-fg">Foundry VTT Module</h1>
17+
<p class="text-sm text-fg-secondary mt-1">
18+
Manage the Chronicle Sync module served to Foundry VTT clients.
19+
</p>
20+
</div>
21+
<a href="/admin" class="btn-secondary text-sm">Back</a>
22+
</div>
23+
24+
// Current status.
25+
<div class="card p-6">
26+
<h2 class="text-lg font-semibold text-fg mb-4">Module Status</h2>
27+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
28+
<div>
29+
<label class="block text-xs font-medium text-fg-muted mb-1">Current Version</label>
30+
<p class="text-lg font-bold text-fg">{ data.Version }</p>
31+
</div>
32+
<div>
33+
<label class="block text-xs font-medium text-fg-muted mb-1">Module ID</label>
34+
<p class="text-sm font-mono text-fg">chronicle-sync</p>
35+
</div>
36+
</div>
37+
</div>
38+
39+
// Install URL.
40+
<div class="card p-6">
41+
<h2 class="text-lg font-semibold text-fg mb-4">Install URL</h2>
42+
<p class="text-sm text-fg-secondary mb-3">
43+
Share this URL with users to install the module in Foundry VTT.
44+
</p>
45+
<div class="flex items-center gap-2">
46+
<input
47+
type="text"
48+
readonly
49+
value={ data.InstallURL }
50+
class="input w-full font-mono text-sm bg-surface-alt"
51+
id="foundry-admin-url"
52+
/>
53+
<button
54+
type="button"
55+
onclick="navigator.clipboard.writeText(document.getElementById('foundry-admin-url').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); })"
56+
class="btn-secondary text-sm px-3 py-2 shrink-0"
57+
title="Copy to clipboard"
58+
>
59+
<i class="fa-solid fa-copy"></i>
60+
</button>
61+
</div>
62+
<p class="text-xs text-fg-muted mt-2">
63+
In Foundry VTT: <strong>Add-on Modules → Install Module → Manifest URL</strong>
64+
</p>
65+
</div>
66+
67+
// Update version.
68+
<div class="card p-6">
69+
<h2 class="text-lg font-semibold text-fg mb-4">Update Module Version</h2>
70+
<p class="text-sm text-fg-secondary mb-4">
71+
Bump the module version to notify Foundry VTT clients that an update is available.
72+
Foundry checks the manifest URL periodically and shows an update badge when the
73+
version changes.
74+
</p>
75+
<form
76+
method="POST"
77+
action="/admin/foundry/version"
78+
hx-put="/admin/foundry/version"
79+
class="flex items-end gap-3"
80+
>
81+
<input type="hidden" name="csrf_token" value={ data.CSRFToken }/>
82+
<div class="flex-1">
83+
<label for="version" class="block text-sm font-medium text-fg-body mb-1">New Version</label>
84+
<input
85+
type="text"
86+
id="version"
87+
name="version"
88+
value={ data.Version }
89+
required
90+
class="input w-full"
91+
placeholder="0.2.0"
92+
pattern="[0-9]+\.[0-9]+\.[0-9]+"
93+
title="Semantic version (e.g. 0.2.0)"
94+
/>
95+
</div>
96+
<button type="submit" class="btn-primary text-sm">Update Version</button>
97+
</form>
98+
</div>
99+
100+
// Info panel.
101+
<div class="card p-6">
102+
<div class="flex items-start gap-4">
103+
<span class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center shrink-0">
104+
<i class="fa-solid fa-circle-info text-accent"></i>
105+
</span>
106+
<div>
107+
<h3 class="text-sm font-semibold text-fg mb-1">How It Works</h3>
108+
<ul class="text-sm text-fg-secondary leading-relaxed space-y-1 list-disc ml-4">
109+
<li>The module manifest and zip are served directly from this Chronicle instance.</li>
110+
<li>Foundry VTT fetches the manifest URL to check for updates.</li>
111+
<li>When you bump the version, Foundry will show an update notification to all users.</li>
112+
<li>The module zip is generated on-the-fly from the <code class="text-xs bg-surface-alt px-1 rounded">foundry-module/</code> directory.</li>
113+
</ul>
114+
</div>
115+
</div>
116+
</div>
117+
</div>
118+
}
119+
}

0 commit comments

Comments
 (0)