diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index cd9f27c36..29f45fb5b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [tankerkiller125,katosdev] +github: [tankerkiller125,katosdev,tonyaellie] diff --git a/.github/workflows/docker-publish-arm.yaml b/.github/workflows/docker-publish-arm.yaml index 0147fa1c7..fa8de098a 100644 --- a/.github/workflows/docker-publish-arm.yaml +++ b/.github/workflows/docker-publish-arm.yaml @@ -4,25 +4,25 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/docker-publish-rootless-arm.yaml b/.github/workflows/docker-publish-rootless-arm.yaml index c40cd55bf..2fee317ea 100644 --- a/.github/workflows/docker-publish-rootless-arm.yaml +++ b/.github/workflows/docker-publish-rootless-arm.yaml @@ -4,25 +4,25 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/docker-publish-rootless.yaml b/.github/workflows/docker-publish-rootless.yaml index f5d022c69..8dd775198 100644 --- a/.github/workflows/docker-publish-rootless.yaml +++ b/.github/workflows/docker-publish-rootless.yaml @@ -4,25 +4,25 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml index ab226bdba..6b1b63e9d 100644 --- a/.github/workflows/docker-publish.yaml +++ b/.github/workflows/docker-publish.yaml @@ -4,25 +4,25 @@ on: schedule: - cron: '00 0 * * *' push: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "main" ] + branches: [ "main", "vnext" ] paths: - 'backend/**' - 'frontend/**' - 'Dockerfile' - 'Dockerfile.rootless' - '.dockerignore' - - '.github/workflows' + - '.github/workflows/**' env: # Use docker.io for Docker Hub if empty diff --git a/.github/workflows/partial-frontend.yaml b/.github/workflows/partial-frontend.yaml index f84940644..0786b9eb7 100644 --- a/.github/workflows/partial-frontend.yaml +++ b/.github/workflows/partial-frontend.yaml @@ -15,7 +15,7 @@ jobs: - uses: pnpm/action-setup@v3.0.0 with: - version: 6.0.2 + version: 9.12.2 - name: Install dependencies run: pnpm install --shamefully-hoist @@ -32,6 +32,20 @@ jobs: integration-tests: name: Integration Tests runs-on: ubuntu-latest + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: homebox + POSTGRES_PASSWORD: homebox + POSTGRES_DB: homebox + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout uses: actions/checkout@v4 @@ -54,7 +68,7 @@ jobs: - uses: pnpm/action-setup@v3.0.0 with: - version: 6.0.2 + version: 9.12.2 - name: Install dependencies run: pnpm install @@ -62,3 +76,53 @@ jobs: - name: Run Integration Tests run: task test:ci + integration-tests-pgsql: + strategy: + matrix: + database_version: [17,16,15] + name: Integration Tests PGSQL ${{ matrix.database_version }} + runs-on: ubuntu-latest + services: + postgres: + image: postgres:${{ matrix.database_version }} + env: + POSTGRES_USER: homebox + POSTGRES_PASSWORD: homebox + POSTGRES_DB: homebox + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Task + uses: arduino/setup-task@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.21" + + - uses: actions/setup-node@v4 + with: + node-version: 18 + + - uses: pnpm/action-setup@v3.0.0 + with: + version: 9.12.2 + + - name: Install dependencies + run: pnpm install + working-directory: frontend + + - name: Run Integration Tests + run: task test:ci:postgresql \ No newline at end of file diff --git a/.github/workflows/pull-requests.yaml b/.github/workflows/pull-requests.yaml index 61a148595..e94977acb 100644 --- a/.github/workflows/pull-requests.yaml +++ b/.github/workflows/pull-requests.yaml @@ -4,10 +4,12 @@ on: pull_request: branches: - main + - vnext paths: - 'backend/**' - 'frontend/**' + - '.github/workflows/**' jobs: backend-tests: diff --git a/.gitignore b/.gitignore index 51a835a44..0159273ca 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ backend/.data/* config.yml homebox.db +homebox.db-journal +homebox.db-shm +homebox.db-wal .idea .DS_Store test-mailer.json diff --git a/Taskfile.yml b/Taskfile.yml index 1fa662f80..41f140477 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,7 +2,8 @@ version: "3" env: HBOX_LOG_LEVEL: debug - HBOX_STORAGE_SQLITE_URL: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1 + HBOX_DATABASE_DRIVER: sqlite3 + HBOX_DATABASE_SQLITE_PATH: .data/homebox.db?_pragma=busy_timeout=1000&_pragma=journal_mode=WAL&_fk=1 HBOX_OPTIONS_ALLOW_REGISTRATION: true UNSAFE_DISABLE_PASSWORD_PROJECTION: "yes_i_am_sure" tasks: @@ -62,6 +63,23 @@ tasks: - go run ./app/api/ {{ .CLI_ARGS }} silent: false + go:run:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: localhost + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Starts the backend api server with postgresql (depends on generate task) + dir: backend + deps: + - generate + cmds: + - go run ./app/api/ {{ .CLI_ARGS }} + silent: false + go:test: desc: Runs all go tests using gotestsum - supports passing gotestsum args dir: backend @@ -116,6 +134,21 @@ tasks: cmds: - cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }} + db:migration:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: localhost + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Runs the database diff engine to generate a SQL migration files for postgresql + deps: + - db:generate + cmds: + - cd backend && go run app/tools/migrations/main.go {{ .CLI_ARGS }} + ui:watch: desc: Starts the vitest test runner in watch mode dir: frontend @@ -145,7 +178,24 @@ tasks: cmds: - cd backend && go build ./app/api - backend/api & - - sleep 5 + - sleep 10 + - cd frontend && pnpm run test:ci + silent: true + + test:ci:postgresql: + env: + HBOX_DATABASE_DRIVER: postgres + HBOX_DATABASE_USERNAME: homebox + HBOX_DATABASE_PASSWORD: homebox + HBOX_DATABASE_DATABASE: homebox + HBOX_DATABASE_HOST: 127.0.0.1 + HBOX_DATABASE_PORT: 5432 + HBOX_DATABASE_SSL_MODE: disable + desc: Runs end-to-end test on a live server with postgresql (only for use in CI) + cmds: + - cd backend && go build ./app/api + - backend/api & + - sleep 10 - cd frontend && pnpm run test:ci silent: true diff --git a/backend/app/api/main.go b/backend/app/api/main.go index db3e6716d..243eea325 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -6,9 +6,11 @@ import ( "encoding/json" "fmt" "github.com/shirou/gopsutil/v4/host" + "github.com/google/uuid" "net/http" "os" "path/filepath" + "strings" "time" atlas "ariga.io/atlas/sql/migrate" @@ -30,6 +32,7 @@ import ( "github.com/sysadminsmedia/homebox/backend/internal/sys/config" "github.com/sysadminsmedia/homebox/backend/internal/web/mid" + _ "github.com/lib/pq" _ "github.com/sysadminsmedia/homebox/backend/pkgs/cgofreesqlite" ) @@ -48,6 +51,19 @@ func build() string { return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime) } +func validatePostgresSSLMode(sslMode string) bool { + validModes := map[string]bool{ + "": true, + "disable": true, + "allow": true, + "prefer": true, + "require": true, + "verify-ca": true, + "verify-full": true, + } + return validModes[strings.ToLower(strings.TrimSpace(sslMode))] +} + // @title Homebox API // @version 1.0 // @description Track, Manage, and Organize your Things. @@ -128,13 +144,32 @@ func run(cfg *config.Config) error { log.Fatal().Err(err).Msg("failed to create data directory") } - c, err := ent.Open("sqlite3", cfg.Storage.SqliteURL) + if strings.ToLower(cfg.Database.Driver) == "postgres" { + if !validatePostgresSSLMode(cfg.Database.SslMode) { + log.Fatal().Str("sslmode", cfg.Database.SslMode).Msg("invalid sslmode") + } + } + + // Set up the database URL based on the driver because for some reason a common URL format is not used + databaseURL := "" + switch strings.ToLower(cfg.Database.Driver) { + case "sqlite3": + databaseURL = cfg.Database.SqlitePath + case "postgres": + databaseURL = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.Username, cfg.Database.Password, cfg.Database.Database, cfg.Database.SslMode) + default: + log.Fatal().Str("driver", cfg.Database.Driver).Msg("unsupported database driver") + } + + c, err := ent.Open(strings.ToLower(cfg.Database.Driver), databaseURL) if err != nil { log.Fatal(). Err(err). - Str("driver", "sqlite"). - Str("url", cfg.Storage.SqliteURL). - Msg("failed opening connection to sqlite") + Str("driver", strings.ToLower(cfg.Database.Driver)). + Str("host", cfg.Database.Host). + Str("port", cfg.Database.Port). + Str("database", cfg.Database.Database). + Msg("failed opening connection to {driver} database at {host}:{port}/{database}") } defer func(c *ent.Client) { err := c.Close() @@ -143,9 +178,14 @@ func run(cfg *config.Config) error { } }(c) - temp := filepath.Join(os.TempDir(), "migrations") + // Always create a random temporary directory for migrations + tempUUID, err := uuid.NewUUID() + if err != nil { + return err + } + temp := filepath.Join(os.TempDir(), fmt.Sprintf("homebox-%s", tempUUID.String())) - err = migrations.Write(temp) + err = migrations.Write(temp, cfg.Database.Driver) if err != nil { return err } @@ -165,17 +205,18 @@ func run(cfg *config.Config) error { if err != nil { log.Error(). Err(err). - Str("driver", "sqlite"). - Str("url", cfg.Storage.SqliteURL). + Str("driver", cfg.Database.Driver). + Str("url", databaseURL). Msg("failed creating schema resources") return err } - err = os.RemoveAll(temp) - if err != nil { - log.Error().Err(err).Msg("failed to remove temporary directory for database migrations") - return err - } + defer func() { + err := os.RemoveAll(temp) + if err != nil { + log.Error().Err(err).Msg("failed to remove temporary directory for database migrations") + } + }() collectFuncs := []currencies.CollectorFunc{ currencies.CollectDefaults(), diff --git a/backend/app/tools/migrations/main.go b/backend/app/tools/migrations/main.go index f0bd2c6d2..fd952cc8f 100644 --- a/backend/app/tools/migrations/main.go +++ b/backend/app/tools/migrations/main.go @@ -3,8 +3,11 @@ package main import ( "context" "fmt" + "github.com/sysadminsmedia/homebox/backend/internal/sys/config" "log" "os" + "path/filepath" + "strings" "github.com/sysadminsmedia/homebox/backend/internal/data/ent/migrate" @@ -12,13 +15,29 @@ import ( _ "ariga.io/atlas/sql/sqlite" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql/schema" + + _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) func main() { + cfg, err := config.New(build(), "Homebox inventory management system") + if err != nil { + panic(err) + } + sqlDialect := "" + switch strings.ToLower(cfg.Database.Driver) { + case "sqlite3": + sqlDialect = dialect.SQLite + case "postgres": + sqlDialect = dialect.Postgres + default: + log.Fatalf("unsupported database driver: %s", cfg.Database.Driver) + } ctx := context.Background() // Create a local migration directory able to understand Atlas migration file format for replay. - dir, err := atlas.NewLocalDir("internal/data/migrations/migrations") + safePath := filepath.Clean(fmt.Sprintf("internal/data/migrations/%s", sqlDialect)) + dir, err := atlas.NewLocalDir(safePath) if err != nil { log.Fatalf("failed creating atlas migration directory: %v", err) } @@ -26,7 +45,7 @@ func main() { opts := []schema.MigrateOption{ schema.WithDir(dir), // provide migration directory schema.WithMigrationMode(schema.ModeReplay), // provide migration mode - schema.WithDialect(dialect.SQLite), // Ent dialect to use + schema.WithDialect(sqlDialect), // Ent dialect to use schema.WithFormatter(atlas.DefaultFormatter), schema.WithDropIndex(true), schema.WithDropColumn(true), @@ -35,11 +54,55 @@ func main() { log.Fatalln("migration name is required. Use: 'go run -mod=mod ent/migrate/main.go '") } + if sqlDialect == dialect.Postgres { + if !validatePostgresSSLMode(cfg.Database.SslMode) { + log.Fatalf("invalid sslmode: %s", cfg.Database.SslMode) + } + } + + databaseURL := "" + switch { + case cfg.Database.Driver == "sqlite3": + databaseURL = fmt.Sprintf("sqlite://%s", cfg.Database.SqlitePath) + case cfg.Database.Driver == "postgres": + databaseURL = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Database, cfg.Database.SslMode) + default: + log.Fatalf("unsupported database driver: %s", cfg.Database.Driver) + } + // Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above). - err = migrate.NamedDiff(ctx, "sqlite://.data/homebox.migration.db?_fk=1", os.Args[1], opts...) + err = migrate.NamedDiff(ctx, databaseURL, os.Args[1], opts...) if err != nil { log.Fatalf("failed generating migration file: %v", err) } fmt.Println("Migration file generated successfully.") } + +var ( + version = "nightly" + commit = "HEAD" + buildTime = "now" +) + +func build() string { + short := commit + if len(short) > 7 { + short = short[:7] + } + + return fmt.Sprintf("%s, commit %s, built at %s", version, short, buildTime) +} + +func validatePostgresSSLMode(sslMode string) bool { + validModes := map[string]bool{ + "": true, + "disable": true, + "allow": true, + "prefer": true, + "require": true, + "verify-ca": true, + "verify-full": true, + } + return validModes[strings.ToLower(strings.TrimSpace(sslMode))] +} diff --git a/backend/go.mod b/backend/go.mod index a17839284..10a1a158d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/schema v1.4.1 github.com/hay-kot/httpkit v0.0.11 + github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.24 github.com/olahol/melody v1.2.1 github.com/pkg/errors v0.9.1 diff --git a/backend/go.sum b/backend/go.sum index abfe8b631..8f6ff61f9 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -106,6 +106,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= diff --git a/backend/internal/data/migrations/migrations.go b/backend/internal/data/migrations/migrations.go index a2afdc894..ae8852f48 100644 --- a/backend/internal/data/migrations/migrations.go +++ b/backend/internal/data/migrations/migrations.go @@ -3,23 +3,29 @@ package migrations import ( "embed" + "fmt" "os" "path" ) -//go:embed all:migrations +//go:embed all:sqlite3 all:postgres var Files embed.FS // Write writes the embedded migrations to a temporary directory. // It returns an error and a cleanup function. The cleanup function // should be called when the migrations are no longer needed. -func Write(temp string) error { +func Write(temp string, dialect string) error { + allowedDialects := map[string]bool{"sqlite3": true, "postgres": true} + if !allowedDialects[dialect] { + return fmt.Errorf("unsupported dialect: %s", dialect) + } + err := os.MkdirAll(temp, 0o755) if err != nil { return err } - fsDir, err := Files.ReadDir("migrations") + fsDir, err := Files.ReadDir(dialect) if err != nil { return err } @@ -29,7 +35,7 @@ func Write(temp string) error { continue } - b, err := Files.ReadFile(path.Join("migrations", f.Name())) + b, err := Files.ReadFile(path.Join(dialect, f.Name())) if err != nil { return err } diff --git a/backend/internal/data/migrations/postgres/20241027025146_init.sql b/backend/internal/data/migrations/postgres/20241027025146_init.sql new file mode 100644 index 000000000..a1ebcb708 --- /dev/null +++ b/backend/internal/data/migrations/postgres/20241027025146_init.sql @@ -0,0 +1,58 @@ +-- Create "groups" table +CREATE TABLE "groups" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "currency" character varying NOT NULL DEFAULT 'usd', PRIMARY KEY ("id")); +-- Create "documents" table +CREATE TABLE "documents" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "title" character varying NOT NULL, "path" character varying NOT NULL, "group_documents" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "documents_groups_documents" FOREIGN KEY ("group_documents") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "locations" table +CREATE TABLE "locations" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "group_locations" uuid NOT NULL, "location_children" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "locations_groups_locations" FOREIGN KEY ("group_locations") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "locations_locations_children" FOREIGN KEY ("location_children") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE SET NULL); +-- Create "items" table +CREATE TABLE "items" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "import_ref" character varying NULL, "notes" character varying NULL, "quantity" bigint NOT NULL DEFAULT 1, "insured" boolean NOT NULL DEFAULT false, "archived" boolean NOT NULL DEFAULT false, "asset_id" bigint NOT NULL DEFAULT 0, "serial_number" character varying NULL, "model_number" character varying NULL, "manufacturer" character varying NULL, "lifetime_warranty" boolean NOT NULL DEFAULT false, "warranty_expires" timestamptz NULL, "warranty_details" character varying NULL, "purchase_time" timestamptz NULL, "purchase_from" character varying NULL, "purchase_price" double precision NOT NULL DEFAULT 0, "sold_time" timestamptz NULL, "sold_to" character varying NULL, "sold_price" double precision NOT NULL DEFAULT 0, "sold_notes" character varying NULL, "group_items" uuid NOT NULL, "item_children" uuid NULL, "location_items" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "items_groups_items" FOREIGN KEY ("group_items") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "items_items_children" FOREIGN KEY ("item_children") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE SET NULL, CONSTRAINT "items_locations_items" FOREIGN KEY ("location_items") REFERENCES "locations" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "item_archived" to table: "items" +CREATE INDEX "item_archived" ON "items" ("archived"); +-- Create index "item_asset_id" to table: "items" +CREATE INDEX "item_asset_id" ON "items" ("asset_id"); +-- Create index "item_manufacturer" to table: "items" +CREATE INDEX "item_manufacturer" ON "items" ("manufacturer"); +-- Create index "item_model_number" to table: "items" +CREATE INDEX "item_model_number" ON "items" ("model_number"); +-- Create index "item_name" to table: "items" +CREATE INDEX "item_name" ON "items" ("name"); +-- Create index "item_serial_number" to table: "items" +CREATE INDEX "item_serial_number" ON "items" ("serial_number"); +-- Create "attachments" table +CREATE TABLE "attachments" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "type" character varying NOT NULL DEFAULT 'attachment', "primary" boolean NOT NULL DEFAULT false, "document_attachments" uuid NOT NULL, "item_attachments" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "attachments_documents_attachments" FOREIGN KEY ("document_attachments") REFERENCES "documents" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "attachments_items_attachments" FOREIGN KEY ("item_attachments") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "users" table +CREATE TABLE "users" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "is_superuser" boolean NOT NULL DEFAULT false, "superuser" boolean NOT NULL DEFAULT false, "role" character varying NOT NULL DEFAULT 'user', "activated_on" timestamptz NULL, "group_users" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "users_groups_users" FOREIGN KEY ("group_users") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "users_email_key" to table: "users" +CREATE UNIQUE INDEX "users_email_key" ON "users" ("email"); +-- Create "auth_tokens" table +CREATE TABLE "auth_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "user_auth_tokens" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "auth_tokens_users_auth_tokens" FOREIGN KEY ("user_auth_tokens") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "auth_tokens_token_key" to table: "auth_tokens" +CREATE UNIQUE INDEX "auth_tokens_token_key" ON "auth_tokens" ("token"); +-- Create index "authtokens_token" to table: "auth_tokens" +CREATE INDEX "authtokens_token" ON "auth_tokens" ("token"); +-- Create "auth_roles" table +CREATE TABLE "auth_roles" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "role" character varying NOT NULL DEFAULT 'user', "auth_tokens_roles" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "auth_roles_auth_tokens_roles" FOREIGN KEY ("auth_tokens_roles") REFERENCES "auth_tokens" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "auth_roles_auth_tokens_roles_key" to table: "auth_roles" +CREATE UNIQUE INDEX "auth_roles_auth_tokens_roles_key" ON "auth_roles" ("auth_tokens_roles"); +-- Create "group_invitation_tokens" table +CREATE TABLE "group_invitation_tokens" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "token" bytea NOT NULL, "expires_at" timestamptz NOT NULL, "uses" bigint NOT NULL DEFAULT 0, "group_invitation_tokens" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "group_invitation_tokens_groups_invitation_tokens" FOREIGN KEY ("group_invitation_tokens") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "group_invitation_tokens_token_key" to table: "group_invitation_tokens" +CREATE UNIQUE INDEX "group_invitation_tokens_token_key" ON "group_invitation_tokens" ("token"); +-- Create "item_fields" table +CREATE TABLE "item_fields" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "type" character varying NOT NULL, "text_value" character varying NULL, "number_value" bigint NULL, "boolean_value" boolean NOT NULL DEFAULT false, "time_value" timestamptz NOT NULL, "item_fields" uuid NULL, PRIMARY KEY ("id"), CONSTRAINT "item_fields_items_fields" FOREIGN KEY ("item_fields") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "labels" table +CREATE TABLE "labels" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "description" character varying NULL, "color" character varying NULL, "group_labels" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "labels_groups_labels" FOREIGN KEY ("group_labels") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "label_items" table +CREATE TABLE "label_items" ("label_id" uuid NOT NULL, "item_id" uuid NOT NULL, PRIMARY KEY ("label_id", "item_id"), CONSTRAINT "label_items_item_id" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "label_items_label_id" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "maintenance_entries" table +CREATE TABLE "maintenance_entries" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "date" timestamptz NULL, "scheduled_date" timestamptz NULL, "name" character varying NOT NULL, "description" character varying NULL, "cost" double precision NOT NULL DEFAULT 0, "item_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "maintenance_entries_items_maintenance_entries" FOREIGN KEY ("item_id") REFERENCES "items" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create "notifiers" table +CREATE TABLE "notifiers" ("id" uuid NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "name" character varying NOT NULL, "url" character varying NOT NULL, "is_active" boolean NOT NULL DEFAULT true, "group_id" uuid NOT NULL, "user_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "notifiers_groups_notifiers" FOREIGN KEY ("group_id") REFERENCES "groups" ("id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "notifiers_users_notifiers" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE); +-- Create index "notifier_group_id" to table: "notifiers" +CREATE INDEX "notifier_group_id" ON "notifiers" ("group_id"); +-- Create index "notifier_group_id_is_active" to table: "notifiers" +CREATE INDEX "notifier_group_id_is_active" ON "notifiers" ("group_id", "is_active"); +-- Create index "notifier_user_id" to table: "notifiers" +CREATE INDEX "notifier_user_id" ON "notifiers" ("user_id"); +-- Create index "notifier_user_id_is_active" to table: "notifiers" +CREATE INDEX "notifier_user_id_is_active" ON "notifiers" ("user_id", "is_active"); diff --git a/backend/internal/data/migrations/postgres/atlas.sum b/backend/internal/data/migrations/postgres/atlas.sum new file mode 100644 index 000000000..c74d1c00a --- /dev/null +++ b/backend/internal/data/migrations/postgres/atlas.sum @@ -0,0 +1,2 @@ +h1:3BgMK0p+hw7ygZFPnd9W6s3IzPNXa87zYGty2LeNjEg= +20241027025146_init.sql h1:PJhm+pjGRtFfgmGu7MwJo8+bVelVfU5LB+LZ/c8nnGE= diff --git a/backend/internal/data/migrations/migrations/20220929052825_init.sql b/backend/internal/data/migrations/sqlite3/20220929052825_init.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20220929052825_init.sql rename to backend/internal/data/migrations/sqlite3/20220929052825_init.sql diff --git a/backend/internal/data/migrations/migrations/20221001210956_group_invitations.sql b/backend/internal/data/migrations/sqlite3/20221001210956_group_invitations.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221001210956_group_invitations.sql rename to backend/internal/data/migrations/sqlite3/20221001210956_group_invitations.sql diff --git a/backend/internal/data/migrations/migrations/20221009173029_add_user_roles.sql b/backend/internal/data/migrations/sqlite3/20221009173029_add_user_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221009173029_add_user_roles.sql rename to backend/internal/data/migrations/sqlite3/20221009173029_add_user_roles.sql diff --git a/backend/internal/data/migrations/migrations/20221020043305_allow_nesting_types.sql b/backend/internal/data/migrations/sqlite3/20221020043305_allow_nesting_types.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221020043305_allow_nesting_types.sql rename to backend/internal/data/migrations/sqlite3/20221020043305_allow_nesting_types.sql diff --git a/backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql b/backend/internal/data/migrations/sqlite3/20221101041931_add_archived_field.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221101041931_add_archived_field.sql rename to backend/internal/data/migrations/sqlite3/20221101041931_add_archived_field.sql diff --git a/backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql b/backend/internal/data/migrations/sqlite3/20221113012312_add_asset_id_field.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221113012312_add_asset_id_field.sql rename to backend/internal/data/migrations/sqlite3/20221113012312_add_asset_id_field.sql diff --git a/backend/internal/data/migrations/migrations/20221203053132_add_token_roles.sql b/backend/internal/data/migrations/sqlite3/20221203053132_add_token_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221203053132_add_token_roles.sql rename to backend/internal/data/migrations/sqlite3/20221203053132_add_token_roles.sql diff --git a/backend/internal/data/migrations/migrations/20221205230404_drop_document_tokens.sql b/backend/internal/data/migrations/sqlite3/20221205230404_drop_document_tokens.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205230404_drop_document_tokens.sql rename to backend/internal/data/migrations/sqlite3/20221205230404_drop_document_tokens.sql diff --git a/backend/internal/data/migrations/migrations/20221205234214_add_maintenance_entries.sql b/backend/internal/data/migrations/sqlite3/20221205234214_add_maintenance_entries.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205234214_add_maintenance_entries.sql rename to backend/internal/data/migrations/sqlite3/20221205234214_add_maintenance_entries.sql diff --git a/backend/internal/data/migrations/migrations/20221205234812_cascade_delete_roles.sql b/backend/internal/data/migrations/sqlite3/20221205234812_cascade_delete_roles.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20221205234812_cascade_delete_roles.sql rename to backend/internal/data/migrations/sqlite3/20221205234812_cascade_delete_roles.sql diff --git a/backend/internal/data/migrations/migrations/20230227024134_add_scheduled_date.sql b/backend/internal/data/migrations/sqlite3/20230227024134_add_scheduled_date.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230227024134_add_scheduled_date.sql rename to backend/internal/data/migrations/sqlite3/20230227024134_add_scheduled_date.sql diff --git a/backend/internal/data/migrations/migrations/20230305065819_add_notifier_types.sql b/backend/internal/data/migrations/sqlite3/20230305065819_add_notifier_types.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230305065819_add_notifier_types.sql rename to backend/internal/data/migrations/sqlite3/20230305065819_add_notifier_types.sql diff --git a/backend/internal/data/migrations/migrations/20230305071524_add_group_id_to_notifiers.sql b/backend/internal/data/migrations/sqlite3/20230305071524_add_group_id_to_notifiers.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20230305071524_add_group_id_to_notifiers.sql rename to backend/internal/data/migrations/sqlite3/20230305071524_add_group_id_to_notifiers.sql diff --git a/backend/internal/data/migrations/migrations/20231006213457_add_primary_attachment_flag.sql b/backend/internal/data/migrations/sqlite3/20231006213457_add_primary_attachment_flag.sql similarity index 100% rename from backend/internal/data/migrations/migrations/20231006213457_add_primary_attachment_flag.sql rename to backend/internal/data/migrations/sqlite3/20231006213457_add_primary_attachment_flag.sql diff --git a/backend/internal/data/migrations/migrations/atlas.sum b/backend/internal/data/migrations/sqlite3/atlas.sum similarity index 100% rename from backend/internal/data/migrations/migrations/atlas.sum rename to backend/internal/data/migrations/sqlite3/atlas.sum diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 2498052e4..74d5d171b 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -161,16 +161,10 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, // Get the Totals for the Start and End of the Given Time Period q := ` SELECT - (SELECT Sum(purchase_price) - FROM items - WHERE group_items = ? - AND items.archived = false - AND items.created_at < ?) AS price_at_start, - (SELECT Sum(purchase_price) - FROM items - WHERE group_items = ? - AND items.archived = false - AND items.created_at < ?) AS price_at_end + SUM(CASE WHEN created_at < $1 THEN purchase_price ELSE 0 END) AS price_at_start, + SUM(CASE WHEN created_at < $2 THEN purchase_price ELSE 0 END) AS price_at_end + FROM items + WHERE group_items = $3 AND archived = false ` stats := ValueOverTime{ Start: start, @@ -180,7 +174,7 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, var maybeStart *float64 var maybeEnd *float64 - row := r.db.Sql().QueryRowContext(ctx, q, gid, sqliteDateFormat(start), gid, sqliteDateFormat(end)) + row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(start), sqliteDateFormat(end), gid) err := row.Scan(&maybeStart, &maybeEnd) if err != nil { return nil, err @@ -229,20 +223,25 @@ func (r *GroupRepository) StatsPurchasePrice(ctx context.Context, gid uuid.UUID, func (r *GroupRepository) StatsGroup(ctx context.Context, gid uuid.UUID) (GroupStatistics, error) { q := ` SELECT - (SELECT COUNT(*) FROM users WHERE group_users = ?) AS total_users, - (SELECT COUNT(*) FROM items WHERE group_items = ? AND items.archived = false) AS total_items, - (SELECT COUNT(*) FROM locations WHERE group_locations = ?) AS total_locations, - (SELECT COUNT(*) FROM labels WHERE group_labels = ?) AS total_labels, - (SELECT SUM(purchase_price*quantity) FROM items WHERE group_items = ? AND items.archived = false) AS total_item_price, - (SELECT COUNT(*) - FROM items - WHERE group_items = ? - AND items.archived = false - AND (items.lifetime_warranty = true OR items.warranty_expires > date()) - ) AS total_with_warranty + COUNT(DISTINCT u.id) as total_users, + COUNT(DISTINCT CASE WHEN i.archived = false THEN i.id END) as total_items, + COUNT(DISTINCT l.id) as total_locations, + COUNT(DISTINCT lb.id) as total_labels, + SUM(CASE WHEN i.archived = false THEN i.purchase_price * i.quantity ELSE 0 END) as total_item_price, + COUNT(DISTINCT CASE + WHEN i.archived = false + AND (i.lifetime_warranty = true OR i.warranty_expires > $1) + THEN i.id + END) as total_with_warranty +FROM groups g + LEFT JOIN users u ON u.group_users = g.id + LEFT JOIN items i ON i.group_items = g.id + LEFT JOIN locations l ON l.group_locations = g.id + LEFT JOIN labels lb ON lb.group_labels = g.id +WHERE g.id = $2; ` var stats GroupStatistics - row := r.db.Sql().QueryRowContext(ctx, q, gid, gid, gid, gid, gid, gid) + row := r.db.Sql().QueryRowContext(ctx, q, sqliteDateFormat(time.Now()), gid) var maybeTotalItemPrice *float64 var maybeTotalWithWarranty *int diff --git a/backend/internal/data/repo/repo_locations.go b/backend/internal/data/repo/repo_locations.go index 7ece91b20..d820a1663 100644 --- a/backend/internal/data/repo/repo_locations.go +++ b/backend/internal/data/repo/repo_locations.go @@ -121,7 +121,7 @@ func (r *LocationRepository) GetAll(ctx context.Context, gid uuid.UUID, filter L FROM locations WHERE - locations.group_locations = ? {{ FILTER_CHILDREN }} + locations.group_locations = $1 {{ FILTER_CHILDREN }} ORDER BY locations.name ASC ` @@ -278,8 +278,8 @@ func (r *LocationRepository) PathForLoc(ctx context.Context, gid, locID uuid.UUI query := `WITH RECURSIVE location_path AS ( SELECT id, name, location_children FROM locations - WHERE id = ? -- Replace ? with the ID of the item's location - AND group_locations = ? -- Replace ? with the ID of the group + WHERE id = $1 -- Replace ? with the ID of the item's location + AND group_locations = $2 -- Replace ? with the ID of the group UNION ALL @@ -332,7 +332,7 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue 'location' AS node_type FROM locations WHERE location_children IS NULL - AND group_locations = ? + AND group_locations = $1 UNION ALL SELECT c.id, @@ -355,10 +355,8 @@ func (r *LocationRepository) Tree(ctx context.Context, gid uuid.UUID, tq TreeQue SELECT * FROM location_tree - {{ WITH_ITEMS_FROM }} - ) tree ORDER BY node_type DESC, -- sort locations before items level, diff --git a/backend/internal/sys/config/conf.go b/backend/internal/sys/config/conf.go index 85d0ea3e1..856586f03 100644 --- a/backend/internal/sys/config/conf.go +++ b/backend/internal/sys/config/conf.go @@ -18,14 +18,15 @@ const ( type Config struct { conf.Version - Mode string `yaml:"mode" conf:"default:development"` // development or production - Web WebConfig `yaml:"web"` - Storage Storage `yaml:"storage"` - Log LoggerConf `yaml:"logger"` - Mailer MailerConf `yaml:"mailer"` - Demo bool `yaml:"demo"` - Debug DebugConf `yaml:"debug"` - Options Options `yaml:"options"` + Mode string `yaml:"mode" conf:"default:development"` // development or production + Web WebConfig `yaml:"web"` + Storage Storage `yaml:"storage"` + Database Database `yaml:"database"` + Log LoggerConf `yaml:"logger"` + Mailer MailerConf `yaml:"mailer"` + Demo bool `yaml:"demo"` + Debug DebugConf `yaml:"debug"` + Options Options `yaml:"options"` } type Options struct { diff --git a/backend/internal/sys/config/conf_database.go b/backend/internal/sys/config/conf_database.go index 2c6a7610c..06d44bfec 100644 --- a/backend/internal/sys/config/conf_database.go +++ b/backend/internal/sys/config/conf_database.go @@ -6,6 +6,16 @@ const ( type Storage struct { // Data is the path to the root directory - Data string `yaml:"data" conf:"default:./.data"` - SqliteURL string `yaml:"sqlite-url" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"` + Data string `yaml:"data" conf:"default:./.data"` +} + +type Database struct { + Driver string `yaml:"driver" conf:"default:sqlite3"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Host string `yaml:"host"` + Port string `yaml:"port"` + Database string `yaml:"database"` + SslMode string `yaml:"ssl_mode"` + SqlitePath string `yaml:"sqlite_path" conf:"default:./.data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1"` } diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 19535d27f..d12da9052 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -54,5 +54,11 @@ export default defineConfig({ { icon: 'github', link: 'https://git.homebox.software' }, { icon: 'mastodon', link: 'https://noc.social/@sysadminszone' }, ] + }, + + markdown: { + image: { + lazyLoading: true + } } }) diff --git a/docs/.vitepress/menus/en.mts b/docs/.vitepress/menus/en.mts index 582babe5f..0a12b3ad6 100644 --- a/docs/.vitepress/menus/en.mts +++ b/docs/.vitepress/menus/en.mts @@ -4,8 +4,9 @@ export default [ items: [ {text: 'Quick Start', link: '/en/quick-start'}, {text: 'Installation', link: '/en/installation'}, - {text: 'Organizing Your Items', link: '/en/organizing-items'}, {text: 'Configure Homebox', link: '/en/configure-homebox'}, + {text: 'Upgrade', link: '/en/upgrade'}, + {text: 'Organizing Your Items', link: '/en/organizing-items'}, {text: 'Tips and Tricks', link: '/en/tips-tricks'} ] }, diff --git a/docs/en/upgrade.md b/docs/en/upgrade.md new file mode 100644 index 000000000..2b7f34d3e --- /dev/null +++ b/docs/en/upgrade.md @@ -0,0 +1,13 @@ +# Upgrade + +## From v0.x.x to v1.0.0 + +::: danger Breaking Changes +This upgrade process involves some potentially breaking changes, please review this documentation carefully before beginning the upgrade process, and follow it closely during your upgrade. +::: + +### Configuration Changes +#### Database Configuration +- `HBOX_STORAGE_SQLITE_URL` has been replaced by `HBOX_DATABASE_SQLITE_PATH` +- `HBOX_DATABASE_DRIVER` has been added to set the database type, valid options are `sqlite3` and `postgres` +- `HBOX_DATABASE_HOST`, `HBOX_DATABASE_PORT`, `HBOX_DATABASE_USERNAME`, `HBOX_DATABASE_DATABASE`, and `HBOX_DATABASE_SSL_MODE` have been added to configure postgres connection options. \ No newline at end of file diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index e018b4b1c..a865e8ea1 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -26,8 +26,10 @@ module.exports = { "vue/no-setup-props-destructure": 0, "vue/no-multiple-template-root": 0, "vue/no-v-model-argument": 0, + "vue/no-v-html": 0, "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/ban-ts-comment": 0, + "tailwindcss/no-custom-classname": 0, "@typescript-eslint/no-unused-vars": [ "error", { diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css index 8a9d8fa72..185c47f5f 100644 --- a/frontend/assets/css/main.css +++ b/frontend/assets/css/main.css @@ -28,3 +28,61 @@ ::-webkit-scrollbar-thumb:hover { background-color: #9B9B9B; } + +.scroll-bg::-webkit-scrollbar { + width: 0.5rem; +} + +.scroll-bg::-webkit-scrollbar-thumb { + border-radius: 0.25rem; + @apply bg-base-300; +} + +.markdown > :first-child { + margin-top: 0px !important; +} + +.markdown :where(p, ul, ol, dl, blockquote, h1, h2, h3, h4, h5, h6) { + margin-top: var(--y-gap); + margin-bottom: var(--y-gap); +} + +.markdown :where(ul) { + list-style: disc; + margin-left: 2rem; +} + +.markdown :where(ol) { + list-style: decimal; + margin-left: 2rem; +} +/* Heading Styles */ +.markdown :where(h1) { + font-size: 2rem; + font-weight: 700; +} + +.markdown :where(h2) { + font-size: 1.5rem; + font-weight: 700; +} + +.markdown :where(h3) { + font-size: 1.25rem; + font-weight: 700; +} + +.markdown :where(h4) { + font-size: 1rem; + font-weight: 700; +} + +.markdown :where(h5) { + font-size: 0.875rem; + font-weight: 700; +} + +.markdown :where(h6) { + font-size: 0.75rem; + font-weight: 700; +} diff --git a/frontend/components/Base/SectionHeader.vue b/frontend/components/Base/SectionHeader.vue index 387d160fc..9f1a86f25 100644 --- a/frontend/components/Base/SectionHeader.vue +++ b/frontend/components/Base/SectionHeader.vue @@ -4,7 +4,6 @@ class="flex items-center text-3xl font-bold tracking-tight" :class="{ 'text-neutral-content': dark, - 'text-content': !dark, }" > diff --git a/frontend/components/Form/Autocomplete2.vue b/frontend/components/Form/Autocomplete2.vue index 92fef222a..552c5da5c 100644 --- a/frontend/components/Form/Autocomplete2.vue +++ b/frontend/components/Form/Autocomplete2.vue @@ -85,7 +85,10 @@ type Props = { label: string; modelValue: SupportValues | null | undefined; - items: string[] | object[]; + items: { + id: string; + treeString: string; + }[]; display?: string; multiple?: boolean; }; @@ -156,7 +159,7 @@ const matches = index.value.search("*" + search.value + "*"); - let resultIDs = [] + const resultIDs = []; for (let i = 0; i < matches.length; i++) { const match = matches[i]; const item = props.items[parseInt(match.ref)]; @@ -170,9 +173,11 @@ * Resolve the issue of language not being supported */ for (let i = 0; i < props.items.length; i++) { - const item = props.items[i] - if(resultIDs.find(item_ => item_ === item.id) != undefined){continue} - if(item.treeString.indexOf(search.value) > -1){ + const item = props.items[i]; + if (resultIDs.find(item_ => item_ === item.id) !== undefined) { + continue; + } + if (item.treeString.includes(search.value)) { const display = extractDisplay(item); list.push({ id: i, display, value: item }); } diff --git a/frontend/components/Form/TextArea.vue b/frontend/components/Form/TextArea.vue index f208b3274..87207415a 100644 --- a/frontend/components/Form/TextArea.vue +++ b/frontend/components/Form/TextArea.vue @@ -6,10 +6,10 @@ :class="{ 'text-red-600': typeof value === 'string' && - ((maxLength && value.length > maxLength) || (minLength && value.length < minLength)), + ((maxLength !== -1 && value.length > maxLength) || (minLength !== -1 && value.length < minLength)), }" > - {{ typeof value === "string" && (maxLength || minLength) ? `${value.length}/${maxLength}` : "" }} + {{ typeof value === "string" && (maxLength !== -1 || minLength !== -1) ? `${value.length}/${maxLength}` : "" }}