diff --git a/ops/nixos/totoro/barf.nix b/ops/nixos/totoro/barf.nix index ce03999304..bc0c07c00a 100644 --- a/ops/nixos/totoro/barf.nix +++ b/ops/nixos/totoro/barf.nix @@ -8,10 +8,16 @@ systemd.targets.barf = { wantedBy = [ "multi-user.target" ]; }; + security.wrappers.barf-cli = { + source = "${depot.web.barf.frontend}/bin/barfcli"; + setuid = true; + owner = "root"; + group = "root"; + }; systemd.services.barf-fe = { wantedBy = [ "barf.target" ]; serviceConfig = { - ExecStart = "${depot.web.barf.frontend}/bin/frontend -serve=:12001 -sam_backend=http://127.0.0.1:11316 -db_path=/var/lib/barf-fe/db.db"; + ExecStart = "${depot.web.barf.frontend}/bin/barffe -serve=:12001 -sam_backend=http://127.0.0.1:11316 -db_path=/var/lib/barf-fe/db.db"; StateDirectory = "barf-fe"; User = "barf-fe"; PrivateTmp = true; diff --git a/web/barf/frontend/barfcli/barfcli.go b/web/barf/frontend/barfcli/barfcli.go new file mode 100644 index 0000000000..90fed15405 --- /dev/null +++ b/web/barf/frontend/barfcli/barfcli.go @@ -0,0 +1,133 @@ +package main + +import ( + "database/sql" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "hg.lukegb.com/lukegb/depot/web/barf/frontend/barfdb" + + _ "github.com/mattn/go-sqlite3" +) + +var ( + db *barfdb.DB + flagDBPath string +) + +var rootCmd = &cobra.Command{ + Use: "barfcli", + Short: "BARF is the Birthday Activities Registration Form", + Long: `Manage your Birthday Activities`, +} + +var initializeCmd = &cobra.Command{ + Use: "initialize", + Short: "Initialize a new database", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return db.Initialize(cmd.Context()) + }, +} + +var addCmd = &cobra.Command{ + Use: "add keyprefix name", + Short: "Add a new key", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + key, err := db.GetOrCreateKey(ctx, args[0], &barfdb.Dataset{Name: args[1]}) + if err != nil { + return err + } + fmt.Println(key) + return nil + }, +} + +var lsCmd = &cobra.Command{ + Use: "ls", + Short: "List keys and respondents", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + entries, err := db.List(ctx) + if err != nil { + return err + } + maxLengths := []int{ + len("basekey"), + len("key"), + len("name"), + } + for _, entry := range entries { + for n, l := range []int{ + len(entry.BaseKey()), + len(entry.FullKey), + len(entry.Dataset.Name), + } { + if maxLengths[n] < l { + maxLengths[n] = l + } + } + } + fmtFmt := fmt.Sprintf("%%-%ds\t%%-%ds\t%%-%ds\t%%v\n", maxLengths[0], maxLengths[1], maxLengths[2]) + fmt.Printf(fmtFmt, "basekey", "key", "name", "completed") + fmt.Printf(fmtFmt, strings.Repeat("-", maxLengths[0]), strings.Repeat("-", maxLengths[1]), strings.Repeat("-", maxLengths[2]), "---------") + for _, entry := range entries { + fmt.Printf(fmtFmt, entry.BaseKey(), entry.FullKey, entry.Dataset.Name, entry.Completed()) + } + return nil + }, +} + +var getCmd = &cobra.Command{ + Use: "get short", + Short: "Get responses for a user", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + resp, err := db.GetResponseForShortKey(ctx, args[0]) + if err != nil { + return err + } + hs, err := resp.HumanString() + if err != nil { + return err + } + fmt.Println(hs) + return nil + }, +} + +func openDB() { + sdb, err := sql.Open("sqlite3", flagDBPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Opening database %v: %v\n", flagDBPath, err) + os.Exit(1) + return + } + db = barfdb.New(sdb) +} + +func closeDB() { + db.Close() +} + +func init() { + cobra.OnInitialize(openDB) + cobra.OnFinalize(closeDB) + + rootCmd.PersistentFlags().StringVar(&flagDBPath, "db", "/var/lib/barf-fe/db.db", "BARF database.") + + rootCmd.AddCommand(initializeCmd, addCmd, lsCmd, getCmd) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/web/barf/frontend/barfdb/barfdb.go b/web/barf/frontend/barfdb/barfdb.go new file mode 100644 index 0000000000..a1c294049d --- /dev/null +++ b/web/barf/frontend/barfdb/barfdb.go @@ -0,0 +1,196 @@ +package barfdb + +import ( + "context" + "crypto/rand" + "database/sql" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "strings" +) + +type Dataset struct { + CurrentPhase int `json:"currentPhase"` + CurrentPhaseDialog int `json:"currentPhaseDialog"` + AudioEnabled bool `json:"audioEnabled"` + Name string `json:"name"` + DiscordUsername string `json:"discordUsername"` + ReceiveEmail bool `json:"receiveEmail"` + Email string `json:"email"` + DateSat31August string `json:"dateSat31August"` + DateSat7September string `json:"dateSat7September"` + ActivityEscapeRoom string `json:"activityEscapeRoom"` + ActivityPub string `json:"activityPub"` + ActivityMeanGirlsTheMusical string `json:"activityMeanGirlsTheMusical"` + ActivityKaraoke string `json:"activityKaraoke"` + AccommodationRequired bool `json:"accommodationRequired"` + TravelCosts bool `json:"travelCosts"` + Misc string `json:"misc"` +} + +func (ds *Dataset) Scan(value any) error { + return json.Unmarshal([]byte(value.(string)), ds) +} + +func (ds *Dataset) Value() (driver.Value, error) { + b, err := json.Marshal(ds) + return string(b), err +} + +func (ds *Dataset) HumanString() (string, error) { + bs, err := json.MarshalIndent(ds, "", " ") + if err != nil { + return "", err + } + return string(bs), nil +} + +type DB struct { + db *sql.DB +} + +func New(sdb *sql.DB) *DB { + return &DB{sdb} +} + +func (d *DB) Close() error { + return d.db.Close() +} + +var ErrNoSuchKey = errors.New("barfdb: no such key") + +const ( + keyBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + keyLength = 12 +) + +func (d *DB) Initialize(ctx context.Context) error { + _, err := d.db.ExecContext(ctx, "CREATE TABLE responses (key STRING PRIMARY KEY NOT NULL, responses JSONB NOT NULL)") + return err +} + +type Entry struct { + FullKey string + Dataset Dataset +} + +func (e Entry) BaseKey() string { + if strings.Contains(e.FullKey, "/") { + return e.FullKey[:strings.Index(e.FullKey, "/")] + } + return e.FullKey +} + +func (e Entry) Completed() bool { + mustHaveValue := []string{ + e.Dataset.DiscordUsername, + e.Dataset.Name, + e.Dataset.ActivityEscapeRoom, + e.Dataset.ActivityKaraoke, + e.Dataset.ActivityMeanGirlsTheMusical, + e.Dataset.ActivityPub, + e.Dataset.DateSat31August, + e.Dataset.DateSat7September, + } + for _, v := range mustHaveValue { + if v == "" { + return false + } + } + return true +} + +func (d *DB) List(ctx context.Context) ([]Entry, error) { + rows, err := d.db.QueryContext(ctx, "SELECT key, responses FROM responses ORDER BY key") + if err != nil { + return nil, fmt.Errorf("querying database: %w", err) + } + var entries []Entry + for rows.Next() { + var ( + fullKey string + dataset Dataset + ) + if err := rows.Scan(&fullKey, &dataset); err != nil { + return nil, fmt.Errorf("scanning row from database: %w", err) + } + + entry := Entry{ + FullKey: fullKey, + Dataset: dataset, + } + entries = append(entries, entry) + } + return entries, nil +} + +func (d *DB) GetOrCreateKey(ctx context.Context, baseKey string, initialDataset *Dataset) (string, error) { + var existingKey string + err := d.db.QueryRowContext(ctx, `SELECT key FROM responses WHERE key LIKE ? || '/%'`, baseKey).Scan(&existingKey) + if err == nil { + return existingKey, nil + } else if errors.Is(err, sql.ErrNoRows) { + // This is fine, we'll create it. + } else { + return "", fmt.Errorf("querying database for existing keys for base %q: %w", baseKey, err) + } + + randomKeyBytes := make([]byte, keyLength) + if _, err := rand.Read(randomKeyBytes); err != nil { + return "", fmt.Errorf("generating random key: %w", err) + } + + randomKeyStringBytes := make([]byte, keyLength) + for n, b := range randomKeyBytes { + randomKeyStringBytes[n] = keyBytes[int(b)%len(keyBytes)] + } + randomKey := string(randomKeyStringBytes) + fullKey := baseKey + "/" + randomKey + + if _, err := d.db.ExecContext(ctx, `INSERT INTO responses (key, responses) VALUES (?, ?)`, fullKey, initialDataset); err != nil { + return "", fmt.Errorf("inserting into responses table: %w", err) + } + return fullKey, nil +} + +func (d *DB) GetResponseForShortKey(ctx context.Context, shortKey string) (*Dataset, error) { + var response Dataset + err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ? OR key LIKE ? || '/%'`, shortKey, shortKey).Scan(&response) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNoSuchKey + } else if err != nil { + return nil, fmt.Errorf("fetching %v from database: %w", shortKey, err) + } + return &response, nil +} + +func (d *DB) GetResponseForKey(ctx context.Context, key string) (*Dataset, error) { + var response Dataset + err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ?`, key).Scan(&response) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrNoSuchKey + } else if err != nil { + return nil, fmt.Errorf("fetching %v from database: %w", key, err) + } + return &response, nil +} + +func (d *DB) SaveResponseForKey(ctx context.Context, key string, dataset *Dataset) error { + res, err := d.db.ExecContext(ctx, `UPDATE responses SET responses = ? WHERE key = ?`, dataset, key) + if err != nil { + return fmt.Errorf("updating database for %v: %w", key, err) + } + rowCount, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("determining affected row count for %v: %w", key, err) + } else if rowCount == 0 { + return ErrNoSuchKey + } + return nil +} + +func NewDB(db *sql.DB) *DB { + return &DB{db: db} +} diff --git a/web/barf/frontend/barfdb/barfdb_test.go b/web/barf/frontend/barfdb/barfdb_test.go new file mode 100644 index 0000000000..091a0da55e --- /dev/null +++ b/web/barf/frontend/barfdb/barfdb_test.go @@ -0,0 +1,166 @@ +package barfdb + +import ( + "context" + "database/sql" + "testing" + + "github.com/google/go-cmp/cmp" + + _ "github.com/mattn/go-sqlite3" +) + +func makeDB(t *testing.T) *DB { + t.Helper() + sdb, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("creating inmemory sqlite3 database: %v", err) + } + t.Cleanup(func() { sdb.Close() }) + + db := &DB{sdb} + if err := db.Initialize(context.Background()); err != nil { + t.Fatalf("initializing DB: %v", err) + } + + return &DB{sdb} +} + +func TestGetOrCreateKey(t *testing.T) { + db := makeDB(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ds := Dataset{ + Name: "Slartibartfast", + } + + key, err := db.GetOrCreateKey(ctx, "testkey", &ds) + if err != nil { + t.Fatalf("GetOrCreateKey (first time): %v", err) + } + + key2, err := db.GetOrCreateKey(ctx, "testkey", &ds) + if err != nil { + t.Fatalf("GetOrCreateKey (second time): %v", err) + } + + if key != key2 { + t.Errorf("GetOrCreateKey returned different results for 'testkey': %v != %v", key, key2) + } + + gotds, err := db.GetResponseForKey(ctx, key) + if err != nil { + t.Fatalf("GetResponseForKey(%q): %v", key, err) + } + if diff := cmp.Diff(*gotds, ds); diff != "" { + t.Errorf("GetResponseForKey diff (-got +want):\n%s", diff) + } +} + +func TestSaveResponseForKey(t *testing.T) { + db := makeDB(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + key, err := db.GetOrCreateKey(ctx, "testkey", &Dataset{}) + if err != nil { + t.Fatalf("GetOrCreateKey: %v", err) + } + + ds := Dataset{ + Name: "Slartibartfast", + } + if err := db.SaveResponseForKey(ctx, key, &ds); err != nil { + t.Fatalf("SaveResponseForKey: %v", err) + } + + gotds, err := db.GetResponseForKey(ctx, key) + if err != nil { + t.Fatalf("GetResponseForKey: %v", err) + } + if diff := cmp.Diff(*gotds, ds); diff != "" { + t.Errorf("GetResponseForKey diff (-got +want):\n%s", diff) + } +} + +func TestSaveResponseForKey_DoesNotExist(t *testing.T) { + db := makeDB(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ds := Dataset{ + Name: "Slartibartfast", + } + if err := db.SaveResponseForKey(ctx, "testkey/foo", &ds); err != ErrNoSuchKey { + t.Fatalf("SaveResponseForKey: %v (want ErrNoSuchKey)", err) + } +} + +func TestGetResponseForKey_DoesNotExist(t *testing.T) { + db := makeDB(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if _, err := db.GetResponseForKey(ctx, "testkey/foo"); err != ErrNoSuchKey { + t.Fatalf("GetResponseForKey: %v (want ErrNoSuchKey)", err) + } +} + +func TestBaseKey(t *testing.T) { + for _, tc := range []struct { + FullKey string + Want string + }{{ + FullKey: "foo", + Want: "foo", + }, { + FullKey: "foo/bar", + Want: "foo", + }, { + FullKey: "foo/bar/baz", + Want: "foo", + }} { + e := Entry{FullKey: tc.FullKey} + got := e.BaseKey() + if got != tc.Want { + t.Errorf("Entry{FullKey: %q}.BaseKey() = %q; want %q", tc.FullKey, got, tc.Want) + } + } +} + +func TestList(t *testing.T) { + db := makeDB(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + keys := map[string]string{} + datasets := map[string]*Dataset{} + mustGetOrCreateKey := func(key string, ds *Dataset) { + fullKey, err := db.GetOrCreateKey(ctx, key, ds) + if err != nil { + t.Fatalf("GetOrCreateKey(ctx, %q, ds): %v", key, err) + } + keys[key] = fullKey + datasets[key] = ds + } + mustGetOrCreateKey("2-foo", &Dataset{Name: "A"}) + mustGetOrCreateKey("1-fully-complete", &Dataset{ + Name: "B Fully Complete", + }) + want := []Entry{{ + FullKey: keys["1-fully-complete"], + Dataset: *datasets["1-fully-complete"], + }, { + FullKey: keys["2-foo"], + Dataset: *datasets["2-foo"], + }} + + es, err := db.List(ctx) + if err != nil { + t.Fatalf("List(ctx): %v", err) + } + if diff := cmp.Diff(es, want); diff != "" { + t.Errorf("List(ctx): has diff (-want +got):\n%s", diff) + } +} diff --git a/web/barf/frontend/default.nix b/web/barf/frontend/default.nix index 8a0e0dd007..0a117a189b 100644 --- a/web/barf/frontend/default.nix +++ b/web/barf/frontend/default.nix @@ -6,6 +6,27 @@ pkgs.buildGoModule { name = "barf-fe"; - src = lib.sourceByRegex ./. [".*\.go$" "go.mod" "go.sum" "static" "static/clipit" "static/fonts" ".*/.*\.webm" ".*/.*\.png" ".*/.*\.wav" ".*/.*\.svg" ".*\.html" ".*/.*\.woff" ".*/.*\.woff2" ".*/.*\.css"]; - vendorHash = "sha256:0w1k1ykga70af3643lky701kf27pfmgc3lhznfq1v32ww365w57f"; + src = lib.sourceByRegex ./. [ + ".*\.go$" + "go.mod" + "go.sum" + "barfdb" + "barfcli" + "static" + "static/clipit" + "static/fonts" + ".*/.*\.webm" + ".*/.*\.png" + ".*/.*\.wav" + ".*/.*\.svg" + ".*\.html" + ".*/.*\.woff" + ".*/.*\.woff2" + ".*/.*\.css" + ]; + vendorHash = "sha256:0caif1kkxycdqpcp593y6dimwpwh7qcngncv360qb7m9a3ikh82y"; + + postInstall = '' + mv $out/bin/frontend $out/bin/barffe + ''; } diff --git a/web/barf/frontend/frontend.go b/web/barf/frontend/frontend.go index 9b5340312b..fcc1f8502b 100644 --- a/web/barf/frontend/frontend.go +++ b/web/barf/frontend/frontend.go @@ -14,6 +14,8 @@ import ( "net/http/httputil" "net/url" + "hg.lukegb.com/lukegb/depot/web/barf/frontend/barfdb" + _ "github.com/mattn/go-sqlite3" ) @@ -30,7 +32,7 @@ var indexTmplBytes []byte var staticFS embed.FS type application struct { - db *sql.DB + db *barfdb.DB indexTmpl *template.Template } @@ -68,42 +70,27 @@ func (a *application) ServeHTTP(rw http.ResponseWriter, r *http.Request) { http.Error(rw, "reading body failed", http.StatusInternalServerError) return } - var response dataset + var response barfdb.Dataset if err := json.Unmarshal(body, &response); err != nil { log.Printf("failed to unmarshal PUT dataset: %v", err) http.Error(rw, "unmarshalling body failed", http.StatusBadRequest) return } - cleanedBody, err := json.Marshal(response) - if err != nil { - log.Printf("failed to remarshal PUT dataset: %v", err) - http.Error(rw, "remarshalling body failed", http.StatusBadRequest) - return - } - res, err := a.db.ExecContext(r.Context(), `UPDATE responses SET responses = ? WHERE key = ?`, string(cleanedBody), key) - if err != nil { - log.Printf("failed to update DB: %v", err) - http.Error(rw, "updating database failed", http.StatusInternalServerError) - return - } - rowCount, err := res.RowsAffected() - if err != nil { - log.Printf("determining affected rows: %v", err) - http.Error(rw, "updating database failed", http.StatusInternalServerError) - return - } else if rowCount == 0 { - log.Printf("no rows affected for key %v", key) + if err := a.db.SaveResponseForKey(r.Context(), key, &response); errors.Is(err, barfdb.ErrNoSuchKey) { rw.WriteHeader(http.StatusNotFound) fmt.Fprintf(rw, "Sorry, you need a key to access this application.\n") return + } else if err != nil { + log.Printf("SaveResponseKeyFor %v: %v", key, err) + http.Error(rw, "saving response failed", http.StatusInternalServerError) + return } rw.WriteHeader(http.StatusNoContent) return } - var responseJSON *string - err := a.db.QueryRowContext(r.Context(), `select responses from responses where key = ?`, key).Scan(&responseJSON) - if errors.Is(err, sql.ErrNoRows) { + response, err := a.db.GetResponseForKey(r.Context(), key) + if errors.Is(err, barfdb.ErrNoSuchKey) { rw.WriteHeader(http.StatusNotFound) fmt.Fprintf(rw, "Sorry, you need a key to access this application.\n") return @@ -113,29 +100,18 @@ func (a *application) ServeHTTP(rw http.ResponseWriter, r *http.Request) { return } - var responseJSONStr string - if responseJSON == nil { - responseJSONStr = "{}" - } else { - var response dataset - if err := json.Unmarshal([]byte(*responseJSON), &response); err != nil { - log.Printf("database key %v corrupt, not valid json?: %v", key, err) - http.Error(rw, "response data corrupt!", http.StatusInternalServerError) - return - } - if response.CurrentPhase >= 6 { - // Make sure people who have saved _completion_ start at the beginning. - response.CurrentPhase = 0 - response.CurrentPhaseDialog = 0 - } - responseJSONBytes, err := json.Marshal(response) - if err != nil { - log.Printf("couldn't reserialise database key %v: %v", key, err) - http.Error(rw, "something went wrong with reserialisation...", http.StatusInternalServerError) - return - } - responseJSONStr = string(responseJSONBytes) + if response.CurrentPhase >= 6 { + // Make sure people who have saved _completion_ start at the beginning. + response.CurrentPhase = 0 + response.CurrentPhaseDialog = 0 } + responseJSONBytes, err := json.Marshal(response) + if err != nil { + log.Printf("couldn't reserialise database key %v: %v", key, err) + http.Error(rw, "something went wrong with reserialisation...", http.StatusInternalServerError) + return + } + responseJSONStr := string(responseJSONBytes) rw.Header().Set("Content-type", "text/html") a.indexTmpl.Execute(rw, template.JS(responseJSONStr)) @@ -149,11 +125,12 @@ func main() { log.Fatalf("parsing index template: %v", err) } - db, err := sql.Open("sqlite3", *dbPath) + sdb, err := sql.Open("sqlite3", *dbPath) if err != nil { log.Fatal(err) } - defer db.Close() + defer sdb.Close() + db := barfdb.New(sdb) app := &application{db: db, indexTmpl: indexTmpl} @@ -163,7 +140,7 @@ func main() { samBackendPath, err := url.Parse(*samBackend) if err != nil { - log.Fatalf("parsing -sam_backend=%q: %w", *samBackend, err) + log.Fatalf("parsing -sam_backend=%q: %v", *samBackend, err) } be := httputil.NewSingleHostReverseProxy(samBackendPath) http.Handle("/sam", be) diff --git a/web/barf/frontend/go.mod b/web/barf/frontend/go.mod index 73d6bee655..7c9aa4e4d7 100644 --- a/web/barf/frontend/go.mod +++ b/web/barf/frontend/go.mod @@ -2,4 +2,13 @@ module hg.lukegb.com/lukegb/depot/web/barf/frontend go 1.21.7 -require github.com/mattn/go-sqlite3 v1.14.22 +require ( + github.com/google/go-cmp v0.6.0 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/spf13/cobra v1.8.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/web/barf/frontend/go.sum b/web/barf/frontend/go.sum index e8d092a96f..870a51ab8f 100644 --- a/web/barf/frontend/go.sum +++ b/web/barf/frontend/go.sum @@ -1,2 +1,14 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/web/barf/frontend/index.html b/web/barf/frontend/index.html index 20e60533c7..d7ef0e0dc7 100644 --- a/web/barf/frontend/index.html +++ b/web/barf/frontend/index.html @@ -14,7 +14,7 @@ html, body, #page { } body { background-color: #003399; - font-family: Tahoma; + font-family: 'Tahoma'; user-select: none; } @@ -373,7 +373,7 @@ body.all-done .now-safe { } .dialog-btn { box-sizing: border-box; - font-family: Tahoma; + font-family: 'Tahoma'; padding: 0.2em 1.8em; background-color: #d8d0cc; @@ -398,7 +398,7 @@ body.all-done .now-safe { width: 100%; min-height: 6rem; resize: vertical; - font-family: Tahoma; + font-family: 'Tahoma'; } .if-on-mobile { display: none;