all repos — weimar @ 28e896519b8b6466fbb9a904b18002993c823f76

Unnamed repository; edit this file 'description' to name the repository.

feat: initial project scaffolding

Single-binary media repository for home servers — Go backend with
embedded SvelteKit SPA.

- Cobra CLI with viper config (TOML, env vars WEIMAR_*, flags)
- Commands: serve, users, image, version
- HTTP server with Go 1.22+ ServeMux, API route stubs, SPA fallback
- SvelteKit frontend with adapter-static (SPA mode, 5 routes)
- Build pipeline: Makefile, GoReleaser, UPX compression
- ldflags-based version and build date injection

💘 Generated with Crush

Assisted-by: Crush:deepseek-reasoner
Maxwell Jensen maxwelljensen@posteo.net
Wed, 13 May 2026 11:05:44 +0200
commit

28e896519b8b6466fbb9a904b18002993c823f76

A .gitignore

@@ -0,0 +1,35 @@

+# Binaries +/bin/ +/dist/ +/*.exe +/*.test + +# Config +weimar.toml + +# Data +/data/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Go +vendor/ + +# SvelteKit +web/build/ +web/.svelte-kit/ +web/node_modules/ + +# Embedded web build (copied by make) +cmd/weimar/web/build/ + +# Honk +.crush
A .golangci.yml

@@ -0,0 +1,16 @@

+version: "2" + +linters: + enable: + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - usetesting # reports uses of functions with replacement inside the testing package + - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative + - unused # checks for unused constants, variables, functions and types + - gocritic # provides diagnostics that check for bugs, performance and style issues + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - godoclint # checks Golang's documentation practice + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - recvcheck # checks for receiver type consistency + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
A .goreleaser.yml

@@ -0,0 +1,62 @@

+version: 2 + +before: + hooks: + - make web-embed + +builds: + - id: default + main: ./cmd/weimar + binary: weimar + env: + - CGO_ENABLED=0 + ldflags: + - -s -w -X main.Version={{.Version}} -X main.BuildDate={{.Date}} + goos: + - linux + # - windows + - darwin + goarch: + - arm64 + - amd64 + - "386" + dir: . + +archives: + - formats: [tar.gz] + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + #format_overrides: + # - goos: windows + # formats: [zip] + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +upx: + - # Templates: allowed. + enabled: true + + # Filter by build ID. + ids: [default] + + # Filter by GOOS. + goos: [linux, darwin] + + # Filter by GOARCH. + goarch: [amd64, arm64] + + # Compress argument. + # Valid options are from '1' (faster) to '9' (better), and 'best'. + compress: 9
A Makefile

@@ -0,0 +1,29 @@

+BIN := weimar +LDFLAGS := -ldflags="-s -w -X main.Version=$(VERSION) -X main.BuildDate=$(shell date -u +%Y-%m-%d)" + +VERSION ?= dev + +.PHONY: all build clean test lint web-build web-dev + +all: web-build build + +build: web-embed + go build -o bin/$(BIN) $(LDFLAGS) ./cmd/weimar + +clean: + rm -rf bin/ dist/ web/build/ web/.svelte-kit/ cmd/weimar/web/build/ + +test: + go test ./... -count=1 + +lint: + golangci-lint run ./... + +web-build: + cd web && bun run build + +web-embed: web-build + cp -r web/build cmd/weimar/web/build + +web-dev: + cd web && bun run dev
A cmd/weimar/image.go

@@ -0,0 +1,27 @@

+package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var imageCmd = &cobra.Command{ + Use: "image", + Short: "Manage images", +} + +var imageDeleteCmd = &cobra.Command{ + Use: "delete <id>", + Short: "Delete an image by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(cmd.OutOrStdout(), "not yet implemented") + return nil + }, +} + +func init() { + rootCmd.AddCommand(imageCmd) + imageCmd.AddCommand(imageDeleteCmd) +}
A cmd/weimar/main.go

@@ -0,0 +1,11 @@

+package main + +import ( + "os" +) + +func main() { + if err := Execute(); err != nil { + os.Exit(1) + } +}
A cmd/weimar/root.go

@@ -0,0 +1,80 @@

+package main + +import ( + "fmt" + "log/slog" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +var rootCmd = &cobra.Command{ + Use: "weimar", + Short: "Self-hosted media repository for small groups", + Long: `weimar is a single-binary media repository for home servers. +Store, tag, and browse memes and group-interest images with friends.`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return initConfig() + }, + SilenceUsage: true, + SilenceErrors: true, +} + +func Execute() error { + return rootCmd.Execute() +} + +func init() { + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "path to config file (default ./weimar.toml)") + rootCmd.PersistentFlags().String("log-level", "info", "log level (debug, info, warn, error)") + viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level")) +} + +func initConfig() error { + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + viper.AddConfigPath(".") + viper.SetConfigName("weimar") + viper.SetConfigType("toml") + } + + viper.SetEnvPrefix("WEIMAR") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return fmt.Errorf("reading config: %w", err) + } + } + + setDefaults() + + level := slog.LevelInfo + switch strings.ToLower(viper.GetString("log-level")) { + case "debug": + level = slog.LevelDebug + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + } + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))) + + return nil +} + +func setDefaults() { + viper.SetDefault("server.host", "0.0.0.0") + viper.SetDefault("server.port", 8080) + viper.SetDefault("database.path", "./data/weimar.db") + viper.SetDefault("storage.path", "./data/images") + viper.SetDefault("upload.max_size_mb", 50) + viper.SetDefault("upload.rate_limit_per_minute", 0) + viper.SetDefault("auth.allow_registration", true) +}
A cmd/weimar/serve.go

@@ -0,0 +1,61 @@

+package main + +import ( + "embed" + "fmt" + "io/fs" + "log/slog" + "os/signal" + "syscall" + + "github.com/mjensen/weimar/internal/config" + "github.com/mjensen/weimar/internal/server" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +//go:embed web/build/* +var webFS embed.FS + +var adminEmail string +var adminPassword string + +var serveCmd = &cobra.Command{ + Use: "serve", + Short: "Start the HTTP server", + RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.FromViper() + + if adminEmail != "" && adminPassword != "" { + slog.Info("first-run admin credentials provided", "email", adminEmail) + } + + webSub, err := fs.Sub(webFS, "web/build") + if err != nil { + return fmt.Errorf("reading embedded web build: %w", err) + } + + srv, err := server.New(cfg, webSub) + if err != nil { + return fmt.Errorf("creating server: %w", err) + } + + ctx, stop := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + slog.Info("starting weimar", "host", cfg.Server.Host, "port", cfg.Server.Port) + if err := srv.Start(ctx); err != nil { + return fmt.Errorf("server error: %w", err) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(serveCmd) + serveCmd.Flags().StringVar(&adminEmail, "admin-email", "", "admin email for first-run setup") + serveCmd.Flags().StringVar(&adminPassword, "admin-password", "", "admin password for first-run setup") + viper.BindPFlag("server.host", serveCmd.Flags().Lookup("host")) + viper.BindPFlag("server.port", serveCmd.Flags().Lookup("port")) +}
A cmd/weimar/users.go

@@ -0,0 +1,64 @@

+package main + +import ( + "fmt" + + "github.com/mjensen/weimar/internal/config" + "github.com/spf13/cobra" +) + +var usersCmd = &cobra.Command{ + Use: "users", + Short: "Manage users", +} + +var usersListCmd = &cobra.Command{ + Use: "list", + Short: "List all users", + RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.FromViper() + fmt.Fprintln(cmd.ErrOrStderr(), "users list (config path:", cfg.Database.Path, ")") + fmt.Fprintln(cmd.OutOrStdout(), "not yet implemented") + return nil + }, +} + +var usersCreateCmd = &cobra.Command{ + Use: "create <username> <password>", + Short: "Create a new user", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.FromViper() + fmt.Fprintln(cmd.ErrOrStderr(), "users create (config path:", cfg.Database.Path, ")") + fmt.Fprintln(cmd.OutOrStdout(), "not yet implemented") + return nil + }, +} + +var usersDeleteCmd = &cobra.Command{ + Use: "delete <username>", + Short: "Delete a user", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(cmd.OutOrStdout(), "not yet implemented") + return nil + }, +} + +var usersPasswordResetCmd = &cobra.Command{ + Use: "password-reset <username>", + Short: "Reset a user's password", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintln(cmd.OutOrStdout(), "not yet implemented") + return nil + }, +} + +func init() { + rootCmd.AddCommand(usersCmd) + usersCmd.AddCommand(usersListCmd) + usersCmd.AddCommand(usersCreateCmd) + usersCmd.AddCommand(usersDeleteCmd) + usersCmd.AddCommand(usersPasswordResetCmd) +}
A cmd/weimar/version.go

