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