loadRecentActivity takes the page's ListParams, reads, pairs, builds and sorts as before, cuts the page with pageSliceClamped and names only the page's rows. A new pageNames asks the operator names resolver for the page's organizations, people, products and ladders, so each kind is one batch read however many rows the page holds. feedNames wraps the resolver for the row builder: a name the batch did not return is empty, and a tier is labelled by tierLabel, so the per-row readers perRowNames, tierNameResolver and newTierNameResolver are deleted. renderLanding and wholeFeed call the new signature, and the comments on ActivityNav, renderLanding and the landing template say the loader windows and names the page. No output changes: feed.json and feed.html are byte-identical, the 1406 test and subtest names of internal/server have the same results at the parent and here apart from the new TestOperatorPagesReadAFixedNumberOfTimes/landing, the operator walkthroughs (22, 16 pass, 6 skip) are equal at both commits, and the screens differ only in operator-integration-stripe (both widths) and the Age cell of operator-domains, which differ on every run. The goldens and nets from earlier commits did not change. TestRecentActivityReadsPerEvent expects no difference between one and three grows: every statement count is the same, where the four per-row reads grew by two. TestRecentActivityReadFailures fails the batch reads instead of the per-row ones (ListProductNamesByIDs, ListTiersWithProductsByLadderIDs, ListOrganizationNamesByIDs, ListPersonNamesByIDs), with the same names and assertions. That is NARROWER in one way: a read that fails for one id or one ladder alone can no longer happen, and a failed batch blanks every name of its kind on the page, which is what each row showed when its own read failed. TestOperatorPagesReadAFixedNumberOfTimes gains a landing row on a scratch database, with no per-row statements: after three grows the feed holds 6 events, fewer than its page of 10, so a read per named row would show. Breaks run at the parent (8 records) and here (15), all caught: the product, tier, organization and person names, the landing's page and count, the blank name of a failed product, organization, person and tier read, and each Want call the page makes. Not probed: that only the page's rows are named and not the whole window. The resolver reads one batch per kind either way and the page's names are the same, so neither the output nor the statement count tells the page from the window. Also not probed here: that an empty id is skipped instead of read, which the resolver's own tests cover.
Embedded Assets
This package provides embedded static files, templates, and configuration using Go's embed directive.
Overview
All assets are embedded at compile time, producing a single binary with no external file dependencies.
package embeds
//go:embed static
var Static embed.FS
//go:embed templates
var Templates embed.FS
//go:embed mc-config.yaml
var Config embed.FS
Directory Structure
internal/embeds/
├── embeds.go # Embed directives
├── mc-config.yaml # Default configuration template
├── static/ # Static assets (CSS, JS, images)
│ ├── bootstrap.css
│ ├── bootstrap.bundle.js
│ ├── htmx.min.js
│ └── favicon*.png
└── templates/ # HTML templates
├── index.html # Main page template
└── partials/ # HTMX partial templates
├── fedwiki_sites.html
├── fedwiki_create_form.html
├── fedwiki_delete_confirm.html
└── fedwiki_delete_success.html
Static Assets
Serving Static Files
staticFS, _ := fs.Sub(embeds.Static, "static")
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
Included Libraries
bootstrap.css(5.3.x) - CSS frameworkbootstrap.bundle.js(5.3.x) - Bootstrap JS (includes Popper)htmx.min.js(2.0.4) - HTMX for dynamic content
Favicons
Standard favicon set for various platforms:
favicon.ico- Traditional faviconfavicon-16x16.png,favicon-32x32.png- Standard sizesapple-touch-icon.png- iOS home screenandroid-chrome-*.png- Android home screensite.webmanifest- PWA manifest
Templates
Main Page Template (index.html)
The full HTML page that serves as the application shell:
<!DOCTYPE html>
<html>
<head>
<!-- Meta, CSS, HTMX -->
</head>
<body>
<!-- Navigation -->
<!-- Content sections with HTMX triggers -->
<div hx-get="/partials/fedwiki/sites"
hx-trigger="load, refreshSites from:body">
<!-- Loading spinner -->
</div>
<!-- Modals -->
<div class="modal" id="createSiteModal">...</div>
<!-- Scripts -->
</body>
</html>
Partial Templates (partials/)
HTML fragments returned by HTMX handlers. These are not full pages.
Naming Convention
{feature}_{action}.html
Examples:
fedwiki_sites.html- Sites list displayfedwiki_create_form.html- Site creation formfedwiki_delete_confirm.html- Delete confirmation dialogfedwiki_delete_success.html- Deletion success message
Template Data Pattern
Each partial has a corresponding Go struct for its data:
// In fedwiki_partials.go
type SitesData struct {
Sites []SiteViewModel
CurrentCount int64
Quota int64
IsMember bool
CanCreate bool
Error string
}
HTMX Patterns in Templates
Loading Content on Page Load
<div hx-get="/partials/fedwiki/sites"
hx-trigger="load, refreshSites from:body"
hx-swap="innerHTML">
<!-- Initial loading spinner -->
<div class="spinner-border"></div>
</div>
Form Submission
<form hx-post="/partials/fedwiki/sites"
hx-swap="innerHTML"
hx-target="#createSiteModalBody">
<!-- Form fields -->
<button type="submit">
<span class="spinner-border d-none" id="spinner"></span>
Create Site
</button>
</form>
Loading Button into Modal
<button data-bs-toggle="modal"
data-bs-target="#createSiteModal"
hx-get="/partials/fedwiki/create-form"
hx-target="#createSiteModalBody">
Create Site
</button>
Conditional Button State
<button {{ if .CanCreate }}
hx-get="/partials/fedwiki/create-form"
{{ else }}
disabled title="Site limit reached"
{{ end }}>
Create Site
</button>
Spinner Pattern
Spinners are hidden by default and shown during HTMX requests:
<!-- CSS to show spinner during request -->
<style>.htmx-request #createSpinner { display: inline-block !important; }</style>
<!-- Spinner hidden by default -->
<span class="spinner-border d-none" id="createSpinner"></span>
The htmx-request class is automatically added to elements during requests.
Configuration Template
mc-config.yaml provides a default configuration template that can be extracted:
// Extract default config
configData, _ := embeds.Config.ReadFile("mc-config.yaml")
This is used by the init command to create a starter configuration file.
Usage
Accessing Templates
import "git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
// For full page templates
tmpl, err := template.ParseFS(embeds.Templates, "templates/index.html")
// For partial templates
partialFS, _ := fs.Sub(embeds.Templates, "templates/partials")
tmpl, err := template.ParseFS(partialFS, "*.html")
Accessing Static Files
// Serve static files
staticFS, _ := fs.Sub(embeds.Static, "static")
http.Handle("/static/", http.FileServer(http.FS(staticFS)))