@@ -0,0 +1,25 @@

+package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + Version = "dev" + BuildDate = "unknown" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print version information", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Fprintf(cmd.OutOrStdout(), "weimar %s (built %s)\n", Version, BuildDate) + return nil + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +}
A docs/ARCHITECTURE.md

@@ -0,0 +1,300 @@

+# weimar Architecture + +> A single-binary media repository for home servers — memes, group interest images, and videos with simple tag-based discovery. +> Inspired by Chevereto V4 (`docs/CHEVERE.md`), scaled down for a small group on a single home server. + +--- + +## 1. Design Principles + +1. **Single binary** — Go backend with SvelteKit SPA (adapter-static) embedded via `embed.FS`. No Docker, no Node process, no external runtime. +2. **Zero external dependencies** at runtime (except optional `ffmpeg` for video thumbnails). +3. **Simple trumps scalable** — SQLite, filesystem storage, no queues, no caches. +4. **Authentication-first** — users are real accounts, not anonymous. +5. **CLI admin** — no web dashboard; admin operations are CLI commands. + +--- + +## 2. Tech Stack + +| Layer | Technology | +|-------|------------| +| **Backend** | Go (net/http, standard library + chi or similar router) | +| **Frontend** | SvelteKit (adapter-static, pre-rendered SPA) | +| **Database** | SQLite (via modernc.org/sqlite or mattn/go-sqlite3) | +| **Session store** | SQLite (same database) | +| **Image processing** | Go native (image/jpeg, image/png, image/gif, golang.org/x/image) | +| **Video thumbnails** | `ffmpeg` binary — optional, skipped if missing | +| **EXIF stripping** | Go native (exif library) | + +--- + +## 3. Directory Structure + +``` +weimar/ +├── main.go # Entry point: CLI flag parsing, serve +├── go.mod / go.sum +├── weimar.toml # User-provided config file +│ +├── cmd/ +│ ├── serve.go # weimar serve (HTTP server) +│ ├── users.go # weimar users (list/create/delete/password-reset) +│ └── image.go # weimar image delete +│ +├── internal/ +│ ├── server/ +│ │ ├── server.go # HTTP server, routes, embedded SPA +│ │ └── middleware.go # Auth middleware, CORS, session handling +│ ├── auth/ +│ │ ├── session.go # Server-side session (cookie-based, SQLite-backed) +│ │ └── register.go # Registration logic +│ ├── api/ +│ │ ├── upload.go # POST /api/upload +│ │ ├── images.go # GET /api/images, GET /api/images/{id} +│ │ ├── download.go # GET /api/images/{id}/download +│ │ ├── thumbnail.go # GET /api/images/{id}/thumbnail +│ │ ├── auth.go # POST /api/auth/login, /register +│ │ └── tags.go # GET /api/tags/suggest +│ ├── db/ +│ │ ├── sqlite.go # Database connection and migrations +│ │ ├── models.go # Structs: User, Image, Session, Tag +│ │ └── queries.go # SQL queries +│ ├── storage/ +│ │ ├── disk.go # File read/write/delete, hash-partitioned paths +│ │ └── processing.go # EXIF stripping, thumbnail generation +│ └── config/ +│ └── config.go # TOML config parsing +│ +├── web/ # SvelteKit SPA (built, embedded) +│ ├── src/ +│ │ ├── routes/ # SvelteKit pages (login, browse, upload, image view) +│ │ ├── lib/ +│ │ │ ├── api.ts # Fetch wrapper for API calls +│ │ │ └── types.ts # TypeScript interfaces +│ │ └── app.html +│ ├── svelte.config.js # adapter-static +│ └── package.json +│ +├── data/ +│ ├── images/ # Hash-partitioned image storage +│ │ └── ab/cd/ef/<hash>.<ext> +│ └── weimar.db # SQLite database +│ +└── docs/ + ├── CHEVERE.md # Chevereto V4 reference architecture + └── ARCHITECTURE.md # This file +``` + +--- + +## 4. Data Model + +### SQLite Tables + +**users** +| Column | Type | Notes | +|--------|------|-------| +| id | INTEGER PRIMARY KEY | | +| username | TEXT UNIQUE NOT NULL | | +| password_hash | TEXT NOT NULL | bcrypt | +| is_admin | INTEGER DEFAULT 0 | | +| created_at | TEXT (ISO 8601) | | +| updated_at | TEXT (ISO 8601) | | + +**sessions** +| Column | Type | Notes | +|--------|------|-------| +| id | INTEGER PRIMARY KEY | | +| user_id | INTEGER REFERENCES users(id) | | +| token | TEXT UNIQUE NOT NULL | Random 64-char hex | +| expires_at | TEXT (ISO 8601) | | +| created_at | TEXT (ISO 8601) | | + +**images** +| Column | Type | Notes | +|--------|------|-------| +| id | INTEGER PRIMARY KEY | | +| filename | TEXT NOT NULL | Original filename | +| storage_path | TEXT NOT NULL | Relative path in storage dir | +| thumbnail_path | TEXT | Relative path, nullable (no ffmpeg) | +| uploaded_by | INTEGER REFERENCES users(id) | | +| file_size | INTEGER | Bytes | +| width | INTEGER | | +| height | INTEGER | | +| mime_type | TEXT | e.g. image/jpeg, video/mp4 | +| created_at | TEXT (ISO 8601) | | +| updated_at | TEXT (ISO 8601) | | + +**image_tags** +| Column | Type | Notes | +|--------|------|-------| +| image_id | INTEGER REFERENCES images(id) | ON DELETE CASCADE | +| tag | TEXT NOT NULL | Lowercase, trimmed | + +**tags** (materialized for autocomplete) +| Column | Type | Notes | +|--------|------|-------| +| tag | TEXT PRIMARY KEY | Lowercase, unique | +| usage_count | INTEGER DEFAULT 0 | Denormalized for sort | + +--- + +## 5. API Endpoints + +| Method | Path | Auth | Description | +|--------|------|------|-------------| +| POST | `/api/auth/login` | No | Login, returns session cookie | +| POST | `/api/auth/register` | No* | Create account (*if registration open) | +| POST | `/api/upload` | Yes | Upload image/video + tags | +| GET | `/api/images` | Yes | Paginated listing, ?tags filter, newest-first | +| GET | `/api/images/{id}` | Yes | Single image metadata | +| GET | `/api/images/{id}/download` | Yes | Serve original file | +| GET | `/api/images/{id}/thumbnail` | Yes | Serve thumbnail | +| GET | `/api/tags/suggest?q=` | Yes | Tag autocomplete (LIKE query) | + +All authenticated endpoints use the session cookie (not Bearer tokens). + +--- + +## 6. Request Flow + +``` +Browser → SvelteKit SPA + ↓ fetch() + Go HTTP server + ↓ middleware + Auth check (session cookie validation) + ↓ + Route handler + ├─ db queries (SQLite) + ├─ storage ops (disk read/write) + └─ image processing (EXIF strip, thumbnail) + ↓ + JSON response → SPA renders +``` + +--- + +## 7. Admin CLI Commands + +``` +weimar serve --config /path/to/weimar.toml --admin-email user@example.com --admin-password s3cur3 +weimar users list --config /path/to/weimar.toml +weimar users create --config /path/to/weimar.toml <username> <password> +weimar users delete --config /path/to/weimar.toml <username> +weimar users password-reset --config /path/to/weimar.toml <username> +weimar image delete --config /path/to/weimar.toml <id> +``` + +All commands require `--config` (or `WEIMAR_CONFIG` env var). + +--- + +## 8. Image Storage + +### Path Structure +``` +{storage_path}/ab/cd/ef/<sha256_hex_prefix>.<ext> +``` +- First 6 hex chars of SHA-256 of file content → 3 levels of 2 chars +- Prevents thousands of files in a single directory + +### Processing Pipeline +1. Validate file type (magic bytes, not extension) +2. Check file size against config limit +3. Generate SHA-256 hash for storage path +4. Strip EXIF metadata (images only) +5. Generate thumbnail: + - Images: resize to configurable max dimension (default 300px) + - Video: `ffmpeg -i <input> -vframes 1 <output.jpg>` (skip if ffmpeg not found) +6. Write original + thumbnail to disk (hash-partitioned paths) +7. Insert metadata + tags into SQLite +8. Upsert tags into tag_usage table for autocomplete + +--- + +## 9. Configuration (TOML) + +```toml +[server] +host = "0.0.0.0" +port = 8080 + +[database] +path = "./data/weimar.db" + +[storage] +path = "./data/images" + +[upload] +max_size_mb = 50 +rate_limit_per_minute = 0 # 0 = unlimited + +[auth] +allow_registration = true # false = gated, admin creates users only +``` + +--- + +## 10. Session Management + +- **Login**: verify password → generate random 64-char hex token → store in `sessions` table → set `httpOnly` cookie named `weimar_session` +- **Auth check**: read cookie → look up token in SQLite → check `expires_at` → attach user context to request +- **Expiry**: sessions expire after 30 days of inactivity (reset on each request) +- **Logout**: delete session from DB + clear cookie + +--- + +## 11. Thumbnail Strategy + +- **Images**: server-side resize via Go `image` package (nearest-neighbour or bilinear). Store as JPEG. +- **Videos**: shell out to `ffmpeg -i {input} -vframes 1 -s 300x300 {output.jpg}`. If ffmpeg not found on `$PATH`, store `null` in `thumbnail_path`. SvelteKit handles missing thumbnail gracefully. +- **Format**: Thumbnails always stored as JPEG regardless of source format. + +--- + +## 12. Tag System + +- **Input**: user types comma-separated tag string on upload +- **Normalization**: lowercase, trimmed, whitespace-collapsed +- **Autocomplete**: `GET /api/tags/suggest?q=fun` → `SELECT tag FROM tags WHERE tag LIKE 'fun%' ORDER BY usage_count DESC LIMIT 10` +- **No collision resolution**: `funny-cat` and `funny_cat` and `funnycat` are distinct tags +- **Deletion**: when an image is deleted, decrement `usage_count` for its tags; remove tag row if count reaches 0 + +--- + +## 13. Key Design Decisions (vs Chevereto) + +| Concern | Chevereto V4 | weimar | +|---------|-------------|--------| +| Database | MySQL/MariaDB | SQLite | +| Deployment | Docker / VPS / cPanel | Single binary | +| Admin UI | Full web dashboard | CLI only | +| Multi-tenancy | Yes (Traefik-based SaaS) | No | +| Storage backends | Local + S3 + S3-compatible | Local filesystem only | +| Image sizes | Original + thumb + medium | Original + thumb only | +| Cache | Redis + file-based | None | +| Job queue | Yes (cron-driven) | No | +| Moderation | Flag-based + external services | None | +| Video thumbnails | php-ffmpeg (required) | ffmpeg (optional) | +| Mail | Symfony Mailer (15+ transports) | None | +| 2FA | Google2FA | None | +| Rate limiting | Flood protection | Configurable, off by default | +| API auth | JWT + API Keys + signed requests | Session cookie only | +| Language | 30+ languages | Single language (English) | + +--- + +## 14. Out of Scope (Future Considerations) + +- Search beyond tag filtering (full-text search) +- Albums / collections +- User profiles +- Comment system +- Federation (ActivityPub) +- S3/remote storage backends +- Multi-architecture builds (x86_64 only is fine) +- Prometheus metrics +- OAuth / social login +- Webhook notifications
A docs/CHEVERE.md

