From 00f2092a1794e32bd84e008a9b796c40ad67438f Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Fri, 27 Mar 2026 09:19:24 -0500 Subject: [PATCH 1/8] Send theme to frontend by uuid --- internal/server.go | 5 +- internal/user_db.go | 28 ++++- internal/utils.go | 20 +++- run/themes/light.css | 262 ++++++++++++++++++++++++++++++++++--------- 4 files changed, 258 insertions(+), 57 deletions(-) diff --git a/internal/server.go b/internal/server.go index 99c948c..8dbc46e 100644 --- a/internal/server.go +++ b/internal/server.go @@ -467,7 +467,10 @@ func serveTheme(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Cache-Control", "private, max-age=0, must-revalidate") writer.Header().Set("Content-Type", "text/css; charset=utf-8") - theme := "light" // getThemeFromCookies(auth.UUID) not implemented + theme := getTheme(auth.UUID) + if theme == "" { + theme = "light" + } http.ServeFile(writer, request, "run/themes/"+theme+".css") } diff --git a/internal/user_db.go b/internal/user_db.go index cb30579..3b76885 100644 --- a/internal/user_db.go +++ b/internal/user_db.go @@ -163,6 +163,7 @@ type UserInfo struct { HighScore int // The high score Color LBColor // The leaderboard color Pfp string // The relative path to the profile picture + Theme string // The theme the user is using } // User information to be served for admins to edit @@ -457,12 +458,33 @@ func getPfp(uuid string) string { } // Sets a given user's path to profile picture -func SetPfp(username string, pfp string) { - uuid, _ := GetUUID(username, true) - +func SetPfp(uuid string, pfp string) { _, execErr := userDB.Exec("update users set pfp = ? where uuid = ?", pfp, uuid) if execErr != nil { LogErrorf(execErr, "Problem executing sql query UPDATE users SET pfp = ? WHERE uuid = ? with args: %v, %v", pfp, uuid) } } + +// Gets the relative path of a given user's profile picture +func getTheme(uuid string) string { + var theme string + response := userDB.QueryRow("select theme from users where uuid = ?", uuid) + scanErr := response.Scan(&theme) + if scanErr != nil { + // LogError(scanErr, "Problem scanning response to sql query SELECT theme FROM users WHERE uuid = ? with arg: "+uuid) + SetTheme(uuid, "light") + return "light" + } + + return theme +} + +// Gets the relative path of a given user's profile picture +func SetTheme(uuid string, themeName string) { + _, execErr := userDB.Exec("update users set theme = ? where uuid = ?", themeName, uuid) + + if execErr != nil { + LogErrorf(execErr, "Problem executing sql query UPDATE users SET theme = ? WHERE uuid = ? with args: %v, %v", themeName, uuid) + } +} diff --git a/internal/utils.go b/internal/utils.go index b1fe1e3..80d9905 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -14,6 +14,24 @@ import ( "strings" ) +func ListAllThemes() ([]string, error) { + files, err := os.ReadDir("run/themes") + if err != nil { + return nil, err + } + var cleanNames []string + for _, file := range files { + if file.IsDir() { + continue + } + fileName := file.Name() + if strings.HasSuffix(fileName, ".css") { + cleanNames = append(cleanNames, strings.TrimSuffix(fileName, ".css")) + } + } + return cleanNames, nil +} + // Simple wrapper for converting bool to string for replays func GetReplayString(isReplay bool) string { if isReplay { @@ -380,7 +398,7 @@ func GetAllMatching(checkAgainst string) []string { for _, jsonFile := range writtenJson { splitFile := strings.Split(jsonFile.Name(), "_") - if len(splitFile) < 4 { + if len(splitFile) < 3 || len(splitAgainst) < 3 { continue } diff --git a/run/themes/light.css b/run/themes/light.css index 1a141b6..9baf187 100644 --- a/run/themes/light.css +++ b/run/themes/light.css @@ -1,84 +1,242 @@ :root { /* Color Variables */ - /*--background-color: #f8f9fe; - --accent-color: #9bcbfb; - --accent-color2: #8fbdeb; - --accent-color3: #cee5ff; - --error-color: lightcoral; - --disabled-color: #a1a2a6; - --font-color: #1e2226; - --sidenav-hover-color: #444; - --faded-text-color: #5f6167; - --kelly-green: #4cbb17; /* Kelly Green is the color of Edina's "The Green Machine" - --error-message-color: #d32f2f; - --check-background-color: #2f628d; - --checkbox-default-color: #eee; - --checkbox-hover-color: #ccc; - --border-color-gray: #93979f; - --border-color-blue: #417096; - --sidenav-button-color: #dfdfdf;*/ + --background-color: #f8f9fe !important; + --accent-color: #9bcbfb !important; + --accent-color2: #8fbdeb !important; + --accent-color3: #cee5ff !important; + --error-color: lightcoral !important; + --disabled-color: #a1a2a6 !important; + --font-color: #1e2226 !important; + --sidenav-hover-color: #444 !important; + --faded-text-color: #5f6167 !important; + --kelly-green: #4cbb17 !important; /* Kelly Green is the color of Edina's "The Green Machine" */ + --error-message-color: #d32f2f !important; + --check-background-color: #2f628d !important; + --checkbox-default-color: #eee !important; + --checkbox-hover-color: #ccc !important; + --border-color-gray: #93979f !important; + --border-color-blue: #417096 !important; + --sidenav-button-color: #dfdfdf !important; /* General Sizing Variables */ - --background-color: #ff0000 !important; - --accent-color: #ff1a1a !important; - --accent-color2: #ff3333 !important; - --accent-color3: #ff6666 !important; - --error-color: #ff0000 !important; - --disabled-color: #ff8080 !important; - --font-color: #7a0000 !important; - --sidenav-hover-color: #b30000 !important; - --faded-text-color: #cc0000 !important; - --kelly-green: #ff0000 !important; /* Kelly Green is the color of Edina's "The Green Machine" */ - --error-message-color: #ff0000 !important; - --check-background-color: #990000 !important; - --checkbox-default-color: #ffcccc !important; - --checkbox-hover-color: #ffb3b3 !important; - --border-color-gray: #cc0000 !important; - --border-color-blue: #ff0000 !important; - --sidenav-button-color: #ff9999 !important; - --font-size-large: 3vh; - --font-size-small: 2.5vh; - --page-height: 100vh; - --page-width: 100vw; - --content-height: calc(var(--page-height) - var(--navbar-height)); - --content-width: var(--page-width); + --border-radius: 1em !important; + --font-size-large: 3vh !important; + --font-size-small: 2.5vh !important; + --font-weight: 1 !important; + --page-height: 100vh !important; + --page-width: 100vw !important; + --content-height: calc(var(--page-height) - var(--navbar-height)) !important; + --content-width: var(--page-width) !important; + --element-height: 6.2vh !important; /* Not used universally, but use a lot */ + /* Navbar Sizing Variables */ - --sidenav-width: 70vw; - --navbar-height: 8vh; - --sidenav-usertile-height: 0%; /* Set to 0 while user tile is diabled */ - --sidenav-usertile-width: 15vh; - --sidenav-section-height: calc((100% - var(--sidenav-usertile-height)) / 2); + --sidenav-width: 70vw !important; + --navbar-height: 8vh !important; + --sidenav-usertile-height: 0% !important; /* Set to 0 while user tile is diabled */ + --sidenav-usertile-width: 15vh !important; + --sidenav-section-height: calc((100% - var(--sidenav-usertile-height)) / 2) !important; + --navbar-hamburger-padding: 15px !important; + --navbar-hamburger-top-distance: -6px !important; + --navbar-text-padding-top: 5px !important; + --sidenav-usertile-margin-bottom: 3vw !important; + --sidenav-userimage-size: 3vw !important; + --sidenav-usertext-h1-margin-top: 1.7vh !important; + --sidenav-usertext-p-margin-top: -4.3vh !important; + --sidenav-usertext-p-margin-left: 0.2vw !important; + --sidenav-padding-top: 60px !important; /* Login Sizing Variables */ + --login-h1-height: 3vh !important; + --login-h1-margin-top: 4vh !important; + --login-error-message-margin: 10px 0 !important; + + --login-box-width: 30vh !important; + --login-box-margin-top: 5vh !important; + --login-box-margin-bottom: 1.5vh !important; + --login-box-padding-left: calc(var(--login-box-background-size) + 1vh) !important; + --login-box-background-size: 3vh !important; + --login-box-background-position: 5px 1.2vh !important; + + --login-button-width: 30vh !important; + --login-button-height: 6vh !important; /* Match Form Sizing Variables */ + --matchform-text-padding-top: 5px !important; + --matchform-text-height: 30px !important; + + --matchform-side-panel-width: 8vh !important; + --matchform-grid-template: 1fr var(--matchform-side-panel-width) !important; + + --matchform-child-margin-side: 7.5vw !important; /* Used Throughout Child Components */ + --matchform-child-width-to-subtract: 15vw !important; /* Used Throughout Child Components */ + + --matchform-element-height: 6.2vh !important; /* Replace throughout with element-height */ + --matchform-matchteam-margin-top: 1.5vh !important; + + --matchform-header-padding-top: 5px !important; + --matchform-header-end-margin-top: 8vh !important; + + --matchform-notes-height: 20vh !important; + + --matchform-cycle-container-height: 40vh !important; + --matchform-cycle-item-height: 9vh !important; + --matchform-cycle-item-margin-bottom: 1vw !important; + --matchform-cycle-item-image-height: 4vh !important; + --matchform-cycle-item-image-width: 4vh !important; + --matchform-cycle-delete-height: 30% !important; + --matchform-cycle-delete-width: 10% !important; + + --matchform-cycle-text-padding-top: 5% !important; + --matchform-cycle-text-padding-left: 5% !important; + --matchform-cycle-text-width: 25 !important; + + --matchform-acclabel-width: 10vh !important; + --matchform-acclabel-height: 3vh !important; + --matchform-accslider-width: 70% !important; + --matchform-accslider-height: 2vh !important; + --matchform-accslider-margin-right: 10% !important; + --matchform-accslider-thumb-size: 50px !important; + + --matchform-bottomspace-height: 15vh !important; + --matchform-bottomspace-max-height: 80px !important; /* Match Form Checkbox Sizing Variables */ + --check-text-height: 30px !important; + --check-padding-top: 1vh !important; + + --check-div-margin: 5vh !important; + + --check-size: 2.5vh !important; + + --check-container-padding-left: 35px !important; + --check-container-margin-bottom: 12px !important; + + --check-mark-left: 1vw !important; + + --check-mark-after-left: 9px !important; + --check-mark-after-top: 5px !important; + --check-mark-after-size: 1.2vh !important; + + --check-special-margin-left: 7.5vw !important; + + --check-auto-text-padding-top: 1vh !important; + --check-auto-text-width: 80vw !important; /* Match Form Counter Sizing Variables */ + --counter-text-padding-top: 5px !important; + --counter-text-height: 30px !important; + --counter-text-margin-bottom: 5vh !important; + + --counter-child-margin-vertical: 3vh !important; + + --counter-button-height: 5.2vh !important; + --counter-add-width: 40% !important; + --counter-subtract-width: 30% !important; + --counter-button-margin-left: 1vw !important; + + /* Match Form Slider Sizing Variables */ + --slider-container-width: 70% !important; + --slider-container-margin-bottom: 10% !important; + + /* Match Form Collapsible Dropdown Sizing Variables */ + --collapsible-open-max-height: 100% !important; /* 2000px */ + --collapsible-padding: 12px 16px !important; /* Match Form Dropdown Sizing Variables */ + --dropdown-margin-top: 3vh !important; + --dropdown-padding-bottom: 0.5vh !important; + --dropdown-height: 5.2vh !important; + --dropdown-width: 15vh !important; + + --dropdown-text-padding-top: 5px !important; + --dropdown-text-height: 30px !important; + + --dropdown-driverstation-margin-top: 1vh !important; + + --dropdown-dtext-height: 3.1vh !important; + --dropdown-dtext-width: 70vw !important; + + --dropdown-collect-container-margin-top: 3% !important; + + --dropdown-dtext-collect-width: 70vw !important; + + --dropdown-collect-margin-top: 16% !important; + --dropdown-collect-padding-bottom: 0.5vh !important; + + --dropdown-park-text-padding-top: 4% !important; /* Match Form Trigger Sizing Variables */ + --trigger-margin-top: 5.7vh !important; + --trigger-margin-bottom: 4.4vh !important; + --trigger-size: 6.5vh !important; + + --trigger-stop-image-height: 2vh !important; + --trigger-stop-image-background-size: 1.7vh 1.7vh !important; + --trigger-stop-image-background-position: 2vh 0vh !important; + + --trigger-reset-height: 8vh !important; /* Match Form Submit/Replay Sizing Variables */ + --submitreplay-div-height: 8vh !important; + --submitreplay-div-margin-bottom: 1vh !important; + + --submitreplay-button-margin-top: 5.7vh !important; + --submitreplay-button-margin-bottom: 4.4vh !important; + + --submitreplay-text-margin-top: -1vh !important; + + --submitreplay-label-margin-top: 2vh !important; + + --submit-image-margin-top: -3.5vh !important; + --submit-image-size: 4.8vh !important; /* General Cycles Sizing Variables */ - --font-size-cycle: 1.8vh; + --font-size-cycle: 1.8vh !important; + /* Cycle Start/Stop Button Sizing Variables */ + --cycle-margin-top: 5.7vh !important; + --cycle-margin-bottom: 4.4vh !important; + --cycle-size: 6.5vh !important; + --cycle-top: 4vh !important; + + --cycle-image-height: 2vh !important; + --cycle-image-background-size: 1.7vh 1.7vh !important; + --cycle-image-background-position: 2vh 0vh !important; /* Score Cycle Sizing Variables */ + --score-top: 13vh !important; + --score-image-height: 3vh !important; /* Shuttle Cycle Sizing Variables */ + --shuttle-top: 22vh !important; + + /* Hub Switch Cycle Sizing Variables */ + --hubSwitch-top: 31vh !important; /* Home Page Sizing Variables */ - --refresh-button-size: 7.9vh; - --new-matchpit-button-height: 8vh; + --new-matchpit-button-height: 8vh !important; + --new-matchpit-button-width: 50vw !important; + + --home-text-padding-top: 5px !important; + --home-assigned-h-padding-top: 10px !important; + --home-refresh-container-bottom: -14px !important; + --home-refresh-container-right: 17px !important; + --home-refresh-button-margin-top: 5.7vh !important; + --home-refresh-button-margin-bottom: 4.4vh !important; + --refresh-button-size: 7.9vh !important; + /* Sizing Variables */ /* Leader Board Sizing Variables */ - --leader-board-element-height: 5vh; + --leader-board-element-height: 5vh !important; /* Settings Sizing Variables */ - --settings-element-width: 80%; - --settings-element-height: calc(var(--font-size-large) * 2); - --settings-element-spacing: calc(var(--font-size-small) / 2); + --settings-element-width: 80% !important; + --settings-element-height: calc(var(--font-size-large) * 2) !important; + --settings-element-spacing: calc(var(--font-size-small) / 2) !important; + --settings-debug-image-offset: 2vh !important; + + --settings-themedrop-padding-top: 5px !important; + --settings-themedrop-height: 30px !important; + --settings-themedrop-text-width: 70vw !important; + --settings-themedrop-margin-top: 10% !important; + --settings-themedrop-padding-bottom: 0.5vh !important; } From e6e9de1400edb01985f98e15eca51d0ee4f2db65 Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:05:26 -0500 Subject: [PATCH 2/8] Create and modify user_db by schema --- internal/user_db.go | 366 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 360 insertions(+), 6 deletions(-) diff --git a/internal/user_db.go b/internal/user_db.go index 3b76885..0d623cd 100644 --- a/internal/user_db.go +++ b/internal/user_db.go @@ -5,8 +5,12 @@ package internal import ( "database/sql" "encoding/json" + "errors" + "fmt" + "os" "path/filepath" "slices" + "strings" "github.com/google/uuid" _ "github.com/mattn/go-sqlite3" @@ -15,16 +19,367 @@ import ( // The reference to users.db var userDB *sql.DB +type UserColumn struct { + columnName string // The name of the SQL column + valueType string // The type of the column (TEXT, INT, etc...) + defaultValue sql.NullString // The value that will be in row if not replaced + notNull bool // Prevents a column from being null + primaryKey bool // If the column should be used to look up a row. Also enforces uniqueness + unique bool // Each row has to have a different value in the column +} + // Initializes users.db and stores the reference to memory func InitUserDB() { dbPath := filepath.Join(CachedConfigs.PathToDatabases, "users.db") - dbRef, dbOpenErr := sql.Open(CachedConfigs.SqliteDriver, dbPath) + _, err := os.Stat(dbPath) + dbMissing := err != nil && errors.Is(err, os.ErrNotExist) + dbRef, err := sql.Open(CachedConfigs.SqliteDriver, dbPath) + if err != nil { + FatalError(err, "Problem opening database "+dbPath) + } userDB = dbRef - if dbOpenErr != nil { - FatalError(dbOpenErr, "Problem opening database "+dbPath) + // when changing this, please make sure it you modify NewUser() as new account creation may fail for whatever reason + schema := []UserColumn{ + {columnName: "uuid", valueType: "TEXT", unique: true, primaryKey: true}, + {columnName: "username", valueType: "TEXT", unique: true}, + {columnName: "displayname", valueType: "TEXT"}, + {columnName: "certificate", valueType: "TEXT"}, + {columnName: "badges", valueType: "TEXT[]"}, + {columnName: "score", valueType: "INT"}, + {columnName: "pfp", valueType: "TEXT", defaultValue: sql.NullString{String: "'" + DefaultPfpPath + "'", Valid: true}}, + {columnName: "lifescore", valueType: "INT"}, + {columnName: "highscore", valueType: "INT"}, + {columnName: "accolades", valueType: "TEXT", defaultValue: sql.NullString{String: "'[]'", Valid: true}}, + {columnName: "color", valueType: "INT"}, + {columnName: "theme", valueType: "TEXT", defaultValue: sql.NullString{String: "'light'", Valid: true}}, + } + + if dbMissing { + LogMessage("Creating new user database. Populating...") + err = populateNewDB(schema) + if err != nil { + FatalError(err, "Failed to update the database with new schema: ") + } + } else { + err = updateCurrentDB(schema) + if err != nil { + FatalError(err, "Failed to update the database with new schema: ") + } + } + +} + +// updateCurrentDB compares an existing SQLite table with the desired schema +// and migrates it to match. It adds missing columns when possible, and rebuilds +// the table when incompatible changes are detected +func updateCurrentDB(schema []UserColumn) error { + exists, err := tableExists() + if err != nil { + return err + } + if !exists { + return createTable(schema) + } + + current, err := readCurrentSchema() + if err != nil { + return err + } + + desiredMap := map[string]UserColumn{} + for _, column := range schema { + desiredMap[column.columnName] = column + } + + currentMap := map[string]UserColumn{} + for _, column := range current { + currentMap[column.columnName] = column + } + + needsRebuild := false + + // checks if theres an extra column in teh database (not mucho gracias) + for name := range currentMap { + if _, ok := desiredMap[name]; !ok { + needsRebuild = true + break + } + } + + // see if existing columns dont like changes in schema + if !needsRebuild { + for _, want := range schema { + got, ok := currentMap[want.columnName] + if !ok { + continue + } + + if !sameColumn(got, want) { + needsRebuild = true + break + } + } + } + + if needsRebuild { + return rebuildTable(schema, currentMap) + } + + for _, want := range schema { + if _, ok := currentMap[want.columnName]; ok { + continue + } + + if _, err := userDB.Exec("ALTER TABLE users ADD COLUMN " + buildColumnDef(want)); err != nil { + return fmt.Errorf("add column %s: %w", want.columnName, err) + } + } + + return nil +} + +func tableExists() (bool, error) { + var n int + err := userDB.QueryRow( + `SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?`, + "users", + ).Scan(&n) + + return n > 0, err +} + +func createTable(schema []UserColumn) error { + columnConstructors := make([]string, 0, len(schema)) + for _, column := range schema { + columnConstructors = append(columnConstructors, buildColumnDef(column)) + } + + _, err := userDB.Exec(fmt.Sprintf("CREATE TABLE users (%s)", strings.Join(columnConstructors, ", "))) + return err +} + +// gets a user column array from the loaded database +func readCurrentSchema() ([]UserColumn, error) { + rows, err := userDB.Query("PRAGMA table_info(users)") + if err != nil { + return nil, err + } + defer rows.Close() + + var cols []UserColumn + for rows.Next() { + var cid int + var name, typee string // more like bladee + var notNull, primaryKey int + var defaultValue sql.NullString + + if err := rows.Scan(&cid, &name, &typee, ¬Null, &defaultValue, &primaryKey); err != nil { + return nil, err + } + + cols = append(cols, UserColumn{ + columnName: name, + valueType: strings.ToUpper(strings.TrimSpace(typee)), + defaultValue: defaultValue, + notNull: notNull == 1, + primaryKey: primaryKey == 1, + unique: false, // filled below + }) + } + if err := rows.Err(); err != nil { + return nil, err + } + + uniqueColumns, err := readUniqueColumns() + if err != nil { + return nil, err + } + for i := range cols { + if uniqueColumns[cols[i].columnName] { + cols[i].unique = true + } + } + + return cols, nil +} + +func readUniqueColumns() (map[string]bool, error) { + result := map[string]bool{} + + idxRows, err := userDB.Query("PRAGMA index_list(users)") + if err != nil { + return nil, err + } + defer idxRows.Close() + + type idx struct { + name string + unique int + } + var idxs []idx + for idxRows.Next() { + var seq int + var name string + var unique int + var origin string + var partial int + if err := idxRows.Scan(&seq, &name, &unique, &origin, &partial); err != nil { + return nil, err + } + if unique == 1 { + idxs = append(idxs, idx{name: name, unique: unique}) + } + } + if err := idxRows.Err(); err != nil { + return nil, err + } + + for _, ix := range idxs { + infoRows, err := userDB.Query(fmt.Sprintf("PRAGMA index_info(%s)", friendlySqlStr(ix.name))) + if err != nil { + return nil, err + } + + var columns []string + for infoRows.Next() { + var seqno, cid int + var colName string + if err := infoRows.Scan(&seqno, &cid, &colName); err != nil { + infoRows.Close() + return nil, err + } + columns = append(columns, colName) + } + infoRows.Close() + + if len(columns) == 1 { + result[columns[0]] = true + } + } + + return result, nil +} + +// compares two UserColumns against each other to if theyre the same +func sameColumn(oldColumn UserColumn, newColumn UserColumn) bool { + if !strings.EqualFold(strings.TrimSpace(oldColumn.valueType), strings.TrimSpace(newColumn.valueType)) { + return false + } + if oldColumn.notNull != newColumn.notNull { + return false + } + if oldColumn.primaryKey != newColumn.primaryKey { + return false + } + if oldColumn.unique != newColumn.unique { + return false + } + + gotDefault := "" + if oldColumn.defaultValue.Valid { + gotDefault = normalizeDefault(oldColumn.defaultValue.String) + } + wantDef := normalizeDefault(newColumn.defaultValue.String) + return gotDefault == wantDef +} + +func rebuildTable(schema []UserColumn, currentSchema map[string]UserColumn) error { + tempName := "users__new" + + tx, err := userDB.Begin() + if err != nil { + return err + } + defer func() { + if err != nil { + _ = tx.Rollback() + } + }() + + columnConstructors := make([]string, 0, len(schema)) + for _, c := range schema { + columnConstructors = append(columnConstructors, buildColumnDef(c)) + } + if _, err = tx.Exec(fmt.Sprintf("CREATE TABLE %s (%s)", friendlySqlStr(tempName), strings.Join(columnConstructors, ", "))); err != nil { + return err + } + + // copies over the existing columns + var shared []string + for _, column := range schema { + if _, ok := currentSchema[column.columnName]; ok { + shared = append(shared, friendlySqlStr(column.columnName)) + } + } + if len(shared) > 0 { + columns := strings.Join(shared, ", ") + if _, err = tx.Exec(fmt.Sprintf("INSERT INTO %s (%s) SELECT %s FROM users", friendlySqlStr(tempName), columns, columns)); err != nil { + return err + } + } + + if _, err = tx.Exec("DROP TABLE users"); err != nil { + return err + } + if _, err = tx.Exec(fmt.Sprintf("ALTER TABLE %s RENAME TO users", friendlySqlStr(tempName))); err != nil { + return err + } + + for _, column := range schema { + if column.unique && !column.primaryKey { + idxName := fmt.Sprintf("idx_users_%s_unique", column.columnName) + stmt := fmt.Sprintf( + "CREATE UNIQUE INDEX IF NOT EXISTS %s ON users (%s)", + friendlySqlStr(idxName), friendlySqlStr(column.columnName), + ) + if _, err = tx.Exec(stmt); err != nil { + return err + } + } + } + + return tx.Commit() +} + +func buildColumnDef(column UserColumn) string { + parts := []string{friendlySqlStr(column.columnName), strings.ToUpper(strings.TrimSpace(column.valueType))} + if column.primaryKey { + parts = append(parts, "PRIMARY KEY") + } + if column.notNull { + parts = append(parts, "NOT NULL") + } + if column.defaultValue.String != "" { + parts = append(parts, "DEFAULT "+column.defaultValue.String) + } + if column.unique && !column.primaryKey { + parts = append(parts, "UNIQUE") + } + return strings.Join(parts, " ") +} + +// jarvis make sure my strings make SQL happy +func friendlySqlStr(input string) string { + return `"` + strings.ReplaceAll(input, `"`, `""`) + `"` +} + +func normalizeDefault(s string) string { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "(") + s = strings.TrimSuffix(s, ")") + return strings.TrimSpace(s) +} + +// creates a table from the schema into the users database +func populateNewDB(schema []UserColumn) error { + columnConstructors := make([]string, 0, len(schema)) + for _, column := range schema { + columnConstructors = append(columnConstructors, buildColumnDef(column)) } + _, err := userDB.Exec(fmt.Sprintf("CREATE TABLE users (%s)", strings.Join(columnConstructors, ", "))) + return err } // Creates a new user @@ -36,7 +391,7 @@ func NewUser(username string, uuid string) { } //The only reason most of these columns don't have default values is that sqlite doesn't let you alter column default values and I don't feel like deleting and remaking every column - _, err := userDB.Exec("insert into users values(?,?,?,?,?,?,?, 0, 0, ?, 0)", uuid, username, username, nil, string(badgeBytes), 0, DefaultPfpPath, "[]") + _, err := userDB.Exec("insert into users values(?,?,?,?,?,?,?, 0, 0, ?, 0, ?)", uuid, username, username, nil, string(badgeBytes), 0, DefaultPfpPath, "[]", "light") if err != nil { LogErrorf(err, "Problem creating new user with args: %v, %v, %v, %v, %v, %v, %v", uuid, username, username, "nil", badgeBytes, 0, DefaultPfpPath) @@ -55,7 +410,7 @@ func userExists(username string) bool { LogError(scanErr, "Problem scanning response to sql query SELECT COUNT(1) FROM users WHERE username = ? with arg: "+username) } - // If we ever get more than 1, something is horribly wrong.s + // If we ever get more than 1, something is horribly wrong. return resultstore == 1 } @@ -163,7 +518,6 @@ type UserInfo struct { HighScore int // The high score Color LBColor // The leaderboard color Pfp string // The relative path to the profile picture - Theme string // The theme the user is using } // User information to be served for admins to edit From 965c139da6efc55d19749368bd4873e8c5fc5437 Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:09:56 -0500 Subject: [PATCH 3/8] Handle theme endpoints --- internal/server.go | 96 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/internal/server.go b/internal/server.go index 8dbc46e..069a105 100644 --- a/internal/server.go +++ b/internal/server.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "strconv" "strings" "time" @@ -141,6 +142,7 @@ func SetupServer() *http.Server { http.HandleFunc("/getPfp", handleWithCORS(handlePfpRequest, true)) http.HandleFunc("/generalInfo", handleWithCORS(handleGeneralInfoRequest, true)) http.HandleFunc("/allEvents", handleWithCORS(handleEventsRequest, true)) + http.HandleFunc("/allThemes", handleWithCORS(handleThemesRequest, false)) http.HandleFunc("/gallery", handleWithCORS(handleGalleryRequest, true)) //Provides Authentication @@ -151,13 +153,14 @@ func SetupServer() *http.Server { http.HandleFunc("/dataEntry", handleWithCORS(postTeamData, true)) http.HandleFunc("/pitScout", handleWithCORS(postPitScout, true)) http.HandleFunc("/singleSchedule", handleWithCORS(serveScouterSchedule, true)) - http.HandleFunc("/theme", handleWithCORS(serveTheme, false)) + http.HandleFunc("/getTheme", handleWithCORS(serveTheme, false)) //Admin or curr user http.HandleFunc("/setDisplayName", handleWithCORS(setDisplayName, true)) http.HandleFunc("/setUserPfp", handleWithCORS(setPfp, true)) http.HandleFunc("/provideAdditions", handleWithCORS(handleFrontendAdditions, true)) http.HandleFunc("/setColor", handleWithCORS(handleColorChange, true)) + http.HandleFunc("/setTheme", handleWithCORS(setTheme, false)) //Admin or verified http.HandleFunc("/spreadsheet", handleWithCORS(serveSpreadsheet, true)) @@ -459,22 +462,107 @@ func serveScouterSchedule(writer http.ResponseWriter, request *http.Request) { httpResponsef(writer, "Problem serving scouter schedule", "%s", response) } +// Gives back a theme css file func serveTheme(writer http.ResponseWriter, request *http.Request) { auth := getAuthFromCookies(request) - _ = auth writer.Header().Set("Vary", "Cookie") - writer.Header().Set("Cache-Control", "private, max-age=0, must-revalidate") + writer.Header().Set("Cache-Control", "private, max-age=2, must-revalidate") writer.Header().Set("Content-Type", "text/css; charset=utf-8") + // Optionally if you want to grab a theme independent of the user + directTheme := request.Header.Get("theme") + theme := getTheme(auth.UUID) - if theme == "" { + + if directTheme != "" { + allThemes, err := ListAllThemes() + if err != nil { + LogError(err, "Failed to fetch all themes: ") + + writer.WriteHeader(500) + httpResponsef(writer, "Could not find specified theme", "An unexpected error occurred preventing validation of the theme name") + return + } + + // this prevents the client from deciding it wants just ANY file on our system (mucho bado) + if slices.Contains(allThemes, directTheme) { + theme = directTheme + } else { + writer.WriteHeader(404) + httpResponsef(writer, "Could not find specified theme", "Theme %s does not exist.", directTheme) + return + } + + } else if theme == "" { theme = "light" } + writer.WriteHeader(200) http.ServeFile(writer, request, "run/themes/"+theme+".css") } +func setTheme(writer http.ResponseWriter, request *http.Request) { + auth := getAuthFromCookies(request) + + if !auth.Authed { + writer.WriteHeader(401) + httpResponsef(writer, "Could not set specified theme", "Not authenticated :(") + return + } + + wantedTheme := request.Header.Get("theme") + + if wantedTheme != "" { + allThemes, err := ListAllThemes() + if err != nil { + LogError(err, "Failed to fetch all themes: ") + + writer.WriteHeader(500) + httpResponsef(writer, "Could not set specified theme", "An unexpected error occurred preventing validation of the theme name") + return + } + + // Do the checking early when we set the theme + if slices.Contains(allThemes, wantedTheme) { + SetTheme(auth.UUID, wantedTheme) + + writer.WriteHeader(200) + httpResponsef(writer, "Could set specified theme", "Successfully switched to \"%s\".", wantedTheme) + } else { + writer.WriteHeader(404) + httpResponsef(writer, "Could not set specified theme", "Theme \"%s\" does not exist.", wantedTheme) + return + } + } + +} + +func handleThemesRequest(writer http.ResponseWriter, request *http.Request) { + allThemes, err := ListAllThemes() + if err != nil { + LogError(err, "Failed to fetch all themes: ") + + writer.WriteHeader(500) + httpResponsef(writer, "Could not fetch all themes", "An unexpected error occurred preventing the reading of the theme list") + return + } + + data := struct { + Themes []string `json:"themes"` + }{ + Themes: allThemes, + } + + writer.Header().Add("Content-Type", "application/json") + encodeErr := json.NewEncoder(writer).Encode(data) + if encodeErr != nil { + LogErrorf(encodeErr, "Problem encoding %v", allThemes) + } else { + writer.WriteHeader(200) + } +} + // Handles adding schedules to a given scouter func addIndividualSchedule(writer http.ResponseWriter, request *http.Request) { auth := getAuthFromCookies(request) From 6561278a4e2f11567a475a07214ca034121bf14b Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:10:36 -0500 Subject: [PATCH 4/8] Appeal to CORS Preflight was messing things up --- internal/server.go | 93 +++++++++++++++++++++++++++++++++++++++------ internal/user_db.go | 4 +- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/internal/server.go b/internal/server.go index 069a105..a233bd1 100644 --- a/internal/server.go +++ b/internal/server.go @@ -160,7 +160,8 @@ func SetupServer() *http.Server { http.HandleFunc("/setUserPfp", handleWithCORS(setPfp, true)) http.HandleFunc("/provideAdditions", handleWithCORS(handleFrontendAdditions, true)) http.HandleFunc("/setColor", handleWithCORS(handleColorChange, true)) - http.HandleFunc("/setTheme", handleWithCORS(setTheme, false)) + http.HandleFunc("/setTheme", handleWithCORS(setTheme, true)) + http.HandleFunc("/currTheme", handleWithCORS(getTheme, false)) //Admin or verified http.HandleFunc("/spreadsheet", handleWithCORS(serveSpreadsheet, true)) @@ -462,10 +463,21 @@ func serveScouterSchedule(writer http.ResponseWriter, request *http.Request) { httpResponsef(writer, "Problem serving scouter schedule", "%s", response) } -// Gives back a theme css file +// Serves a theme's css files func serveTheme(writer http.ResponseWriter, request *http.Request) { auth := getAuthFromCookies(request) + if auth.Preflight { + writer.WriteHeader(200) + return + } + + if !auth.Authed { + writer.WriteHeader(401) + httpResponsef(writer, "Could not get user theme", "Not authenticated :(") + return + } + writer.Header().Set("Vary", "Cookie") writer.Header().Set("Cache-Control", "private, max-age=2, must-revalidate") writer.Header().Set("Content-Type", "text/css; charset=utf-8") @@ -473,7 +485,7 @@ func serveTheme(writer http.ResponseWriter, request *http.Request) { // Optionally if you want to grab a theme independent of the user directTheme := request.Header.Get("theme") - theme := getTheme(auth.UUID) + theme := GetTheme(auth.UUID) if directTheme != "" { allThemes, err := ListAllThemes() @@ -495,13 +507,40 @@ func serveTheme(writer http.ResponseWriter, request *http.Request) { } } else if theme == "" { - theme = "light" + theme = "Light" } writer.WriteHeader(200) http.ServeFile(writer, request, "run/themes/"+theme+".css") } +// serves the current theme that the user is using +func getTheme(writer http.ResponseWriter, request *http.Request) { + auth := getAuthFromCookies(request) + + if !auth.Authed { + writer.WriteHeader(401) + httpResponsef(writer, "Could not set specified theme", "Not authenticated :(") + return + } + + data := struct { + Theme string `json:"theme"` + }{ + Theme: GetTheme(auth.UUID), + } + + writer.Header().Add("Content-Type", "application/json") + encodeErr := json.NewEncoder(writer).Encode(data) + if encodeErr != nil { + LogErrorf(encodeErr, "Problem encoding %v", data) + } else { + writer.WriteHeader(200) + } + +} + +// sets the theme of the authed user func setTheme(writer http.ResponseWriter, request *http.Request) { auth := getAuthFromCookies(request) @@ -511,7 +550,27 @@ func setTheme(writer http.ResponseWriter, request *http.Request) { return } - wantedTheme := request.Header.Get("theme") + requestBytes, err := io.ReadAll(request.Body) + if err != nil { + LogErrorf(err, "Problem reading %v", request.Body) + writer.WriteHeader(500) + return + } + + var data map[string]interface{} + err = json.Unmarshal(requestBytes, &data) + if err != nil { + LogErrorf(err, "Problem unmarshalling %v", requestBytes) + writer.WriteHeader(400) + httpResponsef(writer, "Could not set specified theme", "The json body coukd not be nnmarshalled") + return + } + wantedTheme, ok := data["theme"].(string) + if !ok { + writer.WriteHeader(400) + httpResponsef(writer, "Could not set specified theme", "Missing or invalid 'theme' field") + return + } if wantedTheme != "" { allThemes, err := ListAllThemes() @@ -538,6 +597,7 @@ func setTheme(writer http.ResponseWriter, request *http.Request) { } +// Serves a list of every theme func handleThemesRequest(writer http.ResponseWriter, request *http.Request) { allThemes, err := ListAllThemes() if err != nil { @@ -588,12 +648,15 @@ func addIndividualSchedule(writer http.ResponseWriter, request *http.Request) { // Handles requests for the various leaderboards func serveLeaderboard(writer http.ResponseWriter, request *http.Request) { var lbType string - var typeHeader string = request.Header.Get("type") - if typeHeader == "HighScore" { + + wantedType := request.Header.Get("type") + + switch wantedType { + case "HighScore": lbType = "highscore" - } else if typeHeader == "LifeScore" { + case "LifeScore": lbType = "lifescore" - } else { + default: lbType = "score" } @@ -650,6 +713,7 @@ func handleWithCORS(handler http.HandlerFunc, okCode bool) http.HandlerFunc { if okCode { w.WriteHeader(200) } + handler(w, r) } } @@ -922,6 +986,7 @@ type RequestAuth struct { Username string Role string Authed bool + Preflight bool } func (a RequestAuth) IsAdmin() bool { @@ -931,15 +996,21 @@ func (a RequestAuth) IsAdmin() bool { func getAuthFromCookies(request *http.Request) RequestAuth { var auth RequestAuth + // checks for preflight requests + if request.Method == http.MethodOptions { + auth.Preflight = true + return auth + } + if c, err := request.Cookie("uuid"); err == nil && c != nil { auth.UUID = c.Value - } else if err != nil { + } else if err != nil && err != http.ErrNoCookie { LogError(err, "error getting request cookie 'uuid'") } if c, err := request.Cookie("certificate"); err == nil && c != nil { auth.Certificate = c.Value - } else if err != nil { + } else if err != nil && err != http.ErrNoCookie { LogError(err, "error getting request cookie 'certificate'") } diff --git a/internal/user_db.go b/internal/user_db.go index 0d623cd..080fa83 100644 --- a/internal/user_db.go +++ b/internal/user_db.go @@ -53,7 +53,7 @@ func InitUserDB() { {columnName: "highscore", valueType: "INT"}, {columnName: "accolades", valueType: "TEXT", defaultValue: sql.NullString{String: "'[]'", Valid: true}}, {columnName: "color", valueType: "INT"}, - {columnName: "theme", valueType: "TEXT", defaultValue: sql.NullString{String: "'light'", Valid: true}}, + {columnName: "theme", valueType: "TEXT", defaultValue: sql.NullString{String: "'Light'", Valid: true}}, } if dbMissing { @@ -821,7 +821,7 @@ func SetPfp(uuid string, pfp string) { } // Gets the relative path of a given user's profile picture -func getTheme(uuid string) string { +func GetTheme(uuid string) string { var theme string response := userDB.QueryRow("select theme from users where uuid = ?", uuid) scanErr := response.Scan(&theme) From e1d6af7af3b2f0a2a932e038d99719944a430e13 Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:12:12 -0500 Subject: [PATCH 5/8] Basic Themes The ones that should be okay to push --- run/themes/Dark.css | 25 ++++ run/themes/Hot Pink.css | 25 ++++ run/themes/Light.css | 25 ++++ run/themes/Pittsburgh.css | 25 ++++ run/themes/light.css | 242 -------------------------------------- 5 files changed, 100 insertions(+), 242 deletions(-) create mode 100644 run/themes/Dark.css create mode 100644 run/themes/Hot Pink.css create mode 100644 run/themes/Light.css create mode 100644 run/themes/Pittsburgh.css delete mode 100644 run/themes/light.css diff --git a/run/themes/Dark.css b/run/themes/Dark.css new file mode 100644 index 0000000..94bdc32 --- /dev/null +++ b/run/themes/Dark.css @@ -0,0 +1,25 @@ +/* Like the light one but dark */ + +:root { + --background-color: #181a1b !important; + --accent-color: #4c8ed9 !important; + --accent-color2: #3a6ea5 !important; + --accent-color3: #25364a !important; + --blue-color: #4c8ed9 !important; + --error-color: var(--disabled-color) !important; + --disabled-color: #44484b !important; + --font-color: #f5f6fa !important; + --font-color-prompt-title: #f5f6fa !important; + --font-color-header: #f5f6fa !important; + --drop-shadow-color: #101112 !important; + --sidenav-hover-color: #222 !important; + --faded-text-color: #a0a0a0 !important; + --kelly-green: #4cbb17 !important; + --error-message-color: var(--disabled-color) !important; + --check-background-color: #4cbb17 !important; + --checkbox-default-color: #222 !important; + --checkbox-hover-color: #444 !important; + --border-color-faded: #23272a !important; + --border-color-active: #4c8ed9 !important; + --sidenav-button-color: #23272a !important; +} diff --git a/run/themes/Hot Pink.css b/run/themes/Hot Pink.css new file mode 100644 index 0000000..6c5a18d --- /dev/null +++ b/run/themes/Hot Pink.css @@ -0,0 +1,25 @@ +/* Theme de Jarrpa */ + +:root { + --background-color: #fff0f6 !important; + --accent-color: #ff4da6 !important; + --accent-color2: #ff66b2 !important; + --accent-color3: #ffd6eb !important; + --blue-color: #ff4da6 !important; + --error-color: #ff99c8 !important; + --disabled-color: #b36b8c !important; + --font-color: #1a1a1a !important; + --font-color-prompt-title: #1a1a1a !important; + --font-color-header: #1a1a1a !important; + --drop-shadow-color: rgba(255, 77, 166, 0.4) !important; + --sidenav-hover-color: #ff3399 !important; + --faded-text-color: #a64d79 !important; + --kelly-green: #ff4da6 !important; + --error-message-color: #ff99c8 !important; + --check-background-color: #ff4da6 !important; + --checkbox-default-color: #ffe6f2 !important; + --checkbox-hover-color: #ffb3d9 !important; + --border-color-faded: #ffcce6 !important; + --border-color-active: #ff4da6 !important; + --sidenav-button-color: #ffd6eb !important; +} diff --git a/run/themes/Light.css b/run/themes/Light.css new file mode 100644 index 0000000..f74fb82 --- /dev/null +++ b/run/themes/Light.css @@ -0,0 +1,25 @@ +/* Classic GreenScout Theme */ + +:root { + --background-color: white !important; + --accent-color: #9bcbfb !important; + --accent-color2: #8fbdeb !important; + --accent-color3: #cee5ff !important; + --blue-color: #9bcbfb !important; + --error-color: var(--disabled-color) !important; + --disabled-color: #717172 !important; + --font-color: black !important; + --font-color-prompt-title: black !important; + --font-color-header: black !important; + --drop-shadow-color: #626263 !important; + --sidenav-hover-color: #444 !important; + --faded-text-color: #5f6167 !important; + --kelly-green: #4cbb17 !important; + --error-message-color: var(--disabled-color) !important; + --check-background-color: #4cbb17 !important; + --checkbox-default-color: #eee !important; + --checkbox-hover-color: #ccc !important; + --border-color-faded: #d9d9db !important; + --border-color-active: #417096 !important; + --sidenav-button-color: #dfdfdf !important; +} diff --git a/run/themes/Pittsburgh.css b/run/themes/Pittsburgh.css new file mode 100644 index 0000000..d8ca213 --- /dev/null +++ b/run/themes/Pittsburgh.css @@ -0,0 +1,25 @@ +/* the theme from the 2026 Pittsburgh trip */ + +:root { + --background-color: #141415 !important; + --accent-color: #4cbb17 !important; + --accent-color2: #4cbb17 !important; + --accent-color3: #4cbb17 !important; + --blue-color: #9bcbfb !important; + --error-color: var(--disabled-color) !important; + --disabled-color: #717172 !important; + --font-color: white !important; + --font-color-prompt-title: #4cbb17 !important; + --font-color-header: #58db1c !important; + --drop-shadow-color: #626263 !important; + --sidenav-hover-color: #444 !important; + --faded-text-color: rgb(210, 209, 209) !important; + --kelly-green: #4cbb17 !important; + --error-message-color: var(--disabled-color) !important; + --check-background-color: #4cbb17 !important; + --checkbox-default-color: #eee !important; + --checkbox-hover-color: #ccc !important; + --border-color-faded: #d9d9db !important; + --border-color-active: #4cbb17 !important; + --sidenav-button-color: #626263 !important; +} diff --git a/run/themes/light.css b/run/themes/light.css deleted file mode 100644 index 9baf187..0000000 --- a/run/themes/light.css +++ /dev/null @@ -1,242 +0,0 @@ -:root { - /* Color Variables */ - --background-color: #f8f9fe !important; - --accent-color: #9bcbfb !important; - --accent-color2: #8fbdeb !important; - --accent-color3: #cee5ff !important; - --error-color: lightcoral !important; - --disabled-color: #a1a2a6 !important; - --font-color: #1e2226 !important; - --sidenav-hover-color: #444 !important; - --faded-text-color: #5f6167 !important; - --kelly-green: #4cbb17 !important; /* Kelly Green is the color of Edina's "The Green Machine" */ - --error-message-color: #d32f2f !important; - --check-background-color: #2f628d !important; - --checkbox-default-color: #eee !important; - --checkbox-hover-color: #ccc !important; - --border-color-gray: #93979f !important; - --border-color-blue: #417096 !important; - --sidenav-button-color: #dfdfdf !important; - /* General Sizing Variables */ - --border-radius: 1em !important; - --font-size-large: 3vh !important; - --font-size-small: 2.5vh !important; - --font-weight: 1 !important; - --page-height: 100vh !important; - --page-width: 100vw !important; - --content-height: calc(var(--page-height) - var(--navbar-height)) !important; - --content-width: var(--page-width) !important; - --element-height: 6.2vh !important; /* Not used universally, but use a lot */ - - /* Navbar Sizing Variables */ - --sidenav-width: 70vw !important; - --navbar-height: 8vh !important; - --sidenav-usertile-height: 0% !important; /* Set to 0 while user tile is diabled */ - --sidenav-usertile-width: 15vh !important; - --sidenav-section-height: calc((100% - var(--sidenav-usertile-height)) / 2) !important; - --navbar-hamburger-padding: 15px !important; - --navbar-hamburger-top-distance: -6px !important; - --navbar-text-padding-top: 5px !important; - --sidenav-usertile-margin-bottom: 3vw !important; - --sidenav-userimage-size: 3vw !important; - --sidenav-usertext-h1-margin-top: 1.7vh !important; - --sidenav-usertext-p-margin-top: -4.3vh !important; - --sidenav-usertext-p-margin-left: 0.2vw !important; - --sidenav-padding-top: 60px !important; - /* Login Sizing Variables */ - --login-h1-height: 3vh !important; - --login-h1-margin-top: 4vh !important; - --login-error-message-margin: 10px 0 !important; - - --login-box-width: 30vh !important; - --login-box-margin-top: 5vh !important; - --login-box-margin-bottom: 1.5vh !important; - --login-box-padding-left: calc(var(--login-box-background-size) + 1vh) !important; - --login-box-background-size: 3vh !important; - --login-box-background-position: 5px 1.2vh !important; - - --login-button-width: 30vh !important; - --login-button-height: 6vh !important; - - /* Match Form Sizing Variables */ - --matchform-text-padding-top: 5px !important; - --matchform-text-height: 30px !important; - - --matchform-side-panel-width: 8vh !important; - --matchform-grid-template: 1fr var(--matchform-side-panel-width) !important; - - --matchform-child-margin-side: 7.5vw !important; /* Used Throughout Child Components */ - --matchform-child-width-to-subtract: 15vw !important; /* Used Throughout Child Components */ - - --matchform-element-height: 6.2vh !important; /* Replace throughout with element-height */ - --matchform-matchteam-margin-top: 1.5vh !important; - - --matchform-header-padding-top: 5px !important; - --matchform-header-end-margin-top: 8vh !important; - - --matchform-notes-height: 20vh !important; - - --matchform-cycle-container-height: 40vh !important; - --matchform-cycle-item-height: 9vh !important; - --matchform-cycle-item-margin-bottom: 1vw !important; - --matchform-cycle-item-image-height: 4vh !important; - --matchform-cycle-item-image-width: 4vh !important; - --matchform-cycle-delete-height: 30% !important; - --matchform-cycle-delete-width: 10% !important; - - --matchform-cycle-text-padding-top: 5% !important; - --matchform-cycle-text-padding-left: 5% !important; - --matchform-cycle-text-width: 25 !important; - - --matchform-acclabel-width: 10vh !important; - --matchform-acclabel-height: 3vh !important; - --matchform-accslider-width: 70% !important; - --matchform-accslider-height: 2vh !important; - --matchform-accslider-margin-right: 10% !important; - --matchform-accslider-thumb-size: 50px !important; - - --matchform-bottomspace-height: 15vh !important; - --matchform-bottomspace-max-height: 80px !important; - - /* Match Form Checkbox Sizing Variables */ - --check-text-height: 30px !important; - --check-padding-top: 1vh !important; - - --check-div-margin: 5vh !important; - - --check-size: 2.5vh !important; - - --check-container-padding-left: 35px !important; - --check-container-margin-bottom: 12px !important; - - --check-mark-left: 1vw !important; - - --check-mark-after-left: 9px !important; - --check-mark-after-top: 5px !important; - --check-mark-after-size: 1.2vh !important; - - --check-special-margin-left: 7.5vw !important; - - --check-auto-text-padding-top: 1vh !important; - --check-auto-text-width: 80vw !important; - - /* Match Form Counter Sizing Variables */ - --counter-text-padding-top: 5px !important; - --counter-text-height: 30px !important; - --counter-text-margin-bottom: 5vh !important; - - --counter-child-margin-vertical: 3vh !important; - - --counter-button-height: 5.2vh !important; - --counter-add-width: 40% !important; - --counter-subtract-width: 30% !important; - --counter-button-margin-left: 1vw !important; - - /* Match Form Slider Sizing Variables */ - --slider-container-width: 70% !important; - --slider-container-margin-bottom: 10% !important; - - /* Match Form Collapsible Dropdown Sizing Variables */ - --collapsible-open-max-height: 100% !important; /* 2000px */ - --collapsible-padding: 12px 16px !important; - - /* Match Form Dropdown Sizing Variables */ - --dropdown-margin-top: 3vh !important; - --dropdown-padding-bottom: 0.5vh !important; - --dropdown-height: 5.2vh !important; - --dropdown-width: 15vh !important; - - --dropdown-text-padding-top: 5px !important; - --dropdown-text-height: 30px !important; - - --dropdown-driverstation-margin-top: 1vh !important; - - --dropdown-dtext-height: 3.1vh !important; - --dropdown-dtext-width: 70vw !important; - - --dropdown-collect-container-margin-top: 3% !important; - - --dropdown-dtext-collect-width: 70vw !important; - - --dropdown-collect-margin-top: 16% !important; - --dropdown-collect-padding-bottom: 0.5vh !important; - - --dropdown-park-text-padding-top: 4% !important; - - /* Match Form Trigger Sizing Variables */ - --trigger-margin-top: 5.7vh !important; - --trigger-margin-bottom: 4.4vh !important; - --trigger-size: 6.5vh !important; - - --trigger-stop-image-height: 2vh !important; - --trigger-stop-image-background-size: 1.7vh 1.7vh !important; - --trigger-stop-image-background-position: 2vh 0vh !important; - - --trigger-reset-height: 8vh !important; - - /* Match Form Submit/Replay Sizing Variables */ - --submitreplay-div-height: 8vh !important; - --submitreplay-div-margin-bottom: 1vh !important; - - --submitreplay-button-margin-top: 5.7vh !important; - --submitreplay-button-margin-bottom: 4.4vh !important; - - --submitreplay-text-margin-top: -1vh !important; - - --submitreplay-label-margin-top: 2vh !important; - - --submit-image-margin-top: -3.5vh !important; - --submit-image-size: 4.8vh !important; - - /* General Cycles Sizing Variables */ - --font-size-cycle: 1.8vh !important; - - /* Cycle Start/Stop Button Sizing Variables */ - --cycle-margin-top: 5.7vh !important; - --cycle-margin-bottom: 4.4vh !important; - --cycle-size: 6.5vh !important; - --cycle-top: 4vh !important; - - --cycle-image-height: 2vh !important; - --cycle-image-background-size: 1.7vh 1.7vh !important; - --cycle-image-background-position: 2vh 0vh !important; - - /* Score Cycle Sizing Variables */ - --score-top: 13vh !important; - --score-image-height: 3vh !important; - - /* Shuttle Cycle Sizing Variables */ - --shuttle-top: 22vh !important; - - /* Hub Switch Cycle Sizing Variables */ - --hubSwitch-top: 31vh !important; - - /* Home Page Sizing Variables */ - --new-matchpit-button-height: 8vh !important; - --new-matchpit-button-width: 50vw !important; - - --home-text-padding-top: 5px !important; - --home-assigned-h-padding-top: 10px !important; - --home-refresh-container-bottom: -14px !important; - --home-refresh-container-right: 17px !important; - --home-refresh-button-margin-top: 5.7vh !important; - --home-refresh-button-margin-bottom: 4.4vh !important; - --refresh-button-size: 7.9vh !important; - - /* Sizing Variables */ - - /* Leader Board Sizing Variables */ - --leader-board-element-height: 5vh !important; - - /* Settings Sizing Variables */ - --settings-element-width: 80% !important; - --settings-element-height: calc(var(--font-size-large) * 2) !important; - --settings-element-spacing: calc(var(--font-size-small) / 2) !important; - --settings-debug-image-offset: 2vh !important; - - --settings-themedrop-padding-top: 5px !important; - --settings-themedrop-height: 30px !important; - --settings-themedrop-text-width: 70vw !important; - --settings-themedrop-margin-top: 10% !important; - --settings-themedrop-padding-bottom: 0.5vh !important; -} From 1aa244c94cb6ac70187eca24507c98d6aca8e1fd Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:12:30 -0500 Subject: [PATCH 6/8] Epic themes the themes that we need but dont deserve --- run/themes/2Slimey.css | 34 ++++++++++++++++++++++++++++++++++ run/themes/Best Buy.css | 24 ++++++++++++++++++++++++ run/themes/Cloudflare.css | 24 ++++++++++++++++++++++++ run/themes/FL Studio.css | 25 +++++++++++++++++++++++++ run/themes/Prettifun.css | 25 +++++++++++++++++++++++++ run/themes/Whatsapp.css | 25 +++++++++++++++++++++++++ 6 files changed, 157 insertions(+) create mode 100644 run/themes/2Slimey.css create mode 100644 run/themes/Best Buy.css create mode 100644 run/themes/Cloudflare.css create mode 100644 run/themes/FL Studio.css create mode 100644 run/themes/Prettifun.css create mode 100644 run/themes/Whatsapp.css diff --git a/run/themes/2Slimey.css b/run/themes/2Slimey.css new file mode 100644 index 0000000..99e7cff --- /dev/null +++ b/run/themes/2Slimey.css @@ -0,0 +1,34 @@ +/* viva mexico */ + +:root { + --background-color: #0f0f0f !important; + --font-color: #e8ffe8 !important; + --font-color-prompt-title: #ffffff !important; + --font-color-header: #ffffff !important; + --accent-color: #00ff66 !important; + --accent-color2: #ff1744 !important; + --accent-color3: #1affd5 !important; + --blue-color: #1affd5 !important; + --kelly-green: #00ff66 !important; + --error-color: #ff1744 !important; + --error-message-color: #ff1744 !important; + --disabled-color: #5a5a5a !important; + --faded-text-color: #9cffc7 !important; + --drop-shadow-color: rgba(0, 255, 102, 0.6) !important; + --border-color-faded: #1f1f1f !important; + --border-color-active: #00ff66 !important; + --sidenav-hover-color: #ff1744 !important; + --sidenav-button-color: #1a1a1a !important; + --check-background-color: #00ff66 !important; + --checkbox-default-color: #2a2a2a !important; + --checkbox-hover-color: #3a3a3a !important; +} + +* { + text-shadow: 0 0 6px rgba(0, 255, 102, 0.6); +} + +button, +.accent { + box-shadow: 0 0 12px rgba(0, 255, 102, 0.7); +} diff --git a/run/themes/Best Buy.css b/run/themes/Best Buy.css new file mode 100644 index 0000000..c8bb981 --- /dev/null +++ b/run/themes/Best Buy.css @@ -0,0 +1,24 @@ +:root { + --background-color: #f6f8fb !important; + --font-color: #0b0f14 !important; + --font-color-header: #0b0f14 !important; + --font-color-prompt-title: #0b0f14 !important; + --accent-color: #ffd400 !important; + --accent-color2: #1f4fbf !important; + --accent-color3: #dbe8ff !important; + --blue-color: #2a66ff !important; + --border-color-active: #1f4fbf !important; + --border-color-faded: #d6dde8 !important; + --sidenav-button-color: #eef2f7 !important; + --drop-shadow-color: rgba(11, 15, 20, 0.35) !important; + --sidenav-hover-color: #163b8e !important; + --faded-text-color: #4d5a6b !important; + --disabled-color: #7a8798 !important; + --kelly-green: #2ecc71 !important; + --check-background-color: #2ecc71 !important; + --error-color: #d92d20 !important; + --error-message-color: #d92d20 !important; + --check-background-color: #ffd400 !important; + --checkbox-default-color: #f2f4f7 !important; + --checkbox-hover-color: #e4e8ef !important; +} diff --git a/run/themes/Cloudflare.css b/run/themes/Cloudflare.css new file mode 100644 index 0000000..b40b0a3 --- /dev/null +++ b/run/themes/Cloudflare.css @@ -0,0 +1,24 @@ +/* oh cloudflare why you litttlee if i everrrrr*/ + +:root { + --background-color: #ffffff !important; + --font-color: #111111 !important; + --font-color-prompt-title: #111111 !important; + --font-color-header: #111111 !important; + --accent-color: #f48120 !important; + --accent-color2: #fcb44b !important; + --accent-color3: #ffe2be !important; + --disabled-color: #6b7280 !important; + --faded-text-color: #4b5563 !important; + --border-color-faded: #e5e7eb !important; + --border-color-active: #f48120 !important; + --drop-shadow-color: rgba(17, 24, 39, 0.25) !important; + --sidenav-button-color: #f3f4f6 !important; + --sidenav-hover-color: #2b2f36 !important; + --kelly-green: #22c55e !important; + --check-background-color: #22c55e !important; + --error-color: #ef4444 !important; + --error-message-color: #ef4444 !important; + --checkbox-default-color: #f3f4f6 !important; + --checkbox-hover-color: #e5e7eb !important; +} diff --git a/run/themes/FL Studio.css b/run/themes/FL Studio.css new file mode 100644 index 0000000..0db1d4d --- /dev/null +++ b/run/themes/FL Studio.css @@ -0,0 +1,25 @@ +/* when fl studio die */ + +:root { + --background-color: #223b44 !important; + --accent-color: #ff8a1f !important; + --accent-color2: #ffa84b !important; + --accent-color3: #ffd2a3 !important; + --blue-color: #6fbad1 !important; + --error-color: #ff4d4d !important; + --disabled-color: #7f969d !important; + --font-color: #eef6f8 !important; + --font-color-prompt-title: #f6fbff !important; + --font-color-header: #f6fbff !important; + --drop-shadow-color: rgba(0, 0, 0, 0.45) !important; + --sidenav-hover-color: #3b6674 !important; + --faded-text-color: #b6c9cf !important; + --kelly-green: #63d13a !important; + --error-message-color: #ff6b6b !important; + --check-background-color: #63d13a !important; + --checkbox-default-color: #1d3138 !important; + --checkbox-hover-color: #24404a !important; + --border-color-faded: #365c67 !important; + --border-color-active: #ff8a1f !important; + --sidenav-button-color: #2b4a55 !important; +} diff --git a/run/themes/Prettifun.css b/run/themes/Prettifun.css new file mode 100644 index 0000000..95a162d --- /dev/null +++ b/run/themes/Prettifun.css @@ -0,0 +1,25 @@ +/* pretti! buahahaha */ + +:root { + --background-color: #00c8e6 !important; + --accent-color: #ff3300 !important; + --accent-color2: #ff8c00 !important; + --accent-color3: #c400d4 !important; + --blue-color: #1b3fa8 !important; + --error-color: #ff3300 !important; + --disabled-color: #5a7ab0 !important; + --font-color: #001833 !important; + --font-color-prompt-title: #001833 !important; + --font-color-header: #ffffff !important; + --drop-shadow-color: #003344 !important; + --sidenav-hover-color: #ff3300 !important; + --faded-text-color: #004466 !important; + --kelly-green: #39ff14 !important; + --error-message-color: #ff3300 !important; + --check-background-color: #39ff14 !important; + --checkbox-default-color: #b3eeff !important; + --checkbox-hover-color: #ffe500 !important; + --border-color-faded: #00a0bb !important; + --border-color-active: #ff3300 !important; + --sidenav-button-color: #b3eeff !important; +} diff --git a/run/themes/Whatsapp.css b/run/themes/Whatsapp.css new file mode 100644 index 0000000..1b73535 --- /dev/null +++ b/run/themes/Whatsapp.css @@ -0,0 +1,25 @@ +/* Dear future devs, please make the background whatsapp half-life λ*/ + +:root { + --background-color: #ece5dd !important; + --accent-color: #25d366 !important; + --accent-color2: #075e54 !important; + --accent-color3: #dcf8c6 !important; + --blue-color: #34b7f1 !important; + --error-color: #f44336 !important; + --disabled-color: #bdbdbd !important; + --font-color: #222 !important; + --font-color-prompt-title: #075e54 !important; + --font-color-header: #075e54 !important; + --drop-shadow-color: #0002 !important; + --sidenav-hover-color: #f0f0f0 !important; + --faded-text-color: #888 !important; + --kelly-green: #25d366 !important; + --error-message-color: #f44336 !important; + --check-background-color: #25d366 !important; + --checkbox-default-color: #fff !important; + --checkbox-hover-color: #dcf8c6 !important; + --border-color-faded: #ece5dd !important; + --border-color-active: #25d366 !important; + --sidenav-button-color: #fff !important; +} From cdbf03ab9d015fabf24813184ea7f11334cd624c Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:13:20 -0500 Subject: [PATCH 7/8] Create themes documentation --- docs/Themes.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/Themes.md diff --git a/docs/Themes.md b/docs/Themes.md new file mode 100644 index 0000000..cbfde9e --- /dev/null +++ b/docs/Themes.md @@ -0,0 +1,22 @@ +# Themes + +Inside of the [themes folder](../run/themes/), you may find the CSS files for +each theme. Each theme lives inside a single file CSS. The name of the theme +will be inherited from the name of the file (omitting '.css'). + +The themes will be loaded via a link HTML tag, so expect the entire CSS file to +have an effect on the site. + +ALL CSS variables inside a theme must be followed by `!important`. This will let +the browser know to use our CSS variable definitions instead the defaults from +GreenScoutJS. + +```css +--font-color: #1e2226 !important; +``` + +If a theme does not provide a definition for a CSS variable, its default will be +used in place. The default theme can be found +[here in the GreenScoutJS repo. **Reference this file.**](https://github.com/TheGreenMachine/GreenScoutJS/blob/main/greenscoutjs/src/index.css) +**for all available CSS variables to override.** Again, make sure to include +`!important` when copy and pasting. From 009b281a1c17e93049fa7c1e3a31723d621b8a9b Mon Sep 17 00:00:00 2001 From: Noah D <71520770+DogeDoge17@users.noreply.github.com> Date: Fri, 3 Apr 2026 14:31:31 -0500 Subject: [PATCH 8/8] Use different spreedsheet auth for test and prod compromise Different switching mode --- internal/sheet_writer.go | 24 +++++++++++++++++++++--- main.go | 3 ++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/internal/sheet_writer.go b/internal/sheet_writer.go index 5641a42..5a1afc8 100644 --- a/internal/sheet_writer.go +++ b/internal/sheet_writer.go @@ -17,6 +17,12 @@ import ( yaml "sigs.k8s.io/yaml/goyaml.v2" ) +var useLocalAuth bool = false + +func UseLocalAuth(val bool) { + useLocalAuth = val +} + // Early methods (setup) are from google's quickstart, so I didn't change much about them // Retrieve a token, saves the token, then returns the generated client. @@ -92,10 +98,22 @@ var Srv *sheets.Service func SetupSheetsAPI(creds []byte) { ctx := context.Background() - client, err := google.DefaultClient(context.Background(), sheets.SpreadsheetsScope) - if err != nil { - FatalError(err, "Unable to parse client secret file to config: %v") + var client *http.Client + var err error + + if useLocalAuth { + config, err := google.ConfigFromJSON(creds, "https://www.googleapis.com/auth/spreadsheets") + if err != nil { + FatalError(err, "Unable to parse client secret file to config: %v") + } + client = getClient(config) + } else { + client, err = google.DefaultClient(context.Background(), sheets.SpreadsheetsScope) + if err != nil { + FatalError(err, "Unable to parse client secret file to config: %v") + } } + Srv, err = sheets.NewService(ctx, option.WithHTTPClient(client)) if err != nil { FatalError(err, "Unable to retrieve Sheets client: %v") diff --git a/main.go b/main.go index 21dd354..7219b02 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,8 @@ func main() { /// Setup isSetup := slices.Contains(os.Args, "setup") - internal.SetSecureCookies(slices.Contains(os.Args, "test")) + internal.SetSecureCookies(slices.Contains(os.Args, "local")) + internal.UseLocalAuth(slices.Contains(os.Args, "local")) publicHosting := false //Allows setup to bypass ip and domain validation to run localhost serveTLS := false //switches it between https and http [true for https] updateDB := false