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