@@ -0,0 +1,645 @@

+# Chevereto V4 Architecture Reference + +> Repository: `github.com/chevereto/chevereto` (branch `4.5`) +> Latest release: **4.5.2** (Apr 21, 2026) +> License: AGPL-3.0 (Free edition) / Commercial proprietary (Lite/Pro) +> Author: Rodolfo Berrios (rodolfo@chevereto.com) + +--- + +## 1. Overview + +Chevereto is a **self-hosted media-sharing platform** (like Flickr/Imgur) that allows users to run their own image/video hosting website. It has been in development since 2007 and is currently on V4. The codebase is primarily PHP (83.7%) with JavaScript (10.4%) and CSS (5.6%). + +The project has **921+ stars**, **72+ forks**, and **4 contributors**. It supports multi-tenancy, multi-user, multi-language (30+ languages), and multiple storage backends. + +--- + +## 2. Tech Stack + +| Category | Technologies | +|----------|-------------| +| **Language** | PHP 8.2+ | +| **Framework** | Chevere (custom PSR-7/PSR-15 framework by same author) | +| **Frontend** | Vanilla JavaScript, CSS | +| **Database** | MySQL / MariaDB (PDO) | +| **Cache** | Redis (primary), file-based fallback (PSR-16) | +| **Image Processing** | Imagick, Intervention Image (`intervention/image` ^2.6) | +| **Video Processing** | php-ffmpeg (`php-ffmpeg/php-ffmpeg` ^1.2) | +| **Router** | Chevere Router (`chevere/router` ^0.9.1) | +| **HTTP** | Chevere HTTP (`chevere/http` ^0.7.1), Guzzle PSR-7 | +| **Auth** | Firebase JWT (`firebase/php-jwt` ^7.0), Google2FA | +| **Mail** | Symfony Mailer with 15+ transport integrations | +| **Storage** | AWS SDK S3 (`aws/aws-sdk-php` ^3.336.15), local filesystem | +| **EXIF** | php-exif, XMP Metadata Extractor | +| **QR Codes** | chillerlan/php-qrcode, Google2FA QR Code | +| **Encryption** | phpseclib/phpseclib ^3.0 | +| **Testing** | PHPUnit ^9.2, PHPStan ^1.11, Easy Coding Standard ^12.2 | +| **Refactoring** | Rector | +| **Debug** | PsySH ^0.11, XRdebug, Chevere var-dump | + +--- + +## 3. Directory Structure + +``` +chevereto/ +├── index.php # Entry point → delegates to app/ +├── .htaccess # Apache rewrite rules +├── LICENSE # AGPL-3.0 +│ +├── app/ # Application root +│ ├── composer.json # PHP dependencies +│ ├── composer.lock +│ ├── env-default.php # 100+ default env vars +│ ├── configurator.php # Config processor +│ ├── upgrading.php # Upgrade screen handler +│ ├── phpstan.neon # PHPStan configuration +│ ├── phpstan-bootstrap.php +│ ├── rector.php # Rector configuration +│ │ +│ ├── src/ # Modern PSR-4 code (Chevereto\ namespace) +│ │ ├── Config/ # Config singleton +│ │ ├── Encryption/ # Encrypt/decrypt functions +│ │ │ └── functions.php +│ │ ├── Http/ # HTTP layer +│ │ │ ├── Controllers/Api/V4/ # API V4 controllers (13+) +│ │ │ └── Middlewares/ # IP restrict, API key auth, signed req +│ │ ├── Legacy/ # Legacy systems (modern namespace) +│ │ │ ├── Classes/ # ~40 core domain classes +│ │ │ ├── G/ # Global helpers, Gettext, Handler +│ │ │ ├── functions.php +│ │ │ └── functions-render.php +│ │ ├── Tenants/ # Multi-tenant system +│ │ ├── Traits/Instance/ # Singleton trait +│ │ ├── Vars/ # Immutable superglobal wrappers +│ │ └── PCRE.php # PCRE regex enum +│ │ +│ ├── routes/ # Modern route definitions +│ │ ├── api-v4.php # Main API v4 routes +│ │ ├── api-v4-admin.php # Admin API routes +│ │ ├── api-v4-manager.php # Manager API routes +│ │ └── tenants-internal-api-v4.php # Internal tenant API +│ │ +│ ├── legacy/ # Legacy boot & app code +│ │ ├── entrypoints/ +│ │ │ └── index.php # Web entry point +│ │ ├── load/ # Bootstrap files +│ │ │ ├── php-boot.php +│ │ │ ├── app.php +│ │ │ ├── l10n.php +│ │ │ └── integrity-check.php +│ │ ├── routes/ # 30+ legacy route handlers +│ │ ├── commands/ # 19 CLI commands +│ │ └── install/ # Installer +│ │ +│ ├── schemas/ # Database schemas +│ │ ├── mysql-5/ +│ │ └── mysql-8/ +│ │ +│ ├── languages/ # 30+ i18n translation files +│ ├── bin/ # CLI entry points +│ │ ├── cli, legacy, repl, tenants +│ ├── tests/ # PHPUnit tests +│ ├── .cache/ +│ └── .psysh/ +│ +├── content/ # Web-accessible assets +│ ├── images/ +│ ├── legacy/ +│ └── pages/ +│ +├── images/ # Default media +├── importing/ # Bulk import scripts +├── sdk/ # Client-side SDK +│ ├── pup.js # Upload widget (full) +│ └── pup.min.js # Upload widget (minified) +│ +├── .github/ # GitHub Actions, templates +├── .package/ # Docker packaging +├── .ecs/ # Easy Coding Standard config +├── .vscode/ # Editor settings +└── .tinkerwell/ # Tinkerwell snippets +``` + +--- + +## 4. Architecture & Design Patterns + +### 4.1 Dual Codebase (Legacy + Modern Migration) + +The application is in active transition from legacy procedural code to modern PSR-4 namespaced code: + +- **Modern**: `app/src/` uses `Chevereto\` namespace, PSR-4 autoloading, proper DI via constructor injection, PHP 8 attributes +- **Legacy**: `app/legacy/` and `app/src/Legacy/` contain older procedural code being progressively migrated +- Both coexist via Composer autoload and a custom boot sequence + +The autoload configuration explicitly loads function files to bridge the two worlds: + +```json +"autoload": { + "files": [ + "src/Encryption/functions.php", + "src/Vars/functions.php", + "src/Legacy/functions.php", + "src/Legacy/functions-render.php", + "src/Legacy/G/functions.php", + "src/Legacy/G/functions-render.php", + "legacy/load/integrity-check.php", + "legacy/load/app.php", + "legacy/load/l10n.php" + ], + "psr-4": { + "Chevereto\\": "src/" + } +} +``` + +### 4.2 Request Lifecycle + +``` +index.php + └─→ app/legacy/entrypoints/index.php + ├─ Defines ACCESS = 'web' + ├─ Requires php-boot.php (framework bootstrap) + ├─ Parses URL path from REQUEST_URI + ├─ Checks for /upgrading route + └─ Calls loaderHandler() → bootstraps app + ├─ Loads environment config + ├─ Initializes database connection + ├─ Sets up cache + ├─ Boots routing system + └─ Dispatches to appropriate route handler +``` + +### 4.3 Environment Configuration + +All configuration is driven by `CHEVERETO_*` environment variables. Defaults are defined in `app/env-default.php` which returns an associative array of 100+ settings: + +- **Database**: `CHEVERETO_DB_*` (driver, host, name, user, pass, port, prefix, PDO attrs) +- **Cache**: `CHEVERETO_CACHE_*` (driver, host, port, password, key prefix, TTL, stampede) +- **Feature Flags**: `CHEVERETO_ENABLE_*` (~50 boolean toggles for features) +- **Limits**: `CHEVERETO_MAX_*` (file size, upload size, users, albums, tags, etc.) +- **Storage**: Local and S3-compatible +- **Encryption**: `CHEVERETO_ENCRYPTION_KEY` +- **Multi-tenant**: `CHEVERETO_ENABLE_TENANTS`, `CHEVERETO_TENANT_*` +- **External Services**: Akismet, ModerateContent, ProjectArachnid, StopForumSpam +- **Binary paths**: exiftool, exiftran, ffmpeg, ffprobe +- **Traefik/Proxy**: `CHEVERETO_HOSTNAME`, `CHEVERETO_SERVICE_NAME`, `CHEVERETO_SERVICE_PORT`, `CHEVERETO_PROXY_ENTRYPOINT` +- **Debug/Dev**: `CHEVERETO_DEBUG_LEVEL`, `CHEVERETO_ENABLE_DEBUG`, XRdebug integration + +### 4.4 Routing + +**Two routing systems coexist:** + +#### Modern (Chevere Router) — `app/routes/` + +Routes are defined using the Chevere Router DSL with middleware chains: + +```php +use function Chevere\Router\route; +use function Chevere\Router\routes; + +return routes( + route( + '/_/api/4/config/traefik', + GET: TenantsConfigTraefikGet::class, + ), +)->withAppendMiddleware( + RestrictIpAccess::with(allowList: '127.0.0.1,::1,172.16.0.0/12'), + TenantsApiKeyAuthorization::class, +); +``` + +Route files: +| File | Prefix | Purpose | +|------|--------|---------| +| `api-v4.php` | `/api/4/` | Main public API v4 | +| `api-v4-admin.php` | `/api/4/` | Admin endpoints (storages) | +| `api-v4-manager.php` | `/api/4/` | Manager endpoints (ban users) | +| `tenants-internal-api-v4.php` | `/_/api/4/` | Internal tenant management | + +#### Legacy Routing — `app/legacy/routes/` + +Uses `Chevereto\Legacy\G\Handler` class with path-based matching. Configurable route slugs are stored in the database settings: + +``` +Settings constants: + ROUTING_REGEX = '([\w_-]+)' + ROUTING_REGEX_PATH = '([\w\/_-]+)' +``` + +Configurable routes: `route_user` (→ `user`), `route_image` (→ `image`), `route_album` (→ `album`), `route_video` (→ `video`), `root_route` (→ `user`). + +Legacy routes support **virtual routing** — e.g., the video route maps to the image handler: + +```php +// app/legacy/routes/video.php +$route = 'image'; +virtualRouteHandleRedirect($route, ..., 'video'); +$handler->mapRoute($route); +``` + +30+ legacy route handler files: account, album, api, api-v1, captcha-verify, category, connect, dashboard, explore, following, image, importer-jobs, index, install, json, login, logout, moderate, oembed, page, plugin, redirect, search, settings, signup, tag, tag-autocomplete, update, upload, user, user-albums, video, webmanifest. + +### 4.5 Attribute-based Controllers (PHP 8 Attributes) + +Controllers use PHP 8 attributes for HTTP metadata: + +```php +#[Response( + new Status(200), + new Header('Content-Type', 'application/json'), +)] +class TenantsGet extends Controller +{ + public function __construct( + private Tenants $tenants, + ) {} + + public function __invoke(): array + { + return $this->tenants->getTenants(); + } +} +``` + +### 4.6 HTTP Middleware Layer + +Located in `app/src/Http/Middlewares/`: + +- **`RestrictIpAccess.php`** — IP allow/deny list enforcement (used on internal tenant API) +- **`SignedRequest.php`** — Validates signed request headers for internal communication +- **`TenantsApiKeyAuthorization.php`** — API key-based authorization for tenant management +- **`Cors.php`** — CORS header handling + +Middlewares can be applied per-route-group via the Router's `withAppendMiddleware()` method. + +### 4.7 Multi-Tenant System + +Enabled via `CHEVERETO_ENABLE_TENANTS`. Built for SaaS deployment with first-class support: + +#### Core Classes (`app/src/Tenants/`) +| Class | Purpose | +|-------|---------| +| `Tenant.php` | Single tenant entity (hostname, limits, service config) | +| `Tenants.php` | Repository/collection — fetches tenants from DB | +| `TenantsConfig.php` | Generates **Traefik** reverse proxy config dynamically | +| `TenantsApiKey.php` | API key model for tenant auth | +| `TenantPlan.php` | Tenant plan (pricing tier with limits) | + +#### Traefik Integration +The system dynamically generates Traefik configuration so each tenant gets its own virtual host: + +```php +// TenantsConfig generates: +[ + 'http' => [ + 'routers' => [ + 'tenant-{id}' => [ + 'rule' => "Host(`{hostname}`)", + 'service' => '{service}', + 'entryPoints' => ['{entryPoint}'], + 'middlewares' => [...], + ], + ], + 'services' => [ + '{service}' => [ + 'loadBalancer' => [ + 'servers' => [['url' => "http://{service}:{port}"]], + 'passHostHeader' => true, + ], + ], + ], + ], +] +``` + +#### Tenant API Endpoints +All under `Chevereto\Http\Controllers\Api\V4\`: + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/_/api/4/config/traefik` | GET | Generate Traefik proxy config | +| `/_/api/4/tenants` | GET | List all tenants | +| `/_/api/4/tenants` | POST | Create tenant | +| `/_/api/4/tenants/{id}` | GET | Get single tenant | +| `/_/api/4/tenants/{id}` | PATCH | Update tenant | +| `/_/api/4/tenants/{id}` | DELETE | Delete tenant | +| `/_/api/4/tenants/plans` | GET | List all plans | +| `/_/api/4/tenants/plans` | POST | Create plan | +| `/_/api/4/tenants/plans/{id}` | GET | Get plan | +| `/_/api/4/tenants/plans/{id}` | PATCH | Update plan | +| `/_/api/4/tenants/plans/{id}` | DELETE | Delete plan | +| `/_/api/4/tenants/auth/verify` | POST | Verify tenant auth | +| `/_/api/4/tenants/{id}/user/{id}/password` | PATCH | Reset user password | + +### 4.8 Immutable Superglobal Wrappers (Vars namespace) + +Superglobals are wrapped as **immutable value objects** using an `ImmutableMapTrait`: + +| Class | Source | Mutable? | +|-------|--------|----------| +| `EnvVar` | `$_ENV` | Immutable | +| `GetVar` | `$_GET` | Immutable | +| `PostVar` | `$_POST` | Immutable | +| `FilesVar` | `$_FILES` | Immutable | +| `CookieVar` | `$_COOKIE` | Mutable (for setting cookies) | + +Backed by helper functions in `app/src/Vars/functions.php` (e.g., `env()`). + +### 4.9 Config Singleton + +`Chevereto\Config\Config` uses the `AssertStaticInstanceTrait` to enforce a single application configuration instance. Loaded from environment variables via `app/configurator.php` and `app/env-default.php`. + +### 4.10 Website Modes: Community vs Personal + +- **Community mode** (multi-user, default): Full social features — followers, likes, signups, guest uploads, user routing +- **Personal mode** (single profile): Disables followers, likes, signups, guest uploads, user routing; maps a single user to a custom route path + +--- + +## 5. Legacy Domain Classes + +Located in `app/src/Legacy/Classes/` — ~40 classes forming the core domain model: + +| Class | Purpose | +|-------|---------| +| `Album.php` | Album CRUD and management | +| `ApiKey.php` | API key authentication | +| `AssetStorage.php` | Storage for CSS/JS/images with S3 settings | +| `Cache.php` | Cache abstraction layer | +| `Categories.php` / `Category.php` | Category CRUD | +| `Confirmation.php` | Email/account confirmations | +| `DB.php` | Database abstraction (PDO wrapper) | +| `ExecutableBinary.php` | Checks binary paths/config | +| `ExifTool.php` / `ExifTran.php` | EXIF processing via external tools | +| `Follow.php` | User follow relationships | +| `Image.php` | Image CRUD and metadata | +| `ImageConvert.php` / `ImageResize.php` | Image format conversion and resizing | +| `Import.php` / `ImporterFilterIterator.php` | Bulk importing from filesystem | +| `IpBan.php` | IP ban list enforcement | +| `KeyValue.php` / `KeyValueInterface.php` / `KeyValueNull.php` | Key-value store (Null Object pattern) | +| `L10n.php` | Locale and localization handling | +| `Listing.php` | Paginated data listings | +| `LocalStorage.php` / `Storage.php` / `StorageApis.php` | Storage backend abstraction | +| `Lock.php` | Mutex/locking mechanism | +| `Login.php` | Authentication handling | +| `Notification.php` | Notification system | +| `Page.php` | Custom pages | +| `Palettes.php` | Color palette extraction from images | +| `ProjectArachnid.php` | CSAM detection via Project Arachnid | +| `Queue.php` | Background job queue | +| `RequestLog.php` | Request logging | +| `Search.php` | Search functionality | +| `Settings.php` | Application settings (DB-persisted) | +| `Stat.php` | Statistics tracking | +| `Tag.php` / `Tags.php` | Tag CRUD | +| `TwoFactor.php` | 2FA authentication | +| `Upload.php` / `Uploads.php` | Upload handling and processing | +| `User.php` | User CRUD | +| `Variable.php` | Variable handling | +| `XmpMetadataExtractor.php` | XMP metadata extraction | + +--- + +## 6. Global Helper Functions + +`app/src/Legacy/G/` provides globally-available functions loaded via Composer's `files` autoload: + +- **`functions.php`** — Sanitization, formatting, routing helpers, URL building (`get_base_url()`, `get_sorted_url()`), date formatting, string manipulation +- **`functions-render.php`** — Template rendering helpers, HTML generation +- **`Gettext.php`** — Gettext i18n wrapper class +- **`Handler.php`** — Legacy request handler (route dispatch, settings, user context) + +--- + +## 7. Database Schema + +Schemas stored as SQL files in `app/schemas/`: +- `mysql-5/` — MySQL 5.x compatible +- `mysql-8/` — MySQL 8.x compatible (with newer features) + +Database migration is handled via CLI command `database-migrate.php`. + +**Table prefix**: Configurable via `CHEVERETO_DB_TABLE_PREFIX` (default: `chv_`). + +Key domain tables (inferred from classes and schema): users, images, albums, categories, tags, storage_apis, api_keys, pages, notifications, stats, ip_bans, follows, queues, request_log, settings, confirmations, tenants, tenant_plans. + +--- + +## 8. Image & Video Processing Pipeline + +**Image formats supported**: AVIF, JPEG, PNG, BMP, GIF, WEBP + +**Processing flow**: +1. Upload received → validated (size, type, flood/rate check) +2. EXIF data extracted (ExifTool or PHP exif via `lychee-org/php-exif`) +3. XMP metadata extracted (via `XmpMetadataExtractor`) +4. Image converted/resized as needed (Intervention Image + Imagick) +5. Color palette extracted (`Palettes` class) +6. Watermark applied (if `CHEVERETO_ENABLE_UPLOAD_WATERMARK` is enabled) +7. Thumbnails generated (multiple sizes) +8. Stored to configured storage backend (local or S3) +9. Metadata saved to database + +**External binary dependencies** (configurable paths): exiftool, exiftran, ffmpeg, ffprobe + +--- + +## 9. Storage Backends + +### Architecture +Strategy pattern with three main classes: + +- **`Storage`** (`Chevereto\Legacy\Classes\Storage`) — Main abstraction with static methods for upload/delete +- **`AssetStorage`** — Asset-specific storage (CSS, JS) with S3-compatible settings +- **`LocalStorage`** — Local filesystem storage + +### Backend Types +1. **Local filesystem** — default, stores in `PATH_PUBLIC` +2. **AWS S3** — via `aws/aws-sdk-php ^3.336.15` (configurable: region, bucket, key, secret, endpoint, `use_path_style_endpoint`) +3. **S3-compatible** — Any S3 API provider (DigitalOcean Spaces, MinIO, etc.) + +### Storage Settings (DB-persisted) +Per-storage config: `account_id`, `account_name`, `api_id`, `bucket`, `key`, `region`, `secret`, `server`, `service`, `url`, `use_path_style_endpoint`, `is_https`, `is_active`. + +### CDN Support +CDN delivery via external URLs, toggled by `CHEVERETO_ENABLE_CDN`. + +--- + +## 10. Cache Architecture + +- **Primary driver**: Redis (via `CHEVERETO_CACHE_DRIVER`) +- **Fallback**: File-based cache (Scrapbook library) +- **PSR compliance**: PSR-16 (cache via `chevere/cache`) +- **Configuration**: Host, port, password, key prefix (`chv:` default), TTL (max 86400s), stampede protection +- **CLI commands**: `cache-flush.php`, `cache-view.php` + +--- + +## 11. Security & Authentication + +| Layer | Implementation | +|-------|----------------| +| **Password hashing** | PHP native `password_hash()` / `password_verify()` | +| **2FA** | Google2FA (`pragmarx/google2fa`) with QR codes | +| **JWT** | Firebase php-jwt (`firebase/php-jwt ^7.0`) for API auth | +| **API Keys** | Database-backed API key authentication | +| **IP Bans** | IP-based access control (`IpBan` class + middleware) | +| **Rate limiting** | Upload flood protection | +| **Signed requests** | For internal/tenant API communication | +| **Router secret** | Internal routing verification | +| **External anti-abuse** | Akismet (spam), ProjectArachnid (CSAM), StopForumSpam, ModerateContent | +| **CORS** | Via middleware | + +--- + +## 12. Mail System + +Uses Symfony Mailer with DSN-based transport switching. 15+ integrations all configured via `CHEVERETO_MAILER_DSN` environment variable: + +| Supported Transports | +|----------------------| +| Amazon SES, AhaSend, Azure, Brevo, Infobip, Mailgun, Mailjet, Mailomat, MailPace, MailerSend, Mailtrap, Mailchimp, Microsoft Graph, Postal, Postmark, Resend, Scaleway, SendGrid, Sweego | + +--- + +## 13. Encryption System + +Located in `app/src/Encryption/` (`Chevereto\Encryption\Key` namespace): + +- Encrypts/decrypts sensitive settings (secrets) before persisting to DB +- `CHEVERETO_ENCRYPTION_KEY` env var provides the master key +- CLI commands: `encrypt-secrets.php`, `decrypt-secrets.php` +- Uses `phpseclib/phpseclib ^3.0` for cryptography primitives + +--- + +## 14. CLI System + +Three CLI entry points in `app/bin/`: +- **`cli`** — Modern CLI interface (tenant-aware) +- **`legacy`** — Legacy CLI interface +- **`repl`** — PsySH interactive shell +- **`tenants`** — Tenant management CLI + +### CLI Commands (`app/legacy/commands/`) — 19 commands: + +| Command | Purpose | +|---------|---------| +| `bulk-importer.php` | Import images from filesystem | +| `cache-flush.php` | Flush all cache | +| `cache-view.php` | View cache contents | +| `cipher.php` | Encryption/decryption tool | +| `cron.php` | Scheduled tasks runner | +| `database-migrate.php` | Run database migrations | +| `decrypt-secrets.php` | Decrypt sensitive settings | +| `encrypt-secrets.php` | Encrypt sensitive settings | +| `htaccess-checksum.php` | Verify .htaccess integrity | +| `htaccess-enforce.php` | Enforce .htaccess rules | +| `install.php` | Initial installation | +| `js.php` | JavaScript asset management | +| `langs.php` | Language file management | +| `password-reset.php` | Reset user password | +| `setting-get.php` | Get a setting value | +| `setting-update.php` | Update a setting | +| `stats.php` | View statistics | +| `stats-rebuild.php` | Rebuild statistics | +| `version.php` | Show version | +| `version-installed.php` | Show installed version | + +--- + +## 15. Feature Flags (Key Toggles) + +From `env-default.php`: + +| Variable | Default | Purpose | +|----------|---------|---------| +| `CHEVERETO_ENABLE_API_GUEST` | `false` | Anonymous API access | +| `CHEVERETO_ENABLE_API_USER` | `true` | User API access | +| `CHEVERETO_ENABLE_BANNERS` | `false` | Advertising banners | +| `CHEVERETO_ENABLE_BULK_IMPORTER` | `false` | Bulk import tool | +| `CHEVERETO_ENABLE_CAPTCHA` | `true` | CAPTCHA protection | +| `CHEVERETO_ENABLE_CDN` | `false` | CDN content delivery | +| `CHEVERETO_ENABLE_COOKIE_COMPLIANCE` | `false` | GDPR cookie notice | +| `CHEVERETO_ENABLE_FOLLOWERS` | `true` | User follow system | +| `CHEVERETO_ENABLE_LIKES` | `true` | Like system | +| `CHEVERETO_ENABLE_MODERATION` | `false` | Content moderation | +| `CHEVERETO_ENABLE_NOTIFICATIONS` | `true` | Notification system | +| `CHEVERETO_ENABLE_TENANTS` | `false` | Multi-tenant mode | +| `CHEVERETO_ENABLE_USERS` | `true` | User registration/login | +| `CHEVERETO_ENABLE_UPLOAD_URL` | `false` | URL-based uploads | +| `CHEVERETO_ENABLE_UPLOAD_WATERMARK` | `false` | Image watermarking | + +--- + +## 16. Editions (Licensing) + +- **Free** (AGPL-3.0): Core features, community support +- **Lite**: Additional features, paid license +- **Pro**: All features, commercial license + +Feature gating in the dashboard UI uses helpers like `badgePaid('lite')` and `inputDisabledPaid('lite')` to conditionally show/hide or disable premium features. Edition tracking lives in `app/.editions`. + +--- + +## 17. Dev Tools & Quality + +| Tool | Purpose | Config | +|------|---------|--------| +| **PHPStan** | Static analysis (level 5+) | `app/phpstan.neon` | +| **ECS (Easy Coding Standard)** | Code style | `.ecs/` directory | +| **Rector** | Automated refactoring | `app/rector.php` | +| **PHPUnit** | Unit/integration tests | `app/phpunit-report.xml` | +| **XRdebug** | Debug toolbar | `CHEVERETO_XRDEBUG_*` env vars | +| **PsySH** | Interactive debug shell | `app/.psysh/` | +| **Tinkerwell** | Code snippets | `.tinkerwell/` | + +--- + +## 18. Docker & Deployment + +- **Docker packaging** in `.package/` directory +- **Docker image** via GitHub Packages (`ghcr.io/chevereto/chevereto`) +- **Multi-arch**: x86_64 and arm64 +- **Docker Compose** via [chevereto/docker](https://github.com/chevereto/docker) repo +- **Deployment options**: Pure Docker, Chevereto Docker, VPS, cPanel, Plesk, Installatron, Softaculous, SwiftWave, Cloudron + +--- + +## 19. API Architecture Summary + +### API V4 (Modern) + +| Route Group | Path Prefix | Auth | Routes File | +|-------------|-------------|------|-------------| +| User API | `/api/4/` | JWT/API Key | `api-v4.php` | +| Admin API | `/api/4/` | JWT + admin role | `api-v4-admin.php` | +| Manager API | `/api/4/` | JWT + manager role | `api-v4-manager.php` | +| Internal Tenant | `/_/api/4/` | IP allow + API Key | `tenants-internal-api-v4.php` | + +### Legacy API +- Legacy JSON endpoint accessible via `get_base_url('json')` +- Used by admin dashboard for CRUD operations on storages + +--- + +## 20. Key Architecture Decisions + +1. **Env-var driven config**: All settings via environment variables, no config files to edit +2. **Feature flags everywhere**: Toggle any capability on/off without code changes +3. **Dual routing**: Modern Chevere Router for API V4, legacy routing for web pages +4. **Progressive migration**: Old code lives in `Legacy/` namespaces, being gradually refactored to modern PSR-4 +5. **PSR compliance**: PSR-4 (autoload), PSR-7 (HTTP messages), PSR-15 (HTTP handlers), PSR-16 (cache) +6. **Multi-tenant by design**: From database schema to API layer to reverse proxy config +7. **Storage abstraction**: Strategy pattern for local vs S3 vs S3-compatible backends +8. **Mailer abstraction**: Symfony Mailer with DSN-based transport switching +9. **Security layers**: JWT + 2FA + API keys + IP bans + signed requests + rate limiting +10. **Chevere framework**: Custom PHP framework by same author — attribute-based controllers, immutable superglobal wrappers, router with middleware chains +11. **Singleton pattern** for application config (`Config` class) +12. **Null Object pattern** for optional caching (`KeyValueNull`) +13. **Adaptor pattern** for reverse proxy config generation (`TenantsConfig` → Traefik) +14. **Immutable value objects** for superglobals (`EnvVar`, `GetVar`, `PostVar`, etc.)
A go.mod

