Section 8 of #212's change, after review: the Billing card's muted line under the Subscriptions row becomes an amber "Not delivering" badge on the row's value line, after the state badge, with a title that names the ladders and the holder ("Billing continues while a grant holds Demo Ladder."; with several subscriptions "Billing continues on <k> of <n> subscriptions while ..."). The operator's subscriptions list marks each displaced subscription's row with the same badge and title, read with one grouped statement per page and turned into titles by the same steps the card uses. A failed read on the list says "Failed to load deliveries." once above the table and marks no row. A paused subscription no longer counts: it generates no invoice, so "Billing continues" would be false; it is marked once it is active again. The grant form's warning now reads "Products marked ⚠ would supersede a tier a subscription pays for; the subscription keeps billing and delivers nothing on that ladder until the grant ends." The billing view it named before can change nothing. A Stripe-paused subscription's tier is still marked, an accepted edge until #269's change, which pauses a displaced subscription's billing and rewords the badge and the warning. Goldens: 16 change, each as item 8.9 lists: the organization page's JSON goldens rename DisplacementNote to DisplacementTitle, the two subscriptions-list goldens gain the title field, three HTML goldens carry the new warning, and grant-over-subscription.html the badge in place of the line. No other golden changed. Breaks (scripts/mutation-check, 31 of 31 CAUGHT): - paused restored - not_delivering removed from badgeMap - badge tone - list badge without title - card title never set - card template without badge - account form never chosen - n read as displaced count - subscription holder as grant - several holders as one - ladders joined with commas - sort removed - ladders not deduplicated - holders not deduplicated - status filter dropped (card) - status filter dropped (list) - list row in account form - list row in account form (list vs card) - list titles never set (the field set to strings.TrimPrefix of the title by itself, an empty string, because removing the line leaves titles unused and does not compile) - delivering row given the displaced title - list template without badge - list pool not default - list reads moved into the row loop (emulated: an inline closure calls the title read once per row; a helper only the break called would fail deadcode) - list DisplacementError assignment and L4 removed - O4 template line removed - O4 line moved under the table - rows dropped on a failed read - badge label - card badge without title - delivering row given an entry - old warning restored Hand capture on the Stripe sandbox (API 2025-02-24.acacia): a member subscribed to a $20/month tier through Checkout; an operator issued a complimentary grant of the tier above; an edit to the subscription at Stripe then reconciled it, and the reconcile logged "reconcile: product not conferred; another plan holds its ladder". The Billing card and the subscription's list row each showed Active and Not delivering at 1280 and at the 390 mobile width, with no sideways page scroll; the badge's title read by script: "Billing continues while a grant holds Demo Ladder." Captures under test/screens/displaced-subscription-waits/. The suite on reset databases passed on this section's tree, 4711 tests, 14 skipped; make screens changed no screen this section owns, and every registered form was captured. The review's last fixes (the list test's row checks now fail when a row is missing, two comment rewraps, task wording) came after it; the list test and its five breaks were rerun on them: pass, 5 of 5 CAUGHT. make lint passes; openspec validate --strict passes.
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)))