1// ===== cli.go =====
   2// Package main cli.go
   3package main
   4
   5import (
   6	_ "embed"
   7	"fmt"
   8	"log"
   9	"os"
  10	"reflect"
  11	"runtime"
  12	"strconv"
  13	"strings"
  14	"time"
  15
  16	"github.com/0magnet/calvin"
  17	"github.com/bitfield/script"
  18	cc "github.com/ivanpirog/coloredcobra"
  19	"github.com/spf13/cobra"
  20	"github.com/stripe/stripe-go/v81"
  21	"golang.org/x/text/cases"
  22	"golang.org/x/text/language"
  23)
  24
  25func init() {
  26	stripe.EnableTelemetry = false
  27	rootCmd.CompletionOptions.DisableDefaultCmd = true
  28	rootCmd.AddCommand(
  29		runCmd,
  30		genCmd,
  31		wasmCmd,
  32	)
  33	var helpflag bool
  34	rootCmd.SetUsageTemplate(help)
  35	rootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+rootCmd.Use)
  36	rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
  37	rootCmd.PersistentFlags().MarkHidden("help") //nolint
  38
  39}
  40
  41var rootCmd = &cobra.Command{
  42	Use:   "m2",
  43	Short: "web store server",
  44	Long:  calvin.AsciiFont("magnetosphere.net") + "\n" + "web store server",
  45}
  46
  47var genCmd = &cobra.Command{
  48	Use:   "gen",
  49	Short: "generate conf template",
  50	Long:  "generate conf template",
  51	Run: func(_ *cobra.Command, _ []string) {
  52		fmt.Println(envfiletemplate)
  53	},
  54}
  55
  56// Execute executes the root cli command
  57func Execute() {
  58	cc.Init(&cc.Config{
  59		RootCmd:         rootCmd,
  60		Headings:        cc.HiBlue + cc.Bold,
  61		Commands:        cc.HiBlue + cc.Bold,
  62		CmdShortDescr:   cc.HiBlue,
  63		Example:         cc.HiBlue + cc.Italic,
  64		ExecName:        cc.HiBlue + cc.Bold,
  65		Flags:           cc.HiBlue + cc.Bold,
  66		FlagsDescr:      cc.HiBlue,
  67		NoExtraNewlines: true,
  68		NoBottomNewline: true,
  69	})
  70	if err := rootCmd.Execute(); err != nil {
  71		log.Fatal("Failed to execute command: ", err)
  72	}
  73}
  74
  75var menvfile = os.Getenv("MENV")
  76
  77type flagVars struct {
  78	Teststripekey      bool
  79	ProductsCSV        string
  80	WebPort            int
  81	CoreRunWebPort     int
  82	StripelivePK       string
  83	StripeliveSK       string
  84	StripetestPK       string
  85	StripetestSK       string
  86	StripeSK           string
  87	StripePK           string
  88	Siteimagesrc       string
  89	Siteordersurl      string
  90	Sitename           string
  91	Siteext            string
  92	Sitedomain         string
  93	Sitelongname       string
  94	Sitetagline        string
  95	Sitemeta           string
  96	Siteprettyname     string
  97	Siteprettynamecap  string
  98	Siteprettynamecaps string
  99	SiteASCIILogo      string
 100	Tgcontact          string
 101	Tgchannel          string
 102	UseTinygo          bool
 103	WasmSRC            []string
 104	WasmExecPath       string
 105	WasmExecPathGo     string
 106	WasmExecPathTinyGo string
 107	Gobuild            string
 108	Tinygobuild        string
 109	Buildwasmwith      string
 110	LDFlagsX           string
 111	NoCore             bool          // disable cogentcore UI build and serving
 112	PagesUI            bool          // use programmatic pages UI instead of content system
 113	PrinterName        string        // CUPS queue name (blank = default)
 114	CupsOptions        string        // comma-separated -o options
 115	LpTimeout          time.Duration // timeout for `lp`
 116}
 117
 118var f = flagVars{
 119	//	WasmSRC: []string{"wasm/stl2.go","wasm/checkout_wasm.go"},
 120	WasmExecPath:       runtime.GOROOT() + "/lib/wasm/wasm_exec.js",                                    //nolint
 121	WasmExecPathGo:     runtime.GOROOT() + "/lib/wasm/wasm_exec.js",                                    //nolint
 122	WasmExecPathTinyGo: strings.TrimSuffix(runtime.GOROOT(), "go") + "tinygo" + "/targets/wasm_exec.js", //nolint
 123	Gobuild:            "go build",
 124	Tinygobuild:        "tinygo build -target=wasm --no-debug",
 125	Buildwasmwith:      "go build",
 126	LDFlagsX:           "stripePK",
 127}
 128
 129var (
 130	// Hardcoded array of valid shorthand characters, excluding "h"
 131	shorthandChars = []rune("abcdefgijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
 132	nextShortIndex = 0 // Index for the next shorthand flag
 133)
 134
 135// Get the next available shorthand flag
 136func getNextShortFlag() string {
 137	if nextShortIndex >= len(shorthandChars) {
 138		return ""
 139	}
 140	short := shorthandChars[nextShortIndex]
 141	nextShortIndex++
 142	return string(short)
 143}
 144
 145var a = true
 146var b = false
 147
 148func addStringFlag(cmds []*cobra.Command, f interface{}, fieldPtr *string, description string) {
 149	for i, _ := range cmds {
 150		cmds[i].Flags().StringVarP(fieldPtr, ccc(fieldPtr, f, b), getNextShortFlag(), scriptExecString(fmt.Sprintf("${%s%s}", ccc(fieldPtr, f, a), func(s string) string {
 151			if s != "" {
 152				s = "-" + s
 153			}
 154			return s
 155		}(*fieldPtr))), fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)))
 156	}
 157}
 158
 159func addStringSliceFlag(cmds []*cobra.Command, f interface{}, fieldPtr *[]string, description string) {
 160	for i, _ := range cmds {
 161		cmds[i].Flags().StringSliceVarP(
 162			fieldPtr,
 163			ccc(fieldPtr, f, b),
 164			getNextShortFlag(),
 165			scriptExecStringSlice(fmt.Sprintf("${%s[@]}", ccc(fieldPtr, f, a))),
 166			fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)),
 167		)
 168	}
 169}
 170
 171func addBoolFlag(cmds []*cobra.Command, f interface{}, fieldPtr *bool, description string) {
 172	for i, _ := range cmds {
 173		cmds[i].Flags().BoolVarP(fieldPtr, ccc(fieldPtr, f, b), getNextShortFlag(), scriptExecBool(fmt.Sprintf("${%s%s}", ccc(fieldPtr, f, a), func(b bool) string {
 174			return "-" + strconv.FormatBool(b)
 175		}(*fieldPtr))), fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)))
 176	}
 177}
 178func addIntFlag(cmds []*cobra.Command, f interface{}, fieldPtr *int, description string) {
 179	for i, _ := range cmds {
 180		cmds[i].Flags().IntVarP(fieldPtr, ccc(fieldPtr, f, b), getNextShortFlag(), scriptExecInt(fmt.Sprintf("${%s%s}", ccc(fieldPtr, f, a), func(i int) string {
 181			return fmt.Sprintf("-%d", i)
 182		}(*fieldPtr))), fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)))
 183	}
 184}
 185
 186func addDurationFlag(cmds []*cobra.Command, f interface{}, fieldPtr *time.Duration, description string) {
 187	for i := range cmds {
 188		// Keep parity with your pattern of embedding a "-" when a non-zero default is present.
 189		def := scriptExecDuration(fmt.Sprintf("${%s%s}",
 190			ccc(fieldPtr, f, a),
 191			func(d time.Duration) string {
 192				if d != 0 {
 193					return "-" + d.String() // e.g. "-5s"
 194				}
 195				return ""
 196			}(*fieldPtr),
 197		))
 198		cmds[i].Flags().DurationVarP(
 199			fieldPtr,
 200			ccc(fieldPtr, f, b),
 201			getNextShortFlag(),
 202			def,
 203			fmt.Sprintf("%s env: %s\033[0m\n\r", description, ccc(fieldPtr, f, a)),
 204		)
 205	}
 206}
 207
 208func init() {
 209	runCmd.Flags().SortFlags = false
 210	addStringFlag([]*cobra.Command{runCmd}, &f, &f.ProductsCSV, "products csv file")
 211	addBoolFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.Teststripekey, "use stripe test api keys instead of live key")
 212	addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripeliveSK, "stripe live api sk")
 213	addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripelivePK, "stripe live api pk")
 214	addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripetestSK, "stripe test api sk")
 215	addStringFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.StripetestPK, "stripe test api pk")
 216	addIntFlag([]*cobra.Command{runCmd}, &f, &f.WebPort, "port to serve on")
 217	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Siteimagesrc, "domain for images - leave blank to serve images")
 218	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Siteordersurl, "domain for orders - leave blank for same domain")
 219	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitename, "site name")
 220	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Siteext, "site domain extension")
 221	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitelongname, "site long name")
 222	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitetagline, "site tag line")
 223	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Sitemeta, "site meta")
 224	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Tgcontact, "telegram contact")
 225	addStringFlag([]*cobra.Command{runCmd}, &f, &f.Tgchannel, "telegram channel")
 226	addBoolFlag([]*cobra.Command{runCmd}, &f, &f.NoCore, "disable cogentcore UI build and serving")
 227	addBoolFlag([]*cobra.Command{runCmd}, &f, &f.PagesUI, "use programmatic pages UI instead of content system")
 228	addBoolFlag([]*cobra.Command{runCmd}, &f, &f.UseTinygo, "use tinygo instead of go to compile wasm")
 229	addStringSliceFlag([]*cobra.Command{runCmd, wasmCmd}, &f, &f.WasmSRC, "wasm source code files RELATIVE PATHS without '..'")
 230	addStringFlag([]*cobra.Command{runCmd}, &f, &f.PrinterName, "CUPS printer name (default: system default)")
 231	addStringFlag([]*cobra.Command{runCmd}, &f, &f.CupsOptions, "e.g. 'media=Custom.80x200mm,fit-to-page'")
 232	addDurationFlag([]*cobra.Command{runCmd}, &f, &f.LpTimeout, "timeout for lp command")
 233
 234}
 235
 236// change case
 237func ccc(val interface{}, strct interface{}, upper bool) string {
 238	v := reflect.ValueOf(strct)
 239	if v.Kind() == reflect.Ptr {
 240		v = v.Elem()
 241	}
 242	if v.Kind() != reflect.Struct {
 243		panic("uc: second argument must be a pointer to a struct")
 244	}
 245	for i := 0; i < v.NumField(); i++ {
 246		field := v.Field(i)
 247		if field.CanAddr() && field.Addr().Interface() == val {
 248			if upper {
 249				return strings.ToUpper(v.Type().Field(i).Name)
 250			}
 251			return strings.ToLower(v.Type().Field(i).Name)
 252		}
 253	}
 254	return ""
 255}
 256
 257func initstripePK() {
 258	f.StripeSK = f.StripeliveSK
 259	f.StripePK = f.StripelivePK
 260	if f.Teststripekey {
 261		f.StripeSK = f.StripetestSK
 262		f.StripePK = f.StripetestPK
 263	}
 264	stripe.Key = f.StripeSK
 265	// awkward way to do this
 266	f.LDFlagsX += "=" + f.StripePK
 267}
 268
 269var wasmCmd = &cobra.Command{
 270	Use:   "wasm",
 271	Short: "compile wasm",
 272	Long:  "compile wasm",
 273	Run: func(_ *cobra.Command, _ []string) {
 274		if len(f.WasmSRC) == 0 {
 275			log.Fatal("No wasm source code specified")
 276		}
 277		initstripePK()
 278		compileWASM()
 279	},
 280}
 281
 282var runCmd = &cobra.Command{
 283	Use:   "run",
 284	Short: "run the web application",
 285	Long: calvin.AsciiFont("magnetosphere.net") + "\n" + func() string {
 286		helptext := `Run the web application
 287Generate a config file first
 288
 289Config defaults file may also be specified with:
 290MENV=m2.conf m2 run
 291OR
 292MENV=/path/to/m2.conf m2 run
 293print the MENV file template with:
 294m2 gen`
 295		if menvfile == "" {
 296			return helptext
 297		}
 298		if _, err := os.Stat(menvfile); err == nil {
 299			return `Run the web application
 300
 301menv file detected: ` + menvfile
 302		}
 303		return helptext
 304	}(),
 305	Run: func(_ *cobra.Command, _ []string) {
 306		f.Sitedomain = f.Sitename + f.Siteext
 307		log.Println(" Initializing " + f.Sitedomain)
 308		fmt.Println(calvin.BlackboardBold(f.Sitedomain))
 309		fmt.Println(calvin.AsciiFont(f.Sitedomain))
 310		initstripePK()
 311		f.Siteprettyname = calvin.BlackboardBold(f.Sitedomain) //"π•„π•’π•˜π•Ÿπ•–π•₯𝕠𝕀𝕑𝕙𝕖𝕣𝕖.π•Ÿπ•–π•₯"
 312		c := cases.Title(language.English)
 313		f.Siteprettynamecap = calvin.BlackboardBold(c.String(f.Sitedomain))         //"π•„π•’π•˜π•Ÿπ•–π•₯𝕠𝕀𝕑𝕙𝕖𝕣𝕖.π•Ÿπ•–π•₯"
 314		f.Siteprettynamecaps = calvin.BlackboardBold(strings.ToUpper(f.Sitedomain)) //"π•„π”Έπ”Ύβ„•π”Όπ•‹π•†π•Šβ„™β„π”Όβ„π”Ό.ℕ𝔼𝕋"
 315		f.SiteASCIILogo = strings.Replace(strings.Replace(calvin.AsciiFont(f.Sitedomain), " ", "&nbsp;", -1), "\n", "<br>\n", -1)
 316
 317		if f.UseTinygo {
 318			f.WasmExecPath = f.WasmExecPathTinyGo
 319			f.Buildwasmwith = f.Tinygobuild
 320		}
 321		if len(f.WasmSRC) == 0 {
 322			f.WasmExecPath = ""
 323			f.Buildwasmwith = ""
 324		}
 325		log.Println("Checking for products CSV")
 326		fileInfo, err := os.Stat(f.ProductsCSV)
 327		if err != nil {
 328			log.Fatal("Error getting file info:", err)
 329		}
 330		lastModTime = fileInfo.ModTime()
 331		log.Println("Reading products CSV")
 332		prods := readCSV(f.ProductsCSV)
 333		if warnings := validateCSV(prods); len(warnings) > 0 {
 334			for _, w := range warnings {
 335				log.Println("CSV warning:", w)
 336			}
 337		}
 338		allproductsMu.Lock()
 339		allproducts = prods
 340		allproductsMu.Unlock()
 341		go func() {
 342			for {
 343				fileInfo, err := os.Stat(f.ProductsCSV)
 344				if err != nil {
 345					log.Println("Error getting file info:", err)
 346				}
 347
 348				currentModTime := fileInfo.ModTime()
 349				if currentModTime != lastModTime {
 350					log.Println("CSV file has been modified!")
 351					prods := readCSV(f.ProductsCSV)
 352					if warnings := validateCSV(prods); len(warnings) > 0 {
 353						for _, w := range warnings {
 354							log.Println("CSV warning:", w)
 355						}
 356					}
 357					allproductsMu.Lock()
 358					allproducts = prods
 359					allproductsMu.Unlock()
 360					lastModTime = currentModTime
 361					if !f.NoCore {
 362						createCORE()
 363						buildCORE()
 364					}
 365				}
 366
 367				time.Sleep(10 * time.Second)
 368			}
 369		}()
 370
 371		server()
 372	},
 373}
 374
 375var lastModTime time.Time
 376
 377func scriptExecString(s string) string {
 378	z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
 379	if err == nil {
 380		return strings.TrimSpace(z)
 381	}
 382	return ""
 383}
 384
 385func scriptExecStringSlice(s string) []string {
 386	z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s" "%s"'`, menvfile, "%s\n", s)).Slice()
 387	if err == nil {
 388		return z
 389	}
 390	return []string{""}
 391}
 392
 393func scriptExecBool(s string) bool {
 394	z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
 395	if err == nil {
 396		b, err := strconv.ParseBool(z)
 397		if err == nil {
 398			return b
 399		}
 400	}
 401	return false
 402}
 403
 404/*
 405func scriptExecArray(s string) string {
 406	y, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; for _i in %s ; do echo "$_i" ; done'`, menvfile, s)).Slice()
 407	if err == nil {
 408		return strings.Join(y, ",")
 409	}
 410	return ""
 411}
 412*/
 413
 414func scriptExecInt(s string) int {
 415	z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
 416	if err == nil {
 417		if z == "" {
 418			return 0
 419		}
 420		i, err := strconv.Atoi(z)
 421		if err == nil {
 422			return i
 423		}
 424	}
 425	return 0
 426}
 427
 428// Accepts Go duration strings ("750ms", "2s", "5m", "1h").
 429// Also accepts a bare integer (treated as seconds).
 430// If the evaluated string starts with "-", it is trimmed (matching your other helpers’ default building).
 431func scriptExecDuration(s string) time.Duration {
 432	z, err := script.Exec(fmt.Sprintf(`bash -c 'MENV=%s ; if [[ $MENV != "" ]] && [[ -f $MENV ]] ; then source $MENV ; fi ; printf "%s"'`, menvfile, s)).String()
 433	if err != nil {
 434		return 0
 435	}
 436	z = strings.TrimSpace(z)
 437	if z == "" {
 438		return 0
 439	}
 440	z = strings.TrimPrefix(z, "-") // keep parity with how defaults are built
 441
 442	// Try full Go duration syntax first.
 443	if d, err := time.ParseDuration(z); err == nil {
 444		return d
 445	}
 446	// Fallback: plain integer means seconds.
 447	if n, err := strconv.ParseInt(z, 10, 64); err == nil {
 448		return time.Duration(n) * time.Second
 449	}
 450	return 0
 451}
 452
 453const help = "\r\n" +
 454	"  {{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" +
 455	"{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" +
 456	"Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n  " +
 457	"{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" +
 458	"Flags:\r\n" +
 459	"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" +
 460	"Global Flags:\r\n" +
 461	"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n"
 462
 463const envfiletemplate = `#
 464# /etc/m2.conf
 465#
 466#########################################################################
 467#	M2 CONFIG TEMPLATE
 468# change config defaults
 469# or comment values with # to exclude
 470#########################################################################
 471
 472### Stripe Configuration ################################################
 473
 474#--	Live and test API keys - REQUIRED
 475STRIPELIVEPK='pk_live_...'
 476STRIPELIVESK='sk_live_...'
 477STRIPETESTPK='pk_test_...'
 478STRIPETESTSK='sk_test_...'
 479
 480#--	Use Test Keys
 481USETESTKEY=true
 482
 483### Site Product Data Configuration #####################################
 484
 485#-- Products CSV path (ex. 'products.csv')
 486PRODUCTSCVS=''
 487
 488#-- Image subdomain (ex. 'https://img.magnetosphere.net')
 489# no trailing slash '/' !
 490IMGSRC=''
 491
 492#-- Orders subdomain (ex. 'https://pay.magnetosphere.net')
 493# no trailing slash '/' !
 494ORDERSURL=''
 495
 496### Site Configuration ##################################################
 497
 498#-- Website (Host) Name (domain minus extension - ex. 'magnetosphere')
 499SITENAME=''
 500
 501#-- Website Domain Extension (ex. '.com' '.net')
 502SITEEXT=''
 503
 504#-- Site Long Name (ex. 'magnetosphere electronic surplus')
 505SITELONGNAME=''
 506
 507#-- Site Tag Line (ex. 'we have the technology')
 508SITETAGLINE=''
 509
 510#-- Site Meta Description (ex. 'we have the technology (β—•β€Ώβ—•) electronic surplus for sale')
 511SITEMETA=''
 512
 513#-- Site Telegram Contact
 514# DO NOT INCLUDE 'https://t.me/'
 515# ex. 'magnetosphere' will display on-site as 'https://t.me/magnetosphere'
 516TGCONTACT=''
 517
 518#-- Site Telegram Channel
 519# DO NOT INCLUDE 'https://t.me/'
 520# ex. 'magnetospheredotnet' will display on-site as 'https://t.me/magnetospheredotnet'
 521TGCHANNEL=''
 522
 523### Web Server Configuration ############################################
 524
 525#-- Port to serve http on (ex. '9883')
 526WEBPORT='9883'
 527
 528#-- Disable cogentcore UI build and serving (true/false)
 529NOCORE=false
 530
 531#-- Use programmatic pages UI instead of content system (true/false)
 532PAGESUI=false
 533
 534#-- CUPS printer name (default: system default)
 535PRINTERNAME=''
 536
 537#-- CUPS options, comma separated (e.g. 'media=Custom.80x200mm,fit-to-page')
 538CUPSOPTIONS=''
 539
 540#-- timeout for lp command
 541LPTIMEOUT="10s"
 542`
 543
 544
 545// ===== core.go =====
 546// Package main core.go
 547package main
 548
 549import (
 550	"bufio"
 551	"bytes"
 552	"encoding/json"
 553	htmpl "html/template"
 554	"log"
 555	"os"
 556	"path/filepath"
 557	"sort"
 558	"strings"
 559	ttmpl "text/template"
 560	"time"
 561
 562	p "github.com/0magnet/m2/pkg/product"
 563	"github.com/bitfield/script"
 564	"github.com/briandowns/spinner"
 565	"github.com/gofiber/fiber/v3"
 566)
 567
 568func watchCORE() {
 569	createCORE()
 570	buildCORE()
 571
 572	files := map[string]struct {
 573		lastMod time.Time
 574		handler func()
 575	}{
 576		"ui/🌐.go": {
 577			handler: func() {
 578				createCORE()
 579				buildCORE()
 580			},
 581		},
 582		"htmpl/product.md": {
 583			handler: func() {
 584				createCORE()
 585				buildCORE()
 586			},
 587		},
 588	}
 589
 590	go func() {
 591		for path := range files {
 592			fi, err := os.Stat(path)
 593			if err != nil {
 594				log.Printf("Cannot stat %s: %v", path, err)
 595				continue
 596			}
 597			files[path] = struct {
 598				lastMod time.Time
 599				handler func()
 600			}{
 601				lastMod: fi.ModTime(),
 602				handler: files[path].handler,
 603			}
 604		}
 605
 606		ticker := time.NewTicker(5 * time.Second)
 607		defer ticker.Stop()
 608
 609		for range ticker.C {
 610			for path, entry := range files {
 611				fi, err := os.Stat(path)
 612				if err != nil {
 613					log.Printf("Error stating %s: %v", path, err)
 614					continue
 615				}
 616				if fi.ModTime().After(entry.lastMod) {
 617					log.Println("πŸ“¦ Detected change in", path)
 618					files[path] = struct {
 619						lastMod time.Time
 620						handler func()
 621					}{
 622						lastMod: fi.ModTime(),
 623						handler: entry.handler,
 624					}
 625					entry.handler()
 626				}
 627			}
 628		}
 629	}()
 630}
 631
 632func handleCORE(r *fiber.App) {
 633	r.Use("/", func(c fiber.Ctx) error {
 634		coreUIPath := "ui/bin/web"
 635		trim := strings.Trim(c.Path(), "/")
 636		if trim == "" {
 637			trim = "index.html"
 638		}
 639		fullPath := filepath.Join(coreUIPath, trim)
 640		info, err := os.Stat(fullPath)
 641		if err == nil && info.IsDir() {
 642			fullPath = filepath.Join(fullPath, "index.html")
 643		}
 644		if _, err := os.Stat(fullPath); err != nil {
 645			c.Status(fiber.StatusNotFound)
 646			return c.SendFile(filepath.Join(coreUIPath, "404.html"))
 647		}
 648		if strings.HasSuffix(fullPath, ".wasm") {
 649			c.Set("Content-Type", "application/wasm")
 650		}
 651		return c.SendFile(fullPath)
 652	})
 653}
 654
 655func buildCORE() {
 656	s := spinner.New(spinner.CharSets[14], 25*time.Millisecond)
 657	s.Suffix = " Building C.O.R.E UI..."
 658	s.Start()
 659	ldflags := `-X 'main.` + f.LDFlagsX + `'`
 660	if f.PagesUI {
 661		ldflags += ` -X 'main.uiMode=pages'`
 662	}
 663	_, err := script.Exec(`bash -c 'set -x ; go get -u ./... ; go mod tidy ; go mod vendor ; cd ui || exit 1 ; rm -rf bin ; time go run -x cogentcore.org/core@main build web -vv -ldflags="` + ldflags + `" || timeout 30 go run -x .'`).Stdout()
 664	s.Stop()
 665	if err != nil {
 666		log.Println(err)
 667	} else {
 668		log.Println("βœ… Done building C.O.R.E UI")
 669	}
 670}
 671
 672func createCORE() {
 673	log.Println("Creating Content Files")
 674	if _, err := script.Exec(`bash -c 'rm -rf ui/content ; cp -r ui/content-bak ui/content'`).Stdout(); err != nil {
 675		log.Fatalf(err.Error())
 676	}
 677	log.Println("Populating Content")
 678	prodPageMDTmpl, err := ttmpl.New("index").Funcs(htmpl.FuncMap{
 679		"replace": replace, "mul": mul, "div": div,
 680		"safeHTML": safeHTML, "safeJS": safeJS, "stripProtocol": stripProtocol,
 681		"add": add, "sub": sub, "toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
 682		"getsubcats": getsubcats, "escapesubcat": escapesubcat,
 683		"sortsubcats": sortsubcats, "repeat": repeat,
 684	}).Parse(h.ProductPageMD())
 685	if err != nil {
 686		log.Println("Error parsing product page markdown template:", err)
 687		log.Fatalf(err.Error())
 688	}
 689
 690	catPageMDTmpl, err := ttmpl.New("index").Funcs(htmpl.FuncMap{
 691		"replace": replace, "mul": mul, "div": div,
 692		"safeHTML": safeHTML, "safeJS": safeJS, "stripProtocol": stripProtocol,
 693		"add": add, "sub": sub, "toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
 694		"getsubcats": getsubcats, "escapesubcat": escapesubcat,
 695		"sortsubcats": sortsubcats, "repeat": repeat,
 696	}).Parse(h.CategoryPageMD())
 697	if err != nil {
 698		log.Println("Error parsing category page markdown template:", err)
 699		log.Fatalf(err.Error())
 700	}
 701
 702	var enabled []p.Product
 703	catSet := make(map[string]struct{})
 704	for _, prod := range allproducts {
 705		if strings.EqualFold(prod.Enable, "TRUE") {
 706			enabled = append(enabled, prod)
 707			catSet[prod.Category] = struct{}{}
 708		}
 709	}
 710
 711	var cats []string
 712	for c := range catSet {
 713		cats = append(cats, c)
 714	}
 715	sort.Slice(cats, func(i, j int) bool {
 716		return strings.ToLower(cats[i]) < strings.ToLower(cats[j])
 717	})
 718
 719	for _, cat := range cats {
 720		var prods []p.Product
 721		for _, prod := range enabled {
 722			if strings.EqualFold(prod.Category, cat) {
 723				prods = append(prods, prod)
 724			}
 725		}
 726
 727		var buf bytes.Buffer
 728		if err := catPageMDTmpl.Execute(&buf, map[string]interface{}{
 729			"Products": prods,
 730			"Category": cat,
 731			"Domain":   f.Sitedomain,
 732			"Page": map[string]interface{}{
 733				"ImgSRC": f.Siteimagesrc,
 734			},
 735		}); err != nil {
 736			log.Fatalf("execute category MD (%q): %v", cat, err)
 737		}
 738
 739		lines := strings.Split(buf.String(), "\n")
 740		var cleaned []string
 741		for _, line := range lines {
 742			if strings.TrimSpace(line) != "" {
 743				cleaned = append(cleaned, line)
 744			}
 745		}
 746		md := strings.Join(cleaned, "\n") + "\n"
 747
 748		filename := "ui/content/" + escapesubcat(cat) + ".md"
 749		if _, err := script.Echo(md).WriteFile(filename); err != nil {
 750			log.Fatalf("write category MD (%q): %v", filename, err)
 751		}
 752	}
 753
 754	for _, product := range allproducts {
 755		if product.Enable != "TRUE" {
 756			continue
 757		}
 758
 759		var buf bytes.Buffer
 760		err := prodPageMDTmpl.Execute(&buf, map[string]interface{}{"Prod": product, "Domain": f.Sitedomain})
 761		if err != nil {
 762			log.Fatalf(err.Error())
 763		}
 764		lines := strings.Split(buf.String(), "\n")
 765		var cleanedLines []string
 766		for _, line := range lines {
 767			if strings.TrimSpace(line) != "" {
 768				cleanedLines = append(cleanedLines, line)
 769			}
 770		}
 771		cleaned := strings.Join(cleanedLines, "\n") + "\n"
 772		filename := "ui/content/" + escapesubcat(product.Partno) + ".md"
 773		_, err = script.Echo(cleaned).WriteFile(filename)
 774		if err != nil {
 775			log.Fatalf(err.Error())
 776		}
 777	}
 778	log.Println("Writing products.json")
 779
 780	data := readproductscsv(f.ProductsCSV)
 781	if len(data) == 0 {
 782		log.Fatalf("CSV file %s is empty or unreadable", f.ProductsCSV)
 783	}
 784	scanner := bufio.NewScanner(bytes.NewReader(data))
 785	var jsonData []map[string]string
 786	var headers []string
 787	lineNum := 0
 788	for scanner.Scan() {
 789		line := scanner.Text()
 790		f := strings.Split(line, ",")
 791		if lineNum == 0 {
 792			headers = f
 793			lineNum++
 794			continue
 795		}
 796		if len(f) < 4 {
 797			continue
 798		}
 799		if f[3] == "TRUE" {
 800			entry := make(map[string]string)
 801			for i := range f {
 802				if i < len(headers) {
 803					entry[headers[i]] = f[i]
 804				}
 805			}
 806			jsonData = append(jsonData, entry)
 807		}
 808		lineNum++
 809	}
 810
 811	if err := scanner.Err(); err != nil {
 812		log.Fatalf("Error scanning CSV: %v", err)
 813	}
 814
 815	// Convert to JSON
 816	output, err := json.MarshalIndent(jsonData, "", "  ")
 817	if err != nil {
 818		log.Fatalf("Error encoding JSON: %v", err)
 819	}
 820
 821	_, err = script.Echo(string(output)).WriteFile("ui/products.json")
 822	if err != nil {
 823		log.Fatalf(err.Error())
 824	}
 825}
 826
 827
 828// ===== csv.go =====
 829// Package main csv.go
 830package main
 831
 832import (
 833	"bufio"
 834	"bytes"
 835	_ "embed"
 836	"fmt"
 837	"log"
 838	"strings"
 839	"sync"
 840
 841	p "github.com/0magnet/m2/pkg/product"
 842	"github.com/bitfield/script"
 843)
 844
 845var (
 846	allproducts   p.Products
 847	allproductsMu sync.RWMutex
 848)
 849
 850func readproductscsv(csvFile string) (data []byte) {
 851	data, err := script.File(csvFile).Bytes() //nolint
 852	if err != nil {
 853		log.Printf(`Error reading %s file %v`, csvFile, err)
 854	}
 855	return data
 856}
 857
 858const csvMinFields = 51 // f[0] through f[50]
 859
 860func readCSV(csvFile string) (prods p.Products) {
 861	scanner := bufio.NewScanner(bytes.NewReader(readproductscsv(csvFile)))
 862	lineNum := 0
 863	for scanner.Scan() {
 864		lineNum++
 865		line := scanner.Text()
 866		f := strings.Split(line, ",")
 867		if len(f) < 4 {
 868			continue
 869		}
 870		if f[3] != "TRUE" {
 871			continue
 872		}
 873		if len(f) < csvMinFields {
 874			log.Printf("csv line %d: expected %d fields, got %d β€” skipping", lineNum, csvMinFields, len(f))
 875			continue
 876		}
 877		q := p.Product{
 878			Image1:            f[0],
 879			Partno:            f[1],
 880			Name:              f[2],
 881			Enable:            f[3],
 882			Price:             f[4],
 883			Quantity:          f[5],
 884			Shippable:         f[6],
 885			Minorder:          f[7],
 886			Maxorder:          f[8],
 887			Defaultquantity:   f[9],
 888			Stepquantity:      f[10],
 889			Mfgpartno:         f[11],
 890			Mfgname:           f[12],
 891			Category:          f[13],
 892			Subcategory:       f[14],
 893			Location:          f[15],
 894			Msrp:              f[16],
 895			Cost:              f[17],
 896			Typ:               f[18],
 897			Packagetype:       f[19],
 898			Technology:        f[20],
 899			Materials:         f[21],
 900			Value:             f[22],
 901			ValUnit:           f[23],
 902			Resistance:        f[24],
 903			ResUnit:           f[25],
 904			Tolerance:         f[26],
 905			VoltsRating:       f[27],
 906			AmpsRating:        f[28],
 907			WattsRating:       f[29],
 908			TempRating:        f[30],
 909			TempUnit:          f[31],
 910			Description1:      f[32],
 911			Description2:      f[33],
 912			Color1:            f[34],
 913			Color2:            f[35],
 914			Sourceinfo:        f[36],
 915			Datasheet:         f[37],
 916			Docs:              f[38],
 917			Reference:         f[39],
 918			Attributes:        f[40],
 919			Year:              f[41],
 920			Condition:         f[42],
 921			Note:              f[43],
 922			Warning:           f[44],
 923			CableLengthInches: f[45],
 924			LengthInches:      f[46],
 925			WidthInches:       f[47],
 926			HeightInches:      f[48],
 927			WeightLb:          f[49],
 928			WeightOz:          f[50],
 929		}
 930		prods = append(prods, q)
 931	}
 932	return prods
 933}
 934
 935// validateCSV scans products for patterns that could cause issues in HTML/JS rendering.
 936// Returns a list of warnings. Call after readCSV to check data integrity.
 937func validateCSV(prods p.Products) []string {
 938	var warnings []string
 939	for i, pr := range prods {
 940		check := func(field, value string) {
 941			if strings.ContainsAny(value, "<>\"'&") {
 942				warnings = append(warnings, fmt.Sprintf("product %d (%s): %s contains HTML-unsafe characters: %q", i, pr.Partno, field, value))
 943			}
 944			if strings.Contains(value, "|") {
 945				warnings = append(warnings, fmt.Sprintf("product %d (%s): %s contains pipe character: %q", i, pr.Partno, field, value))
 946			}
 947		}
 948		check("Partno", pr.Partno)
 949		check("Name", pr.Name)
 950		check("Description1", pr.Description1)
 951		check("Description2", pr.Description2)
 952		check("Note", pr.Note)
 953		check("Warning", pr.Warning)
 954		check("Category", pr.Category)
 955		check("Subcategory", pr.Subcategory)
 956		check("Mfgname", pr.Mfgname)
 957		check("Mfgpartno", pr.Mfgpartno)
 958	}
 959	return warnings
 960}
 961
 962
 963// ===== m2.go =====
 964// Package main m2.go
 965package main
 966
 967import (
 968	"bytes"
 969	"encoding/base64"
 970	"errors"
 971	"fmt"
 972	"log"
 973	"path/filepath"
 974	"regexp"
 975	"sort"
 976	"strconv"
 977	"strings"
 978	"sync"
 979	"time"
 980
 981	p "github.com/0magnet/m2/pkg/product"
 982	"github.com/bitfield/script"
 983	"github.com/gofiber/fiber/v3"
 984	"github.com/gofiber/fiber/v3/middleware/static"
 985)
 986
 987
 988func main() { Execute() }
 989
 990var collapseNewlines = regexp.MustCompile(`\n{2,}`)
 991
 992func methodColor(method string, colors fiber.Colors) string {
 993	switch method {
 994	case fiber.MethodGet:
 995		return colors.Cyan
 996	case fiber.MethodPost:
 997		return colors.Green
 998	case fiber.MethodPut:
 999		return colors.Yellow
1000	case fiber.MethodDelete:
1001		return colors.Red
1002	case fiber.MethodPatch:
1003		return colors.White
1004	case fiber.MethodHead:
1005		return colors.Magenta
1006	case fiber.MethodOptions:
1007		return colors.Blue
1008	default:
1009		return colors.Reset
1010	}
1011}
1012
1013func statusColor(code int, colors fiber.Colors) string {
1014	switch {
1015	case code >= fiber.StatusOK && code < fiber.StatusMultipleChoices:
1016		return colors.Green
1017	case code >= fiber.StatusMultipleChoices && code < fiber.StatusBadRequest:
1018		return colors.Blue
1019	case code >= fiber.StatusBadRequest && code < fiber.StatusInternalServerError:
1020		return colors.Yellow
1021	default:
1022		return colors.Red
1023	}
1024}
1025
1026func server() {
1027	wg := new(sync.WaitGroup)
1028	wg.Add(1)
1029	initTMPL()
1030	r := fiber.New(fiber.Config{
1031		ErrorHandler: func(c fiber.Ctx, err error) error {
1032			code := fiber.StatusInternalServerError
1033			var e *fiber.Error
1034			if errors.As(err, &e) {
1035				code = e.Code
1036			}
1037			c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
1038			return c.Status(code).SendString(err.Error())
1039		},
1040	})
1041
1042	r.Use(func(c fiber.Ctx) error {
1043		start := time.Now()
1044		err := c.Next()
1045		status := c.Response().StatusCode()
1046		lat := time.Since(start)
1047		colors := c.App().Config().ColorScheme
1048		ip := fmt.Sprintf("%*s", 15, c.IP())
1049		ipsStr := strings.Join(c.IPs(), ", ")
1050		ips := fmt.Sprintf("%*s", 15, ipsStr)
1051		method := fmt.Sprintf("%-*s", 6, c.Method())
1052		statCol := statusColor(status, colors) + fmt.Sprintf("%3d", status) + colors.Reset
1053		methCol := methodColor(c.Method(), colors) + method + colors.Reset
1054		fmt.Printf("%s | %s | %12s | %s | %s | %s | %s\n", time.Now().Format("2006-01-02 15:04:05"), statCol, lat, ip, ips, methCol, c.Path())
1055		return err
1056	})
1057	serveSourceCode(r)
1058	serveWASM(r)
1059	r.Get("/logo", logo)
1060	r.Get("/logo/:width", logo)
1061	r.Get("/logo/:width/:height", logo)
1062	r.Get("/logo.png", sendFile)
1063	r.Get("/logo.html", sendFile)
1064	r.Get("/mobilelogo.html", sendFile)
1065	r.Get("/logolarge.html", sendFile)
1066	r.Get("/favicon.ico", sendImage)
1067	r.Get("/robots.txt", robots)
1068	if f.Siteimagesrc == "" {
1069		r.Use("/i", static.New("./img"))
1070		r.Use("/img", static.New("./img"))
1071	}
1072	r.Use("/font", static.New("./font"))
1073	r.Get("/stl/:filename", func(c fiber.Ctx) error {
1074		name := c.Params("filename")
1075		if strings.ContainsAny(name, "/\\..") || strings.Contains(name, "..") {
1076			return c.SendStatus(fiber.StatusBadRequest)
1077		}
1078		return c.SendFile("./img/stl/" + name)
1079	})
1080	r.Get("/stl/base64/:filename", stlbase64)
1081	r.Get("/site.webmanifest", func(c fiber.Ctx) error {
1082		return c.JSON([]byte(`{"name":"","short_name":"","icons":[{"src":"` + f.Siteimagesrc + `/i/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"` + f.Siteimagesrc + `/i/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}`))
1083	})
1084	r.Get("/sitemap", sitemap)
1085	r.Get("/sitemap.xml", sitemap)
1086	r.Get("/coffee", func(c fiber.Ctx) error { return c.SendStatus(fiber.StatusTeapot) })
1087	r.Get("/clock", clock)
1088	r.Get("/", homepage)
1089	r.Get("/attractors", attractorspage)
1090	r.Get("/p/:partno", productpage)
1091	r.Get("/post/:partno", handlecat)
1092	r.Get("/p", handlecat)
1093	r.Get("/cat", handlecat)
1094	r.Get("/cat/:cat", handlecat)
1095	r.Get("/cat/:cat/:subcat", handlecat)
1096	r.Get("/style.css", style)
1097	handleOthers(r)
1098	handleOrder(r)
1099	if !f.NoCore {
1100		handleCORE(r)
1101	}
1102	go func() {
1103		err := r.Listen(fmt.Sprintf(":%d", f.WebPort))
1104		if err != nil {
1105			log.Println("Error serving http: ", err)
1106		}
1107		wg.Done()
1108	}()
1109	if !f.NoCore {
1110		watchCORE()
1111	}
1112	compileWASM()
1113	wg.Wait()
1114}
1115
1116func sitemap(c fiber.Ctx) error {
1117	c.Type("xml", "utf-8")
1118	return c.SendString(generateSitemapXML())
1119}
1120
1121func clock(c fiber.Ctx) error {
1122	c.Set("Content-Type", "text/html;charset=utf-8")
1123	_, err := c.Status(fiber.StatusOK).Write([]byte(h.Clock()))
1124	return err
1125}
1126
1127func logo(c fiber.Ctx) error {
1128	tmpl, err := auxTmpl()
1129	if err != nil {
1130		msg := fmt.Sprintf("Error parsing html template: %v", err)
1131		log.Println(msg)
1132		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1133	}
1134	tmpl0, err := tmpl.Clone()
1135	if err != nil {
1136		msg := fmt.Sprintf("Error cloning template: %v", err)
1137		log.Println(msg)
1138		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1139	}
1140	_, err = tmpl0.New("main").Parse(h.Logo())
1141	if err != nil {
1142		msg := fmt.Sprintf("Error parsing product page template: %v", err)
1143		log.Println(msg)
1144		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1145	}
1146	tmpl = tmpl0
1147	c.Set("Content-Type", "text/html;charset=utf-8")
1148
1149	img2txtFlags := ""
1150	if w, err := strconv.Atoi(c.Params("width")); err == nil {
1151		img2txtFlags = fmt.Sprintf("--width=%d ",w)
1152	}
1153	if h, err := strconv.Atoi(c.Params("height")); err == nil {
1154		img2txtFlags = fmt.Sprintf("--height=%d ",h)
1155	}
1156
1157	logoHTMLslice, err := script.Exec(fmt.Sprintf("bash -c 'img2txt %s logo.jpg | ansifilter -H'", img2txtFlags)).Slice()
1158	if err != nil {
1159		log.Println("error: ", err)
1160		_, err = c.Status(fiber.StatusInternalServerError).Write([]byte(err.Error()+"/n"+strings.Join(logoHTMLslice,"\n")))
1161		return err
1162	}
1163	if len(logoHTMLslice) > 2 {
1164	    logoHTMLslice = logoHTMLslice[:len(logoHTMLslice)-3]
1165	}
1166	if len(logoHTMLslice) > 18 {
1167	    logoHTMLslice = logoHTMLslice[19:]
1168	}
1169
1170
1171	var result bytes.Buffer
1172	h1 := pageMeta(c, htmlTemplateData{})
1173	h1.Page = "logo"
1174	h1.Title = "logo"
1175	tmplData := map[string]interface{}{
1176		"Content": strings.Join(logoHTMLslice, "\n"),
1177	}
1178	err = tmpl.Execute(&result, tmplData)
1179	if err != nil {
1180		log.Println("error: ", err)
1181		_, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1182		return err
1183	}
1184	_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1185	return err
1186}
1187
1188func robots(c fiber.Ctx) error {
1189	c.Set("Content-Type", "text/plain;charset=utf-8")
1190	_, err := c.Status(fiber.StatusOK).Write([]byte(fmt.Sprintf("User-agent: *\n\nSitemap: https://%s/sitemap.xml", c.Hostname())))
1191	return err
1192}
1193
1194func style(c fiber.Ctx) error {
1195	c.Set("Content-Type", "text/css;charset=utf-8")
1196	_, err := c.Status(fiber.StatusOK).Write([]byte(h.StyleCSS()))
1197	return err
1198}
1199
1200func serveWASM(r *fiber.App) {
1201	if f.WasmExecPath != "" {
1202		_, err := script.File(f.WasmExecPath).Bytes()
1203		if err != nil {
1204			log.Printf("Error reading %s: %v\n", f.WasmExecPath, err)
1205		} else { //the wasm exec must be present or none of the webassembly stuff will work ; provided by the golang installaton
1206			r.Get(f.WasmExecPathTinyGo, func(c fiber.Ctx) error {
1207				wasmExecData, err := script.File(f.WasmExecPathTinyGo).Bytes()
1208				if err != nil {
1209					log.Printf("Error reading %s: %v\n", f.WasmExecPathTinyGo, err)
1210					return c.SendStatus(fiber.StatusNotFound)
1211				}
1212				c.Set("Content-Type", "application/js")
1213				_, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1214				return err
1215			})
1216
1217			r.Get(f.WasmExecPathGo, func(c fiber.Ctx) error {
1218				wasmExecData, err := script.File(f.WasmExecPathGo).Bytes()
1219				if err != nil {
1220					log.Printf("Error reading %s: %v\n", f.WasmExecPathGo, err)
1221					return c.SendStatus(fiber.StatusNotFound)
1222				}
1223				c.Set("Content-Type", "application/js")
1224				_, err = c.Status(fiber.StatusOK).Write(wasmExecData)
1225				return err
1226			})
1227
1228			suffix := ".wasm"
1229			if f.UseTinygo {
1230				suffix = "-tiny.wasm"
1231			}
1232			for _, wasmSRC := range f.WasmSRC {
1233				outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + suffix
1234				r.Get("/"+outputFile, func(c fiber.Ctx) error {
1235					data, err := script.File(outputFile).Bytes()
1236					if err != nil {
1237						script.File(outputFile).Stdout() //nolint
1238						return c.SendStatus(fiber.StatusInternalServerError)
1239					}
1240					c.Set("Content-Type", "application/wasm")
1241					return c.Status(fiber.StatusOK).Send(data)
1242				})
1243			}
1244		}
1245	}
1246}
1247
1248func sendFile(c fiber.Ctx) error {
1249	return c.SendFile("." + c.Path())
1250}
1251func sendImage(c fiber.Ctx) error {
1252	c.Set("Content-Type", "image/jpeg")
1253	return c.SendFile("./img" + c.Path())
1254}
1255
1256func stlbase64(c fiber.Ctx) error {
1257	name := c.Params("filename")
1258	if strings.ContainsAny(name, "/\\..") || strings.Contains(name, "..") {
1259		return c.SendStatus(fiber.StatusBadRequest)
1260	}
1261	stlfile, err := script.File("img/stl/" + name).Bytes()
1262	if err != nil {
1263		return c.SendStatus(fiber.StatusNotFound)
1264	}
1265	_, err = c.Status(fiber.StatusOK).Write([]byte("data:model/stl;base64," + base64.StdEncoding.EncodeToString(stlfile)))
1266	return err
1267}
1268
1269type item struct {
1270	ID     string
1271	Amount int64
1272}
1273
1274func cathtmlfunc(c fiber.Ctx) error {
1275	tmpl, err := mainTmpl()
1276	if err != nil {
1277		msg := fmt.Sprintf("Error parsing html template: %v", err)
1278		log.Println(msg)
1279		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1280	}
1281	tmpl0, err := tmpl.Clone()
1282	if err != nil {
1283		msg := fmt.Sprintf("Error cloning html template: %v", err)
1284		log.Println(msg)
1285		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1286	}
1287	_, err = tmpl0.New("main").Parse(h.CategoryPage())
1288	if err != nil {
1289		msg := fmt.Sprintf("Error parsing Category page template: %v", err)
1290		log.Println(msg)
1291		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1292	}
1293	tmpl = tmpl0
1294	var tmplData map[string]interface{}
1295	var result bytes.Buffer
1296	var categoryproducts p.Products
1297	c.Set("Content-Type", "text/html;charset=utf-8")
1298	h1 := pageMeta(c, htmlPageTemplateData)
1299	h1.Title = fmt.Sprintf("%s | %s", func() string {
1300		var str string
1301		if c.Params("partno") != "" {
1302			return "No product matching partno.: " + c.Params("partno") + " | Showing All Products"
1303		}
1304		if c.Params("cat") == "" {
1305			return "All Products"
1306		}
1307		str = fmt.Sprintf("Category: %s", c.Params("cat"))
1308		if c.Params("subcat") != "" {
1309			str += fmt.Sprintf("; Subcategory: %s", c.Params("subcat"))
1310		}
1311		return str
1312	}(), h1.Title)
1313	h1.Page = "category"
1314	if c.Params("cat") == "" && c.Params("subcat") == "" {
1315		tmplData = map[string]interface{}{
1316			"Products":    allproducts,
1317			"Page":        h1,
1318			"Category":    c.Params("cat"),
1319			"Subcategory": c.Params("subcat"),
1320			"Prods":       allproducts,
1321			"Product":     c.Params("partno"),
1322		}
1323	} else {
1324
1325		for _, prod := range allproducts {
1326			if strings.EqualFold(prod.Category, c.Params("cat")) && (c.Params("subcat") == "" || strings.EqualFold(escapesubcat(prod.Subcategory), c.Params("subcat"))) {
1327				categoryproducts = append(categoryproducts, prod)
1328			}
1329		}
1330		tmplData = map[string]interface{}{
1331			"Products":    categoryproducts,
1332			"Page":        h1,
1333			"Category":    c.Params("cat"),
1334			"Subcategory": c.Params("subcat"),
1335			"Prods":       allproducts,
1336		}
1337	}
1338	err = tmpl.Execute(&result, tmplData)
1339	if err != nil {
1340		msg := fmt.Sprintf("Error execute html template: %v", err)
1341		log.Println(msg)
1342		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1343	}
1344	_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1345	return err
1346}
1347
1348func getcats() (cats []string) {
1349	var catsMap = make(map[string]int)
1350	for _, prod := range allproducts {
1351		catsMap[prod.Category]++
1352	}
1353	for cat := range catsMap {
1354		cats = append(cats, cat)
1355	}
1356	return cats
1357}
1358func contains(slice []string, str string) bool {
1359	for _, s := range slice {
1360		if s == str {
1361			return true
1362		}
1363	}
1364	return false
1365}
1366func getcategories(allproducts p.Products) (map[string]int, []string, map[string]map[string]int, map[string][]string) {
1367	categoryCounts := make(map[string]int)
1368	subcategoryCounts := make(map[string]map[string]int)
1369	subcategoriesByCategory := make(map[string][]string)
1370
1371	for _, prod := range allproducts {
1372		if prod.Category != "" {
1373			categoryCounts[prod.Category]++
1374			if prod.Subcategory != "" {
1375				if subcategoryCounts[prod.Category] == nil {
1376					subcategoryCounts[prod.Category] = make(map[string]int)
1377				}
1378				subcategoryCounts[prod.Category][prod.Subcategory]++
1379				if !contains(subcategoriesByCategory[prod.Category], prod.Subcategory) {
1380					subcategoriesByCategory[prod.Category] = append(subcategoriesByCategory[prod.Category], prod.Subcategory)
1381				}
1382			}
1383		}
1384	}
1385
1386	var sortableCategories []struct {
1387		Name  string
1388		Count int
1389	}
1390	for cat, count := range categoryCounts {
1391		sortableCategories = append(sortableCategories, struct {
1392			Name  string
1393			Count int
1394		}{Name: cat, Count: count})
1395	}
1396	sort.Slice(sortableCategories, func(i, j int) bool {
1397		return sortableCategories[i].Count > sortableCategories[j].Count
1398	})
1399	var sortedCategories []string
1400	for _, cat := range sortableCategories {
1401		sortedCategories = append(sortedCategories, cat.Name)
1402		var sortableSubcategories []struct {
1403			Name  string
1404			Count int
1405		}
1406		for subcat, count := range subcategoryCounts[cat.Name] {
1407			sortableSubcategories = append(sortableSubcategories, struct {
1408				Name  string
1409				Count int
1410			}{Name: subcat, Count: count})
1411		}
1412		sort.Slice(sortableSubcategories, func(i, j int) bool {
1413			return sortableSubcategories[i].Count > sortableSubcategories[j].Count
1414		})
1415		var sortedSubcategories []string
1416		for _, subcat := range sortableSubcategories {
1417			sortedSubcategories = append(sortedSubcategories, subcat.Name)
1418		}
1419		subcategoriesByCategory[cat.Name] = sortedSubcategories
1420	}
1421	return categoryCounts, sortedCategories, subcategoryCounts, subcategoriesByCategory
1422}
1423
1424func getsubcats(cat string) (subcats []string) {
1425	var subcatsMap = make(map[string]int)
1426	for _, prod := range allproducts {
1427		if cat == "" || strings.EqualFold(cat, prod.Category) {
1428			if prod.Subcategory != "" {
1429				subcatsMap[escapesubcat(prod.Subcategory)]++
1430			}
1431		}
1432	}
1433	for subcat := range subcatsMap {
1434		subcats = append(subcats, subcat)
1435	}
1436	return subcats
1437}
1438func escapesubcat(sc string) (esc string) {
1439	esc = strings.Replace(sc, "ΒΌ", "quarter-", -1)
1440	esc = strings.Replace(esc, "Β½", "half-", -1)
1441	esc = strings.Replace(esc, "1/16", "sixteenth-", -1)
1442	esc = strings.Replace(esc, "%", "-pct", -1)
1443	esc = strings.Replace(esc, "  ", " ", -1)
1444	esc = strings.Replace(esc, " ", "-", -1)
1445	esc = strings.Replace(esc, "--", "-", -1)
1446	esc = strings.Replace(esc, "watt1", "watt-1", -1)
1447	esc = strings.Replace(esc, "watt5", "watt-5", -1)
1448	return esc
1449}
1450
1451func handlecat(c fiber.Ctx) error {
1452	if c.Params("cat") == "" && c.Params("subcat") == "" {
1453		return cathtmlfunc(c)
1454	}
1455	var catexists bool
1456	var subcatexists bool
1457	catexists = false
1458	for _, cat := range getcats() {
1459		if strings.EqualFold(cat, c.Params("cat")) {
1460			catexists = true
1461			break
1462		}
1463	}
1464	subcatexists = false
1465	if c.Params("subcat") != "" {
1466		for _, subcat := range getsubcats("") {
1467			if strings.EqualFold(escapesubcat(subcat), c.Params("subcat")) {
1468				subcatexists = true
1469				break
1470			}
1471		}
1472	}
1473	if c.Params("subcat") != "" && !subcatexists {
1474		log.Printf("subcategory %s does not match any existing subcategory\n", c.Params("subcat"))
1475		return c.Redirect().To("/cat/" + c.Params("cat"))
1476	}
1477	if !catexists {
1478		log.Printf("category %s does not match any existing category\n", c.Params("cat"))
1479		return c.Redirect().To("/cat")
1480	}
1481	if catexists || (catexists && subcatexists) {
1482		return cathtmlfunc(c)
1483	}
1484	return c.SendStatus(fiber.StatusNotFound)
1485}
1486
1487func homepage(c fiber.Ctx) error {
1488	tmpl, err := mainTmpl()
1489	if err != nil {
1490		msg := fmt.Sprintf("Could not parsing html template: %v", err)
1491		log.Println(msg)
1492		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1493	}
1494	tmpl0, err := tmpl.Clone()
1495	if err != nil {
1496		msg := fmt.Sprintf("Error cloning template: %v", err)
1497		log.Println(msg)
1498		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1499	}
1500	_, err = tmpl0.New("main").Parse(h.FrontPage())
1501	if err != nil {
1502		msg := fmt.Sprintf("Error parsing Front Page template: %v", err)
1503		log.Println(msg)
1504		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1505	}
1506	_, err = tmpl0.New("about").Parse(h.AboutPage())
1507	if err != nil {
1508		msg := fmt.Sprintf("Error parsing About Page template: %v", err)
1509		log.Println(msg)
1510		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1511	}
1512	_, err = tmpl0.New("policy").Parse(h.PolicyPage())
1513	if err != nil {
1514		msg := fmt.Sprintf("Error parsing Policy Page template: %v", err)
1515		log.Println(msg)
1516		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1517	}
1518	_, err = tmpl0.New("links").Parse(h.LinksPage())
1519	if err != nil {
1520		msg := fmt.Sprintf("Error parsing Links Page template: %v", err)
1521		log.Println(msg)
1522		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1523	}
1524	tmpl = tmpl0
1525	log.Println(c.Get("User-Agent"))
1526	c.Set("Content-Type", "text/html;charset=utf-8")
1527	h1 := pageMeta(c, htmlPageTemplateData)
1528	tmplData := map[string]interface{}{
1529		"Page":  h1,
1530		"Prods": allproducts,
1531	}
1532	var result bytes.Buffer
1533	err = tmpl.Execute(&result, tmplData)
1534	if err != nil {
1535		msg := fmt.Sprintf("Error executing template: %v", err)
1536		log.Println(msg)
1537		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1538	}
1539	_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1540	return err
1541}
1542
1543func productpage(c fiber.Ctx) error {
1544	tmpl, err := mainTmpl()
1545	if err != nil {
1546		msg := fmt.Sprintf("Error parsing html template: %v", err)
1547		log.Println(msg)
1548		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1549	}
1550	tmpl0, err := tmpl.Clone()
1551	if err != nil {
1552		msg := fmt.Sprintf("Error cloning template: %v", err)
1553		log.Println(msg)
1554		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1555	}
1556	_, err = tmpl0.New("main").Parse(h.ProductPage())
1557	if err != nil {
1558		msg := fmt.Sprintf("Error parsing product page template: %v", err)
1559		log.Println(msg)
1560		return c.Status(fiber.StatusInternalServerError).SendString(msg)
1561	}
1562	tmpl = tmpl0
1563	c.Set("Content-Type", "text/html;charset=utf-8")
1564	for _, prod := range allproducts {
1565		if strings.EqualFold(prod.Partno, c.Params("partno")) {
1566			var result bytes.Buffer
1567			h1 := pageMeta(c, htmlPageTemplateData)
1568			h1.Page = "product"
1569			h1.Title = fmt.Sprintf("%s | %s", prod.Name, h1.Title)
1570			tmplData := map[string]interface{}{
1571				"Prod":  prod,
1572				"Page":  h1,
1573				"Prods": allproducts,
1574			}
1575			err := tmpl.Execute(&result, tmplData)
1576			if err != nil {
1577				log.Println("error: ", err)
1578				_, err = c.Status(fiber.StatusInternalServerError).Write(result.Bytes())
1579				return err
1580			}
1581			_, err = c.Status(fiber.StatusOK).Write(collapseNewlines.ReplaceAll(result.Bytes(), []byte("\n")))
1582			return err
1583		}
1584	}
1585	log.Printf("product %s does not match any existing product\n", c.Params("partno"))
1586	return c.Status(fiber.StatusNotFound).Redirect().To("/cat")
1587}
1588
1589// attractorspage renders a chromeless fullscreen page for the
1590// strange-attractor visualizer (no header, footer, cart, or store
1591// nav). Reuses the existing stl2 wasm β€” its URL-path dispatcher
1592// sees "/attractors" and falls into the default branch which
1593// invokes attractor.Run(). Loads the TinyGo or stdlib wasm based
1594// on f.UseTinygo.
1595func attractorspage(c fiber.Ctx) error {
1596	suffix := ".wasm"
1597	if f.UseTinygo {
1598		suffix = "-tiny.wasm"
1599	}
1600	wasmFile := "stl2" + suffix
1601	html := fmt.Sprintf(`<!DOCTYPE html>
1602<html lang="en">
1603<head>
1604<meta charset="utf-8">
1605<meta name="viewport" content="width=device-width, initial-scale=1">
1606<title>Strange Attractors β€” %s</title>
1607<meta name="description" content="Interactive 3D strange-attractor visualizer with mouse-drag rotation. Lorenz, Rossler, Chua, Aizawa, Sprott, Lissajous, Thomas, Halvorsen, Chen, Dadras, Rabinovich-Fabrikant, Burke-Shaw, Platonic solids, globe, sphere, torus, magnetosphere.">
1608<meta name="robots" content="index, follow">
1609<style>html,body{margin:0;padding:0;width:100%%;height:100%%;background:#000;color:#fff;overflow:hidden;}#gocanvas{position:fixed;top:0;left:0;width:100%%;height:100%%;display:block;}</style>
1610<script src="%s"></script>
1611<script>
1612if (!WebAssembly.instantiateStreaming) {
1613  WebAssembly.instantiateStreaming = async (resp, importObject) => {
1614    const source = await (await resp).arrayBuffer();
1615    return await WebAssembly.instantiate(source, importObject);
1616  };
1617}
1618const go = new Go();
1619WebAssembly.instantiateStreaming(fetch("/%s"), go.importObject).then((result) => {
1620  go.run(result.instance);
1621}).catch((err) => { console.error("Failed to run WASM:", err); });
1622</script>
1623</head>
1624<body>
1625<canvas id="gocanvas"></canvas>
1626</body>
1627</html>`, f.Sitelongname, f.WasmExecPath, wasmFile)
1628	c.Set("Content-Type", "text/html; charset=utf-8")
1629	return c.Status(fiber.StatusOK).SendString(html)
1630}
1631
1632
1633// ===== order.go =====
1634// Package main order.go
1635package main
1636
1637import (
1638	"bytes"
1639	"encoding/json"
1640	"fmt"
1641	htmpl "html/template"
1642	"log"
1643	"os"
1644	"path/filepath"
1645	"regexp"
1646	"strconv"
1647	"strings"
1648	"time"
1649
1650	"github.com/bitfield/script"
1651	"github.com/gofiber/fiber/v3"
1652	"github.com/stripe/stripe-go/v81"
1653	"github.com/stripe/stripe-go/v81/paymentintent"
1654)
1655
1656// validPIID matches Stripe PaymentIntent IDs: "pi_" followed by alphanumeric chars.
1657// Also allows plain alphanumeric+underscore+hyphen for test order IDs.
1658var validPIID = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
1659
1660func handleOrder(r *fiber.App) {
1661	r.Get("/checkout.css", func(c fiber.Ctx) error {
1662		c.Set("Content-Type", "text/css;charset=utf-8")
1663		_, err := c.Status(fiber.StatusOK).Write([]byte(h.CheckoutCSS()))
1664		return err
1665	})
1666
1667	r.Get("/complete", func(c fiber.Ctx) error {
1668		// Complete template
1669		completetmpl := htmpl.New("index")
1670		if _, err := completetmpl.Parse(h.CompletePage()); err != nil {
1671			msg := fmt.Sprintf("Error parsing complete page template: %v", err)
1672			log.Println(msg)
1673			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1674		}
1675		if _, err := completetmpl.New("wasm").Parse(h.Wasm()); err != nil {
1676			log.Println("Error parsing wasm template:", err)
1677			msg := fmt.Sprintf("Error parsing wasm template: %v", err)
1678			log.Println(msg)
1679			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1680		}
1681		h1 := htmlPageTemplateData
1682		/*
1683			proto := "http"
1684			if c.Secure() {
1685				proto += "s"
1686			}
1687		*/
1688		proto := "https"
1689		h1.Canonical = proto + `://` + c.Hostname() + c.OriginalURL()
1690		h1.BaseURL = proto + `://` + c.Hostname()
1691		h1.RequestHost = c.Hostname()
1692		h1.Protocol = proto
1693		h1.Time = time.Now().Format(time.RFC3339Nano)
1694		h1.Year = fmt.Sprintf("%v", time.Now().Year())
1695		tmplData := map[string]interface{}{
1696			"Page": h1,
1697		}
1698		var result bytes.Buffer
1699		err := completetmpl.Execute(&result, tmplData)
1700		if err != nil {
1701			msg := fmt.Sprintf("Could not execute html template %v", err)
1702			log.Println(msg)
1703			return c.Status(fiber.StatusInternalServerError).SendString(msg)
1704		}
1705		c.Set("Content-Type", "text/html;charset=utf-8")
1706		return c.Status(fiber.StatusOK).Send(result.Bytes())
1707	})
1708
1709	r.Get("/order/:piid", func(c fiber.Ctx) error {
1710		piid := c.Params("piid")
1711		if !validPIID.MatchString(piid) {
1712			return c.Status(fiber.StatusBadRequest).SendString("Invalid order ID")
1713		}
1714		order, err := script.File("orders/" + piid + ".json").Bytes()
1715		if err != nil {
1716			return c.Status(fiber.StatusNotFound).SendString("Order not found")
1717		}
1718		return c.Status(fiber.StatusOK).Send(order)
1719	})
1720
1721	r.Get("/order/:piid/html", func(c fiber.Ctx) error {
1722		piid := c.Params("piid")
1723		if !validPIID.MatchString(piid) {
1724			return c.Status(fiber.StatusBadRequest).SendString("Invalid order ID")
1725		}
1726		order, err := script.File("orders/" + piid + ".json").Bytes()
1727		if err != nil {
1728			return c.Status(fiber.StatusNotFound).SendString("Order not found")
1729		}
1730		var m map[string]interface{}
1731		if err := json.Unmarshal(order, &m); err != nil {
1732			return c.Status(500).SendString("failed to unmarshal order json: " + err.Error())
1733		}
1734		receipt, err := buildReceipt(m, piid)
1735		if err != nil {
1736			return c.Status(500).SendString("failed to build receipt: " + err.Error())
1737		}
1738		return c.Status(200).SendString(string(receipt))
1739	})
1740
1741	r.Post("/create-payment-intent", func(c fiber.Ctx) error {
1742		rawBody := c.Body()
1743		if rawBody == nil {
1744			log.Printf("Failed to read raw request body")
1745			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to read request body"})
1746		}
1747
1748		var req struct {
1749			Items []item `json:"items"`
1750		}
1751		if err := json.Unmarshal(rawBody, &req); err != nil {
1752			log.Printf("Failed to parse JSON: %v", err)
1753			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
1754		}
1755
1756		if len(req.Items) == 0 {
1757			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No items in request"})
1758		}
1759
1760		// Validate each item's amount against the server-side product catalog.
1761		// Client sends ID as "partno X qty" for products, or "shipping-to|..." for shipping.
1762		total := int64(0)
1763		for _, it := range req.Items {
1764			if it.Amount <= 0 {
1765				log.Printf("Rejected item with non-positive amount: %s = %d", it.ID, it.Amount)
1766				return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid item amount"})
1767			}
1768			if strings.HasPrefix(it.ID, "shipping-to|") {
1769				// Shipping line β€” accept the client-supplied amount
1770				total += it.Amount
1771				continue
1772			}
1773			// Extract partno and qty from "partno X qty"
1774			expectedAmt, err := validateItemAmount(it.ID, it.Amount)
1775			if err != nil {
1776				log.Printf("Item validation failed for %q: %v", it.ID, err)
1777				return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Item validation failed"})
1778			}
1779			total += expectedAmt
1780		}
1781
1782		if total < 50 {
1783			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Order total must be at least $0.50"})
1784		}
1785
1786		params := &stripe.PaymentIntentParams{
1787			Amount:   stripe.Int64(total),
1788			Currency: stripe.String(string(stripe.CurrencyUSD)),
1789		}
1790		pi, err := paymentintent.New(params)
1791		if err != nil {
1792			log.Printf("Failed to create PaymentIntent: %v", err)
1793			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
1794		}
1795
1796		log.Printf("Created PaymentIntent %s for %d cents", pi.ID, total)
1797		return c.Status(fiber.StatusOK).JSON(fiber.Map{
1798			"clientSecret":   pi.ClientSecret,
1799			"dpmCheckerLink": fmt.Sprintf("https://dashboard.stripe.com/settings/payment_methods/review?transaction_id=%s", pi.ID),
1800		})
1801	})
1802
1803	r.Post("/submit-order", func(c fiber.Ctx) error {
1804		var requestData struct {
1805			LocalStorageData map[string]interface{} `json:"localStorageData"`
1806			PaymentIntentID  string                 `json:"paymentIntentId"`
1807		}
1808
1809		if err := c.Bind().Body(&requestData); err != nil {
1810			log.Println(err)
1811			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request data"})
1812		}
1813
1814		if !validPIID.MatchString(requestData.PaymentIntentID) {
1815			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payment intent ID"})
1816		}
1817
1818		log.Printf("Received payment intent ID: %s\n", requestData.PaymentIntentID)
1819
1820		paymentIntent, err := paymentintent.Get(requestData.PaymentIntentID, nil)
1821		if err != nil {
1822			log.Printf("Error retrieving payment intent: %v", err)
1823			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to verify payment"})
1824		}
1825		if paymentIntent.Status != stripe.PaymentIntentStatusSucceeded {
1826			log.Printf("Payment was not successful, status: %s", paymentIntent.Status)
1827			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Payment not successful"})
1828		}
1829
1830		ordersDir := "./orders"
1831		if err := os.MkdirAll(ordersDir, os.ModePerm); err != nil {
1832			log.Printf("Error creating orders directory: %v", err)
1833			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1834		}
1835
1836		filePath := filepath.Join(ordersDir, fmt.Sprintf("%s.json", requestData.PaymentIntentID))
1837
1838		// Idempotency: if the order file already exists, don't overwrite or reprint
1839		if _, err := os.Stat(filePath); err == nil {
1840			log.Printf("Order %s already exists, skipping duplicate submission", requestData.PaymentIntentID)
1841			return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Order already submitted"})
1842		}
1843
1844		// Include the verified Stripe amount alongside the client-supplied data
1845		orderData := map[string]interface{}{
1846			"clientData":    requestData.LocalStorageData,
1847			"verifiedCents": paymentIntent.Amount,
1848			"currency":      string(paymentIntent.Currency),
1849			"stripeStatus":  string(paymentIntent.Status),
1850			"submittedAt":   time.Now().Format(time.RFC3339),
1851		}
1852
1853		data, err := json.MarshalIndent(orderData, "", "  ")
1854		if err != nil {
1855			log.Printf("Error marshaling data to json: %v", err)
1856			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1857		}
1858		if err := os.WriteFile(filePath, data, 0o644); err != nil {
1859			log.Printf("Error writing data to file: %v", err)
1860			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Unable to save order"})
1861		}
1862
1863		// ---- Print receipt via CUPS (non-blocking so your response is snappy)
1864		go func(pid string, local map[string]interface{}) {
1865			receipt, err := buildReceipt(local, pid)
1866			if err != nil {
1867				log.Printf("build receipt failed: %v", err)
1868				return
1869			}
1870			if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1871				log.Printf("print failed: %v", err)
1872				_ = os.WriteFile(filepath.Join(ordersDir, pid+".print_failed"), []byte(err.Error()), 0o644)
1873			}
1874		}(requestData.PaymentIntentID, requestData.LocalStorageData)
1875
1876		return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Order submitted successfully"})
1877	})
1878
1879	/*
1880			r.Post("/reprint/:pid", func(c fiber.Ctx) error {
1881		    pid := c.Params("pid")
1882		    b, err := os.ReadFile(filepath.Join("./orders", pid+".json"))
1883		    if err != nil { return c.Status(404).SendString("not found") }
1884		    var m map[string]interface{}
1885		    if err := json.Unmarshal(b, &m); err != nil { return c.Status(500).SendString(err.Error()) }
1886		    receipt, err := buildReceipt(m, pid)
1887		    if err != nil { return c.Status(500).SendString(err.Error()) }
1888		    if err := sendToCUPS(receipt, "Order "+pid); err != nil {
1889		        return c.Status(500).SendString(err.Error())
1890		    }
1891		    return c.SendStatus(204)
1892		})
1893	*/
1894}
1895
1896func buildReceipt(local map[string]interface{}, paymentIntentID string) ([]byte, error) {
1897	// Pretty JSON body from what you already persisted
1898	body, err := json.MarshalIndent(local, "", "  ")
1899	if err != nil {
1900		return nil, err
1901	}
1902	// Simple text receipt header
1903	ts := time.Now().Format("2006-01-02 15:04:05")
1904	hdr := fmt.Sprintf(
1905		"==================== ORDER ====================\n"+
1906			"PaymentIntent: %s\nTime: %s\n===============================================\n\n",
1907		paymentIntentID, ts,
1908	)
1909	// Footer (optional)
1910	ftr := "\n\n---------------------- END ---------------------\n"
1911	receipt := append([]byte(hdr), body...)
1912	receipt = append(receipt, []byte(ftr)...)
1913	return receipt, nil
1914}
1915
1916// serverPriceCents looks up a product's price from the server-side catalog by part number.
1917func serverPriceCents(partno string) (int64, error) {
1918	allproductsMu.RLock()
1919	prods := allproducts
1920	allproductsMu.RUnlock()
1921	for _, prod := range prods {
1922		if prod.Partno == partno {
1923			return parsePriceCents(prod.Price), nil
1924		}
1925	}
1926	return 0, fmt.Errorf("product %q not found in catalog", partno)
1927}
1928
1929// parsePriceCents converts a price string like "$1.23" or "1.23" to cents.
1930func parsePriceCents(s string) int64 {
1931	if s == "" {
1932		return 0
1933	}
1934	s = strings.TrimPrefix(s, "$")
1935	f, err := strconv.ParseFloat(s, 64)
1936	if err != nil {
1937		return 0
1938	}
1939	if f < 0 {
1940		return -int64(-f*100 + 0.5)
1941	}
1942	return int64(f*100 + 0.5)
1943}
1944
1945// validateItemAmount parses a client item ID ("partno X qty"), looks up the
1946// server-side price, computes the expected total, and returns it. If the
1947// client-supplied amount doesn't match, an error is returned.
1948func validateItemAmount(itemID string, clientAmount int64) (int64, error) {
1949	// Parse "partno X qty"
1950	parts := strings.SplitN(itemID, " X ", 2)
1951	if len(parts) != 2 {
1952		return 0, fmt.Errorf("unexpected item ID format: %q", itemID)
1953	}
1954	partno := parts[0]
1955	qty, err := strconv.Atoi(parts[1])
1956	if err != nil || qty <= 0 {
1957		return 0, fmt.Errorf("invalid quantity in item ID %q", itemID)
1958	}
1959
1960	unitCents, err := serverPriceCents(partno)
1961	if err != nil {
1962		return 0, err
1963	}
1964	expected := unitCents * int64(qty)
1965	if expected != clientAmount {
1966		return 0, fmt.Errorf("amount mismatch for %q: client sent %d cents, server expects %d cents", partno, clientAmount, expected)
1967	}
1968	return expected, nil
1969}
1970
1971// escape for inclusion inside *double quotes* in a bash command string
1972func bashEscapeDoubleQuoted(s string) string {
1973	s = strings.ReplaceAll(s, `\`, `\\`)
1974	s = strings.ReplaceAll(s, `"`, `\"`)
1975	s = strings.ReplaceAll(s, "$", `\$`)
1976	s = strings.ReplaceAll(s, "`", "\\`")
1977	return s
1978}
1979
1980func sendToCUPS(receipt []byte, title string) error {
1981	if title == "" {
1982		title = "Order"
1983	}
1984	var cmd strings.Builder
1985	cmd.WriteString("lp")
1986
1987	if f.PrinterName != "" {
1988		cmd.WriteString(` -d "`)
1989		cmd.WriteString(bashEscapeDoubleQuoted(f.PrinterName))
1990		cmd.WriteString(`"`)
1991	}
1992
1993	cmd.WriteString(` -t "`)
1994	cmd.WriteString(bashEscapeDoubleQuoted(title))
1995	cmd.WriteString(`"`)
1996
1997	if f.CupsOptions != "" {
1998		for _, opt := range strings.Split(f.CupsOptions, ",") {
1999			opt = strings.TrimSpace(opt)
2000			if opt == "" {
2001				continue
2002			}
2003			cmd.WriteString(` -o "`)
2004			cmd.WriteString(bashEscapeDoubleQuoted(opt))
2005			cmd.WriteString(`"`)
2006		}
2007	}
2008
2009	full := fmt.Sprintf(`bash -lc %q`, cmd.String())
2010
2011	_, err := script.Echo(string(receipt)).Exec(full).Stdout()
2012	if err != nil {
2013		return fmt.Errorf("lp failed: %v", err)
2014	}
2015	return nil
2016}
2017
2018
2019// ===== other.go =====
2020// Package main other.go
2021package main
2022
2023import (
2024	"bytes"
2025	"fmt"
2026	"log"
2027
2028	"github.com/gofiber/fiber/v3"
2029)
2030
2031func handleOthers(r *fiber.App) {
2032	r.Get("/COVID", func(c fiber.Ctx) error {
2033		tmpl, err := mainTmpl()
2034		if err != nil {
2035			msg := fmt.Sprintf("Error parse html template: %v", err)
2036			log.Println(msg)
2037			return c.Status(fiber.StatusInternalServerError).SendString(msg)
2038		}
2039		tmpl0, err := tmpl.Clone()
2040		if err != nil {
2041			msg := fmt.Sprintf("Error cloning template: %v", err)
2042			log.Println(msg)
2043			return c.Status(fiber.StatusInternalServerError).SendString(msg)
2044		}
2045		_, err = tmpl0.New("main").Parse(h.COVIDPage())
2046		if err != nil {
2047			msg := fmt.Sprintf("Error parsing main template: %v", err)
2048			log.Println(msg)
2049			return c.Status(fiber.StatusInternalServerError).SendString(msg)
2050		}
2051		tmpl = tmpl0
2052		log.Println(c.Get("User-Agent"))
2053		c.Set("Content-Type", "text/html;charset=utf-8")
2054		h1 := pageMeta(c, htmlPageTemplateData)
2055		h1.Page = "hidden"
2056		h1.MetaDesc = "The COVID  ΜΆvΜΆaΜΆcΜΆcΜΆiΜΆnΜΆeΜΆ bioweapon injection genocide and the new dark age of humanity"
2057		//		h1.Mobile = strings.Contains(strings.ToLower(c.Get("User-Agent")), "mobile")
2058		tmplData := map[string]interface{}{
2059			"Page":  h1,
2060			"Prods": allproducts,
2061		}
2062		var result bytes.Buffer
2063		err = tmpl.Execute(&result, tmplData)
2064		if err != nil {
2065			msg := fmt.Sprintf("Error executing template: %v", err)
2066			log.Println(msg)
2067			return c.Status(fiber.StatusInternalServerError).SendString(msg)
2068		}
2069		_, err = c.Status(fiber.StatusOK).Write(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(bytes.Replace(result.Bytes(), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1), []byte("\n\n"), []byte("\n"), -1))
2070		return err
2071	})
2072
2073}
2074
2075
2076// ===== source.go =====
2077// Package main source.go
2078package main
2079
2080import (
2081	"bytes"
2082	"embed"
2083	"fmt"
2084	"io/fs"
2085	"os"
2086	"strings"
2087
2088	"github.com/alecthomas/chroma/v2"
2089	"github.com/alecthomas/chroma/v2/formatters/html"
2090	"github.com/alecthomas/chroma/v2/lexers"
2091	"github.com/alecthomas/chroma/v2/styles"
2092	"github.com/gofiber/fiber/v3"
2093)
2094
2095//go:embed *.go
2096var quine embed.FS
2097
2098var sourceWasm = os.DirFS("wasm")
2099var sourcesWasm []fs.FS
2100var sourceCore = os.DirFS("ui")
2101var sourceHtml = os.DirFS("htmpl")
2102var sourceContent = os.DirFS("content")
2103
2104func serveSourceCode(r *fiber.App) {
2105	for _, wasmSRC := range f.WasmSRC {
2106		sourcesWasm = append(sourcesWasm, os.DirFS(wasmSRC))
2107	}
2108	r.Get("/sourcecode", func(c fiber.Ctx) error {
2109		ret := `<!doctype html>
2110<html lang='en'>
2111<head>
2112<link rel="stylesheet" href="/style.css" type="text/css">
2113</head>
2114<body class='grid-container' style='background-color:black;color:white;'>
2115<a href='/sourcecode/go'>GO</a><br><br>
2116
2117<a href='/sourcecode/html'>HTML</a><br><br>
2118
2119<a href='/sourcecode/content'>Content</a><br><br>
2120
2121<a href='/sourcecode/core'>C.O.R.E.</a><br><br>
2122
2123<a href='/sourcecode/wasm'>WASM</a><br><br>
2124
2125</body>
2126</html>
2127`
2128		c.Set("Content-Type", "text/html;charset=utf-8")
2129		_, err := c.Status(fiber.StatusOK).Write([]byte(ret))
2130		return err
2131	})
2132
2133	r.Get("/sourcecode/html", sourcecodehtml)
2134	r.Get("/sourcecode/content", sourcecodecontent)
2135	r.Get("/sourcecode/go", sourcecodego)
2136	r.Get("/sourcecode/core", sourcecodecore)
2137	//	r.Get("/sourcecodewasm", sourcecodewasm)
2138	r.Get("/sourcecode/wasm", func(c fiber.Ctx) error {
2139		ret := `<!doctype html>
2140<html lang='en'>
2141<head>
2142<link rel="stylesheet" href="/style.css" type="text/css">
2143</head>
2144<body class='grid-container' style='background-color:black;color:white;'>
2145`
2146		for _, wasmSRC := range f.WasmSRC {
2147			pathNameSlc := strings.Split(wasmSRC, "/")
2148			pathName := pathNameSlc[len(pathNameSlc)-1]
2149			ret += `<a href='/sourcecode/wasm/` + pathName + `'>` + pathName + `</a><br>
2150			`
2151		}
2152		ret += `</body></html>
2153		`
2154		c.Set("Content-Type", "text/html;charset=utf-8")
2155		_, err := c.Status(fiber.StatusOK).Write([]byte(ret))
2156		return err
2157	})
2158
2159	for i, wasmSRC := range f.WasmSRC {
2160		pathNameSlc := strings.Split(wasmSRC, "/")
2161		pathName := pathNameSlc[len(pathNameSlc)-1]
2162		r.Get("/sourcecode/wasm/"+pathName, func(c fiber.Ctx) error {
2163			return sourcecode(c, sourcesWasm[i], "dracula", "go")
2164		})
2165	}
2166}
2167
2168func sourcecodehtml(c fiber.Ctx) error {
2169	return sourcecode(c, sourceHtml, "monokai", "html")
2170}
2171func sourcecodecontent(c fiber.Ctx) error {
2172	return sourcecode(c, sourceContent, "monokai", "html")
2173}
2174func sourcecodego(c fiber.Ctx) error {
2175	return sourcecode(c, quine, "monokai", "go")
2176}
2177
2178func sourcecodewasm(c fiber.Ctx) error {
2179	return sourcecode(c, sourceWasm, "dracula", "go")
2180}
2181
2182func sourcecodecore(c fiber.Ctx) error {
2183	return sourcecode(c, sourceCore, "solarized-dark256", "go")
2184}
2185
2186func sourcecode(c fiber.Ctx, fsys fs.FS, styleName string, lang string) error {
2187	c.Set("Content-Type", "text/html;charset=utf-8")
2188	var buf bytes.Buffer
2189	var builder strings.Builder
2190
2191	fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
2192		if err != nil {
2193			return err
2194		}
2195		if !d.IsDir() && strings.HasSuffix(path, "."+lang) {
2196			content, err := fs.ReadFile(fsys, path)
2197			if err != nil {
2198				return err
2199			}
2200			builder.WriteString(fmt.Sprintf("// ===== %s =====\n", path))
2201			builder.Write(content)
2202			builder.WriteString("\n\n")
2203		}
2204		return nil
2205	})
2206
2207	// Pick lexer & style
2208	lexer := lexers.Get(lang)
2209	if lexer == nil {
2210		lexer = lexers.Fallback
2211	}
2212	lexer = chroma.Coalesce(lexer)
2213
2214	style := styles.Get(styleName)
2215	if style == nil {
2216		style = styles.Fallback
2217	}
2218
2219	// Formatter with line numbers & CSS classes
2220	formatter := html.New(
2221		html.WithLineNumbers(true),
2222		html.WithClasses(true),
2223	)
2224
2225	iterator, err := lexer.Tokenise(nil, builder.String())
2226	if err != nil {
2227		return err
2228	}
2229
2230	// Optional: include CSS in output
2231	var css bytes.Buffer
2232	_ = formatter.WriteCSS(&css, style)
2233	buf.WriteString("<style>")
2234	buf.Write(css.Bytes())
2235	buf.WriteString("</style>")
2236
2237	if err := formatter.Format(&buf, style, iterator); err != nil {
2238		return err
2239	}
2240
2241	_, err = c.Status(fiber.StatusOK).Write(buf.Bytes())
2242	return err
2243}
2244
2245
2246// ===== tmpl.go =====
2247// Package main tmpl.go
2248package main
2249
2250import (
2251	"bytes"
2252	"fmt"
2253	htmpl "html/template"
2254	"log"
2255	"os"
2256	"path/filepath"
2257	"sort"
2258	"strconv"
2259	"strings"
2260	ttmpl "text/template"
2261	"time"
2262
2263	p "github.com/0magnet/m2/pkg/product"
2264	"github.com/gofiber/fiber/v3"
2265)
2266
2267/*
2268//go:embed htmpl/*
2269var templatesFS embed.FS
2270
2271//go:embed content/*
2272var contentFS embed.FS
2273*/
2274/*
2275var (
2276	templatesFS = os.DirFS("htmpl")
2277	contentFS   = os.DirFS("content")
2278)
2279*/
2280/*
2281func mustReadEmbeddedFileToString(path string, fs embed.FS) string {
2282	return string(mustReadEmbeddedFileToBytes(path, fs))
2283}
2284
2285func mustReadEmbeddedFileToBytes(path string, fs embed.FS) []byte {
2286	data, err := fs.ReadFile(path)
2287	if err != nil {
2288		panic(err)
2289	}
2290	return data
2291}
2292*/
2293
2294func mustReadFileToString(path string) string {
2295	return string(mustReadFileToBytes(path))
2296}
2297
2298func mustReadFileToBytes(path string) []byte {
2299	data, err := os.ReadFile(path) //nolint
2300	if err != nil {
2301		panic(err)
2302	}
2303	return data
2304}
2305
2306type htmlTemplate struct {
2307	Empty           func() string
2308	Head           func() string
2309	Logo           func() string
2310	Header         func() string
2311	Categories     func() string
2312	CatSubcats     func() string
2313	Footer         func() string
2314	MainPage       func() string
2315	AuxPage        func() string
2316	FrontPage      func() string
2317	CategoryPage   func() string
2318	CategoryPageMD func() string
2319	ProductPage    func() string
2320	ProductPageMD  func() string
2321	Schema         func() string
2322	Cart           func() string
2323	XMLSitemap     func() string
2324	Wasm           func() string
2325	Clock          func() string
2326	AboutPage      func() string
2327	PolicyPage     func() string
2328	LinksPage      func() string
2329	CheckoutPage   func() string
2330	CompletePage   func() string
2331	CheckoutCSS    func() string
2332	StyleCSS       func() string
2333	COVIDPage      func() string
2334}
2335
2336var h = htmlTemplate{
2337	Empty:           func() string { return mustReadFileToString("htmpl/empty.html") },
2338	Head:           func() string { return mustReadFileToString("htmpl/head.html") },
2339	Logo:           func() string { return mustReadFileToString("htmpl/logo.html") },
2340	Header:         func() string { return mustReadFileToString("htmpl/header.html") },
2341	Categories:     func() string { return mustReadFileToString("htmpl/categories.html") },
2342	CatSubcats:     func() string { return mustReadFileToString("htmpl/catsubcats.html") },
2343	Footer:         func() string { return mustReadFileToString("htmpl/footer.html") },
2344	MainPage:       func() string { return mustReadFileToString("htmpl/main.html") },
2345	AuxPage:        func() string { return mustReadFileToString("htmpl/aux.html") },
2346	FrontPage:      func() string { return mustReadFileToString("htmpl/front.html") },
2347	CategoryPage:   func() string { return mustReadFileToString("htmpl/category.html") },
2348	CategoryPageMD: func() string { return mustReadFileToString("htmpl/category.md") },
2349	ProductPage:    func() string { return mustReadFileToString("htmpl/product.html") },
2350	ProductPageMD:  func() string { return mustReadFileToString("htmpl/product.md") },
2351	Schema:         func() string { return mustReadFileToString("htmpl/schema.html") },
2352	Cart:           func() string { return mustReadFileToString("htmpl/cart.html") },
2353	XMLSitemap:     func() string { return mustReadFileToString("htmpl/sitemap.xml") },
2354	Wasm:           func() string { return mustReadFileToString("htmpl/wasm.html") },
2355	CompletePage:   func() string { return mustReadFileToString("htmpl/complete.html") },
2356	Clock:          func() string { return mustReadFileToString("content/clock.html") },
2357	AboutPage:      func() string { return mustReadFileToString("content/about.html") },
2358	PolicyPage:     func() string { return mustReadFileToString("content/policy.html") },
2359	LinksPage:      func() string { return mustReadFileToString("content/links.html") },
2360	CheckoutPage:   func() string { return mustReadFileToString("content/checkout.html") },
2361	CheckoutCSS:    func() string { return mustReadFileToString("content/checkout.css") },
2362	StyleCSS:       func() string { return mustReadFileToString("content/style.css") },
2363	COVIDPage:      func() string { return mustReadFileToString("content/mementomori.html") },
2364}
2365
2366var htmlPageTemplateData htmlTemplateData
2367
2368var funcs = htmpl.FuncMap{
2369	"replace": replace, "mul": mul, "div": div, "safeHTML": safeHTML,
2370	"safeJS": safeJS, "stripProtocol": stripProtocol, "add": add, "sub": sub,
2371	"toFloat": toFloat, "equalsIgnoreCase": equalsIgnoreCase,
2372	"getsubcats": getsubcats, "escapesubcat": escapesubcat,
2373	"sortsubcats": sortsubcats, "repeat": repeat, "subcatlink": subcatlink,
2374}
2375
2376func mainTmpl() (tmpl *htmpl.Template, err error) {
2377	tmpl = htmpl.New("index").Funcs(funcs)
2378	if _, err := tmpl.Parse(h.MainPage()); err != nil {
2379		log.Println("Error parsing index template:", err)
2380		return tmpl, err
2381	}
2382
2383	partials := []struct {
2384		Name    string
2385		Content string
2386	}{
2387		{"head", h.Head()},
2388		{"schema", h.Schema()},
2389		{"header", h.Header()},
2390		{"catsubcats", h.CatSubcats()},
2391		{"categories", h.Categories()},
2392		{"footer", h.Footer()},
2393		{"cart", h.Cart()},
2394		{"wasm", h.Wasm()},
2395	}
2396
2397	for _, p := range partials {
2398		if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2399			log.Printf("Error parsing %s template: %v", p.Name, err)
2400			return tmpl, err
2401		}
2402	}
2403	return tmpl, err
2404}
2405
2406func auxTmpl() (tmpl *htmpl.Template, err error) {
2407	tmpl = htmpl.New("index").Funcs(funcs)
2408	if _, err := tmpl.Parse(h.AuxPage()); err != nil {
2409		log.Println("Error parsing index template:", err)
2410		return tmpl, err
2411	}
2412
2413	partials := []struct {
2414		Name    string
2415		Content string
2416	}{
2417		{"head", h.Head()},
2418		{"schema", h.Empty()},
2419		{"wasm", h.Empty()},
2420	}
2421
2422	for _, p := range partials {
2423		if _, err := tmpl.New(p.Name).Parse(p.Content); err != nil {
2424			log.Printf("Error parsing %s template: %v", p.Name, err)
2425			return tmpl, err
2426		}
2427	}
2428	return tmpl, err
2429}
2430
2431func pageMeta(c fiber.Ctx, base htmlTemplateData) htmlTemplateData {
2432	h := base
2433	host := string(c.Request().Host())
2434	/*
2435		proto := "http"
2436		if c.Secure() {
2437			proto += "s"
2438		}
2439	*/
2440	proto := "https"
2441	h.Canonical = proto + "://" + host + c.OriginalURL()
2442	h.BaseURL = proto + "://" + host
2443	h.RequestHost = host
2444	h.Protocol = proto
2445	h.CatsCounts, h.Cats, h.SubCatsCounts, h.SubCatsByCat = getcategories(allproducts)
2446	h.LenAllProducts = len(allproducts)
2447	h.Time = time.Now().Format(time.RFC3339Nano)
2448	h.Year = fmt.Sprintf("%v", time.Now().Year())
2449	h.MetaDesc = f.Sitemeta
2450	h.KeyWords = strings.Replace(f.Sitelongname, " ", ", ", -1)
2451	return h
2452}
2453
2454func initTMPL() {
2455	htmlPageTemplateData = htmlTemplateData{
2456		NoCore:             f.NoCore,
2457		TestMode:           f.Teststripekey,
2458		Title:              f.Sitelongname,
2459		StripePK:           f.StripePK,
2460		SiteName:           f.Sitedomain,
2461		SiteTagLine:        f.Sitetagline,
2462		SiteName1:          htmpl.HTML(checkerBoard(f.Sitedomain)), //nolint
2463		SiteLongName:       f.Sitelongname,
2464		SiteASCIILogo:      htmpl.HTML(f.SiteASCIILogo), //nolint
2465		SitePrettyName:     f.Siteprettyname,
2466		SitePrettyNameCap:  f.Siteprettynamecap,
2467		SitePrettyNameCaps: f.Siteprettynamecaps,
2468		TelegramContact:    f.Tgcontact,
2469		TelegramChannel:    f.Tgchannel,
2470		WasmExecPath:       f.WasmExecPath,
2471		WasmExecRel:        f.WasmExecPath,
2472		Cats:               getcats(),
2473		LenAllProducts:     len(allproducts),
2474		ImgSRC: func() (ret string) {
2475			ret = f.Siteimagesrc
2476			if ret == "" {
2477				ret = "/i"
2478			}
2479			return ret
2480		}(),
2481		Page: "front",
2482		Time: time.Now().Format(time.RFC3339Nano),
2483		Year: fmt.Sprintf("%v", time.Now().Year()),
2484	}
2485	htmlPageTemplateData.CatsCounts, htmlPageTemplateData.Cats, htmlPageTemplateData.SubCatsCounts, htmlPageTemplateData.SubCatsByCat = getcategories(allproducts)
2486	htmlPageTemplateData.WasmBinary = wasmBinary()
2487
2488}
2489
2490func wasmBinary() (ret []string) {
2491	if len(f.WasmSRC) == 0 {
2492		return ret
2493	}
2494	if f.UseTinygo {
2495		for _, wasmSRC := range f.WasmSRC {
2496			outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2497			ret = append(ret, outputFile)
2498		}
2499		return ret
2500	}
2501	for _, wasmSRC := range f.WasmSRC {
2502		outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2503		ret = append(ret, outputFile)
2504	}
2505	return ret
2506}
2507
2508type xmlTemplateData struct {
2509	Cats         []string
2510	SubCatsByCat map[string][]string
2511	Products     p.Products
2512	Update       string
2513}
2514
2515func generateSitemapXML() string {
2516	xmlSitemapTemplateData := xmlTemplateData{
2517		Products: allproducts,
2518		Update:   time.Now().Format("2006-01-02"),
2519	}
2520	_, xmlSitemapTemplateData.Cats, _, xmlSitemapTemplateData.SubCatsByCat = getcategories(allproducts)
2521	var err1 error
2522	xtmpl, err1 := ttmpl.New("index").Funcs(ttmpl.FuncMap{"getsubcats": getsubcats}).Parse(h.XMLSitemap())
2523	if err1 != nil {
2524		log.Println("Error parsing index template:", err1)
2525	}
2526	var result bytes.Buffer
2527	err1 = xtmpl.Execute(&result, xmlSitemapTemplateData)
2528	if err1 != nil {
2529		log.Println("error: ", err1)
2530	}
2531	return result.String()
2532}
2533
2534func toFloat(s string) float64 {
2535	if s == "" {
2536		return 0.0
2537	}
2538	f, err := strconv.ParseFloat(s, 64)
2539	if err != nil {
2540		return 0.0
2541	}
2542	return f
2543}
2544
2545func checkerBoard(input string) string {
2546	var result strings.Builder
2547	for i, char := range input {
2548		// Wrap every other letter with the specified HTML
2549		if i%2 == 0 {
2550			result.WriteString(fmt.Sprintf("<span class='nv'>%c</span>", char))
2551		} else {
2552			result.WriteRune(char)
2553		}
2554	}
2555	return result.String()
2556}
2557
2558type htmlTemplateData struct {
2559	Title              string
2560	MetaDesc           string
2561	Canonical          string
2562	BaseURL            string
2563	ImgSRC             string // url where images are hosted
2564	OrdersURL          string // url where checkout is served from
2565	SiteName           string
2566	SiteTagLine        string
2567	SiteName1          htmpl.HTML //checkerboard - alternate swap text & bg color
2568	SiteLongName       string
2569	SitePrettyName     string //π•„π•’π•˜π•Ÿπ•–π•₯𝕠𝕀𝕑𝕙𝕖𝕣𝕖.π•Ÿπ•–π•₯
2570	SitePrettyNameCap  string //π•„π•’π•˜π•Ÿπ•–π•₯𝕠𝕀𝕑𝕙𝕖𝕣𝕖.π•Ÿπ•–π•₯
2571	SitePrettyNameCaps string //π•„π”Έπ”Ύβ„•π”Όπ•‹π•†π•Šβ„™β„π”Όβ„π”Ό.ℕ𝔼𝕋
2572	SiteASCIILogo      htmpl.HTML
2573	TelegramContact    string
2574	TelegramChannel    string
2575	Protocol           string
2576	RequestHost        string
2577	KeyWords           string
2578	Style              htmpl.HTML
2579	Heading            htmpl.HTML
2580	StripePK           string
2581	Cats               []string
2582	CatsCounts         map[string]int
2583	SubCatsCounts      map[string]map[string]int
2584	SubCatsByCat       map[string][]string
2585	LenAllProducts     int
2586	Mobile             bool
2587	Gocanvas           htmpl.HTML
2588	WasmBinary         []string
2589	WasmExecPath       string
2590	WasmExecRel        string
2591	StyleFontFace      htmpl.CSS
2592	Message            htmpl.HTML
2593	Page               string
2594	Year               string
2595	Time               string
2596	AboutHTML          htmpl.HTML
2597	LinksHTML          htmpl.HTML
2598	PolicyHTML         htmpl.HTML
2599	TestMode           bool
2600	NoCore             bool
2601}
2602
2603func equalsIgnoreCase(a, b string) bool {
2604	return strings.EqualFold(strings.Join(strings.Fields(a), ""), strings.Join(strings.Fields(b), ""))
2605}
2606
2607func replace(s, o, n string) string {
2608	return strings.ReplaceAll(s, o, n)
2609}
2610func mul(a, b float64) float64 {
2611	return a * b
2612}
2613func div(a, b float64) float64 {
2614	return a / b
2615}
2616func add(a, b int) int {
2617	return a + b
2618}
2619func sub(a, b int) int {
2620	return a - b
2621}
2622func safeHTML(s string) htmpl.HTML {
2623	return htmpl.HTML(s) //nolint
2624}
2625func safeJS(s string) htmpl.JS {
2626	return htmpl.JS(s) //nolint
2627}
2628func stripProtocol(s string) string {
2629	return strings.Replace(strings.Replace(s, "https://", "", -1), "http://", "", -1)
2630}
2631func repeat(s string, count int) string {
2632	var result string
2633	for i := 0; i < count; i++ {
2634		result += s
2635	}
2636	return result
2637}
2638func sortsubcats(subcats []string, counts map[string]map[string]int) []string {
2639	sort.Slice(subcats, func(i, j int) bool {
2640		catI, catJ := subcats[i], subcats[j]
2641		countI, countJ := counts[catI]["count"], counts[catJ]["count"]
2642		return countI > countJ
2643	})
2644	return subcats
2645}
2646
2647func subcatlink(subcategory string) string {
2648	s := subcategory
2649	s = strings.ReplaceAll(s, "ΒΌ", "quarter-")
2650	s = strings.ReplaceAll(s, "Β½", "half-")
2651	s = strings.ReplaceAll(s, "1/16", "sixteenth-")
2652	s = strings.ReplaceAll(s, "%", "-pct")
2653	s = strings.ReplaceAll(s, "  ", " ")
2654	s = strings.ReplaceAll(s, "watt1", "watt-1")
2655	s = strings.ReplaceAll(s, "watt5", "watt-5")
2656	s = strings.ReplaceAll(s, " ", "-")
2657	s = strings.ReplaceAll(s, "--", "-")
2658	return s
2659}
2660
2661
2662// ===== wasm.go =====
2663// Package main wasm.go
2664package main
2665
2666import (
2667	"fmt"
2668	"log"
2669	"path/filepath"
2670	"strings"
2671	"time"
2672
2673	"github.com/bitfield/script"
2674	"github.com/briandowns/spinner"
2675)
2676
2677func compileWASM() {
2678	s := spinner.New(spinner.CharSets[14], 25*time.Millisecond)
2679	s.Suffix = " Compiling wasm..."
2680	for _, wasmSRC := range f.WasmSRC {
2681		ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2682		outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + ".wasm"
2683		compilecmd := fmt.Sprintf("bash -c 'cd %s || exit 1 ; time GOOS=js GOARCH=wasm %s -o %s %s -ldflags=\"-s -w\" %s && cd %s && du %s'", wasmSRC, f.Gobuild, ascend+outputFile, ldflags(wasmSRC), ".", ascend, outputFile)
2684		log.Println("Compiling wasm with:")
2685		log.Println(compilecmd)
2686		s.Start()
2687		_, err := script.Exec(compilecmd).Stdout()
2688		if err != nil {
2689			log.Fatal(err)
2690		}
2691		s.Stop()
2692		log.Println("Compiled wasm!")
2693	}
2694	for _, wasmSRC := range f.WasmSRC {
2695		ascend := strings.Repeat("../", len(strings.Split(wasmSRC, "/")))
2696		outputFile := strings.TrimSuffix(filepath.Base(wasmSRC), filepath.Ext(wasmSRC)) + "-tiny.wasm"
2697		compilecmd := fmt.Sprintf("bash -c 'cd %s || exit 1 ; time GOOS=js GOARCH=wasm %s -o %s %s %s && cd %s && du %s'", wasmSRC, f.Tinygobuild, ascend+outputFile, ldflags(wasmSRC), ".", ascend, outputFile)
2698		log.Println("compiling wasm with:")
2699		log.Println(compilecmd)
2700		s.Start()
2701		_, err := script.Exec(compilecmd).Stdout()
2702		if err != nil {
2703			log.Fatal(err)
2704		}
2705		s.Stop()
2706		log.Println("Compiled wasm!")
2707	}
2708}
2709
2710func ldflags(s string) (ss string) {
2711	checkFiles, err := script.FindFiles(s).Slice()
2712	if err != nil {
2713		log.Fatal(err)
2714	}
2715	if f.LDFlagsX != "" {
2716		for _, s := range checkFiles {
2717			res, err := script.File(s).Match(strings.Split(f.LDFlagsX, "=")[0]).String()
2718			if err != nil {
2719				log.Fatal(err)
2720			}
2721			if res != "" {
2722				ss += fmt.Sprintf(` -X 'main.%s' `, f.LDFlagsX)
2723				break
2724			}
2725		}
2726	}
2727	for _, s := range checkFiles {
2728		res, err := script.File(s).Match("wasmName").String()
2729		if err != nil {
2730			log.Fatal(err)
2731		}
2732		if res != "" {
2733			ss += fmt.Sprintf(` -X 'main.wasmName=%s' `, strings.TrimSuffix(filepath.Base(s), filepath.Ext(s))+".wasm")
2734			break
2735		}
2736	}
2737	if ss != "" {
2738		ss = `-ldflags="` + ss + `"`
2739	}
2740	return ss
2741}
2742
2743