@@ -0,0 +1,24 @@

+module github.com/mjensen/weimar + +go 1.26.2 + +require ( + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.21.0 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.28.0 // indirect +)
A go.sum

@@ -0,0 +1,54 @@

+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A internal/config/config.go

@@ -0,0 +1,55 @@

+package config + +import "github.com/spf13/viper" + +type Config struct { + Server ServerConfig + Database DatabaseConfig + Storage StorageConfig + Upload UploadConfig + Auth AuthConfig +} + +type ServerConfig struct { + Host string + Port int +} + +type DatabaseConfig struct { + Path string +} + +type StorageConfig struct { + Path string +} + +type UploadConfig struct { + MaxSizeMB int `mapstructure:"max_size_mb"` + RateLimitPerMinute int `mapstructure:"rate_limit_per_minute"` +} + +type AuthConfig struct { + AllowRegistration bool `mapstructure:"allow_registration"` +} + +func FromViper() Config { + return Config{ + Server: ServerConfig{ + Host: viper.GetString("server.host"), + Port: viper.GetInt("server.port"), + }, + Database: DatabaseConfig{ + Path: viper.GetString("database.path"), + }, + Storage: StorageConfig{ + Path: viper.GetString("storage.path"), + }, + Upload: UploadConfig{ + MaxSizeMB: viper.GetInt("upload.max_size_mb"), + RateLimitPerMinute: viper.GetInt("upload.rate_limit_per_minute"), + }, + Auth: AuthConfig{ + AllowRegistration: viper.GetBool("auth.allow_registration"), + }, + } +}
A internal/server/server.go

@@ -0,0 +1,79 @@

+package server + +import ( + "context" + "io/fs" + "net/http" + + "github.com/mjensen/weimar/internal/config" +) + +func New(cfg config.Config, webFS fs.FS) (*Server, error) { + mux := http.NewServeMux() + + // API routes + mux.HandleFunc("GET /api/health", handleHealth) + + // Auth + mux.HandleFunc("POST /api/auth/login", handleNotImplemented) + mux.HandleFunc("POST /api/auth/register", handleNotImplemented) + + // Images + mux.HandleFunc("POST /api/upload", handleNotImplemented) + mux.HandleFunc("GET /api/images", handleNotImplemented) + mux.HandleFunc("GET /api/images/{id}", handleNotImplemented) + mux.HandleFunc("GET /api/images/{id}/download", handleNotImplemented) + mux.HandleFunc("GET /api/images/{id}/thumbnail", handleNotImplemented) + + // Tags + mux.HandleFunc("GET /api/tags/suggest", handleNotImplemented) + + // SPA — serve index.html for all non-API, non-file routes + spaFS := http.FileServer(http.FS(webFS)) + mux.Handle("/", spaFS) + + return &Server{ + cfg: cfg, + mux: mux, + }, nil +} + +type Server struct { + cfg config.Config + mux *http.ServeMux +} + +func (s *Server) Start(ctx context.Context) error { + addr := s.cfg.Server.Host + ":" + itoa(s.cfg.Server.Port) + srv := &http.Server{Addr: addr, Handler: s.mux} + + go func() { + <-ctx.Done() + srv.Shutdown(context.Background()) + }() + + return srv.ListenAndServe() +} + +func itoa(n int) string { + if n == 0 { + return "0" + } + var buf [20]byte + i := len(buf) + for n > 0 { + i-- + buf[i] = byte('0' + n%10) + n /= 10 + } + return string(buf[i:]) +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"ok"}`)) +} + +func handleNotImplemented(w http.ResponseWriter, r *http.Request) { + http.Error(w, "not implemented", http.StatusNotImplemented) +}
A web/.gitignore

@@ -0,0 +1,3 @@

+/build/ +/.svelte-kit/ +/node_modules/
A web/bun.lock

@@ -0,0 +1,220 @@

+{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "weimar-web", + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.20.7", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "svelte": "^5.28.2", + "typescript": "^5.8.3", + "vite": "^6.3.5", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.3", "", { "os": "android", "cpu": "arm64" }, "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.3", "", { "os": "none", "cpu": "arm64" }, "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="], + + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.59.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.4", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-d8OON70AphLdDesuTIl//M2O6fRTIicX8aYv8vhCiYEhTTI2OboKqey0Hu1A4VFhqwgqtq0vKDmPFGkw8kKmgw=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "devalue": ["devalue@5.8.0", "", {}, "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.2.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-MPweq2EvEGj8jwOI7Hgycw/QIHzqA1EbAM8lG7p+FBfZbZq/hQ6h3AMsqnu/djzisH1KVWNzbb7LSgIVtMlPSg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], + + "rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "svelte": ["svelte@5.55.5", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw=="], + + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + + "rollup/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + } +}
A web/package.json

