Files
member-console/internal/embeds
cgalo5758 2f1803d1e6 Mark a subscription that delivers nothing with a badge, on the organization page and in the subscriptions list
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.
2026-10-26 17:38:12 -05:00
..
2026-09-06 02:29:42 -05:00

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 framework
  • bootstrap.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 favicon
  • favicon-16x16.png, favicon-32x32.png - Standard sizes
  • apple-touch-icon.png - iOS home screen
  • android-chrome-*.png - Android home screen
  • site.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 display
  • fedwiki_create_form.html - Site creation form
  • fedwiki_delete_confirm.html - Delete confirmation dialog
  • fedwiki_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)))