@@ -0,0 +1,19 @@

+{ + "name": "weimar-web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.8", + "@sveltejs/kit": "^2.20.7", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "svelte": "^5.28.2", + "typescript": "^5.8.3", + "vite": "^6.3.5" + } +}
A web/src/app.html

@@ -0,0 +1,11 @@

+<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + %sveltekit.head% + </head> + <body> + <div style="display: contents">%sveltekit.body%</div> + </body> +</html>
A web/src/routes/+layout.js

@@ -0,0 +1,2 @@

+export const ssr = false; +export const prerender = false;
A web/src/routes/+page.svelte

@@ -0,0 +1,12 @@

+<h1>weimar</h1> +<p>Loading...</p> + +<script> + import { onMount } from 'svelte'; + + onMount(async () => { + const res = await fetch('/api/health'); + const data = await res.json(); + document.querySelector('p').textContent = 'Status: ' + data.status; + }); +</script>
A web/src/routes/browse/+page.svelte

@@ -0,0 +1,2 @@

+<h1>Browse</h1> +<p>Browse page — coming soon</p>
A web/src/routes/image/[id]/+page.svelte

@@ -0,0 +1,2 @@

+<h1>Image</h1> +<p>Image detail page — coming soon</p>
A web/src/routes/login/+page.svelte

@@ -0,0 +1,2 @@

+<h1>Login</h1> +<p>Login page — coming soon</p>
A web/src/routes/upload/+page.svelte

@@ -0,0 +1,2 @@

+<h1>Upload</h1> +<p>Upload page — coming soon</p>
A web/svelte.config.js

@@ -0,0 +1,16 @@

+import adapter from '@sveltejs/adapter-static'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: false + }) + } +}; + +export default config;
A web/tsconfig.json

@@ -0,0 +1,3 @@

+{ + "extends": "./.svelte-kit/tsconfig.json" +}
A web/vite.config.ts

@@ -0,0 +1,6 @@

+import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});