A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.

Compare changes

Choose any two refs to compare.

Changed files
+8516 -1972
cmd
appview
credential-helper
hold
usage-report
deploy
docs
examples
plugins
gatekeeper-provider
ratify-verifier
lexicons
io
atcr
pkg
+25
.air.hold.toml
···
··· 1 + root = "." 2 + tmp_dir = "tmp" 3 + 4 + [build] 5 + cmd = "go build -buildvcs=false -o ./tmp/atcr-hold ./cmd/hold" 6 + entrypoint = ["./tmp/atcr-hold"] 7 + include_ext = ["go"] 8 + exclude_dir = ["bin", "tmp", "vendor", "deploy", "docs", ".git", "dist", "pkg/appview"] 9 + exclude_regex = ["_test\\.go$"] 10 + delay = 1000 11 + stop_on_error = true 12 + send_interrupt = true 13 + kill_delay = 500 14 + 15 + [log] 16 + time = false 17 + 18 + [color] 19 + main = "blue" 20 + watcher = "magenta" 21 + build = "yellow" 22 + runner = "green" 23 + 24 + [misc] 25 + clean_on_exit = true
+1
.gitignore
··· 18 pkg/appview/static/js/lucide.min.js 19 20 # IDE 21 .claude/ 22 .vscode/ 23 .idea/
··· 18 pkg/appview/static/js/lucide.min.js 19 20 # IDE 21 + .zed/ 22 .claude/ 23 .vscode/ 24 .idea/
+7 -2
.golangci.yml
··· 1 # golangci-lint configuration for ATCR 2 # See: https://golangci-lint.run/usage/configuration/ 3 version: "2" 4 - linters: 5 settings: 6 staticcheck: 7 checks: ··· 20 exclusions: 21 presets: 22 - std-error-handling 23 formatters: 24 enable: 25 - gofmt 26 - - goimports
··· 1 # golangci-lint configuration for ATCR 2 # See: https://golangci-lint.run/usage/configuration/ 3 version: "2" 4 + linters: 5 settings: 6 staticcheck: 7 checks: ··· 20 exclusions: 21 presets: 22 - std-error-handling 23 + rules: 24 + - path: _test\.go 25 + linters: 26 + - errcheck 27 + 28 formatters: 29 enable: 30 - gofmt 31 + - goimports
+6 -4
Dockerfile.dev
··· 1 # Development image with Air hot reload 2 - # Build: docker build -f Dockerfile.dev -t atcr-appview-dev . 3 - # Run: docker run -v $(pwd):/app -p 5000:5000 atcr-appview-dev 4 FROM docker.io/golang:1.25.4-trixie 5 6 ENV DEBIAN_FRONTEND=noninteractive 7 8 RUN apt-get update && \ 9 apt-get install -y --no-install-recommends sqlite3 libsqlite3-dev curl && \ ··· 17 RUN go mod download 18 19 # For development: source mounted as volume, Air handles builds 20 - EXPOSE 5000 21 - CMD ["air", "-c", ".air.toml"]
··· 1 # Development image with Air hot reload 2 + # Build: docker build -f Dockerfile.dev -t atcr-dev . 3 + # Run: docker run -v $(pwd):/app -p 5000:5000 atcr-dev 4 FROM docker.io/golang:1.25.4-trixie 5 6 + ARG AIR_CONFIG=.air.toml 7 + 8 ENV DEBIAN_FRONTEND=noninteractive 9 + ENV AIR_CONFIG=${AIR_CONFIG} 10 11 RUN apt-get update && \ 12 apt-get install -y --no-install-recommends sqlite3 libsqlite3-dev curl && \ ··· 20 RUN go mod download 21 22 # For development: source mounted as volume, Air handles builds 23 + CMD ["sh", "-c", "air -c ${AIR_CONFIG}"]
+11 -25
cmd/appview/serve.go
··· 114 115 slog.Debug("Base URL for OAuth", "base_url", baseURL) 116 if testMode { 117 - slog.Info("TEST_MODE enabled - will use HTTP for local DID resolution and transition:generic scope") 118 } 119 120 // Create OAuth client app (automatically configures confidential client for production) ··· 122 oauthClientApp, err := oauth.NewClientApp(baseURL, oauthStore, desiredScopes, cfg.Server.OAuthKeyPath, cfg.Server.ClientName) 123 if err != nil { 124 return fmt.Errorf("failed to create OAuth client app: %w", err) 125 - } 126 - if testMode { 127 - slog.Info("Using OAuth scopes with transition:generic (test mode)") 128 - } else { 129 - slog.Info("Using OAuth scopes with RPC scope (production mode)") 130 } 131 132 // Invalidate sessions with mismatched scopes on startup ··· 380 // OAuth client metadata endpoint 381 mainRouter.Get("/oauth-client-metadata.json", func(w http.ResponseWriter, r *http.Request) { 382 config := oauthClientApp.Config 383 metadata := config.ClientMetadata() 384 385 // For confidential clients, ensure JWKS is included 386 // The indigo library should populate this automatically, but we explicitly set it here ··· 390 metadata.JWKS = &jwks 391 } 392 393 - // Convert indigo's metadata to map so we can add custom fields 394 - metadataBytes, err := json.Marshal(metadata) 395 - if err != nil { 396 - http.Error(w, "Failed to marshal metadata", http.StatusInternalServerError) 397 - return 398 - } 399 - 400 - var metadataMap map[string]interface{} 401 - if err := json.Unmarshal(metadataBytes, &metadataMap); err != nil { 402 - http.Error(w, "Failed to unmarshal metadata", http.StatusInternalServerError) 403 - return 404 - } 405 - 406 - // Add custom fields 407 - metadataMap["client_name"] = cfg.Server.ClientName 408 - metadataMap["client_uri"] = cfg.Server.BaseURL 409 - metadataMap["logo_uri"] = cfg.Server.BaseURL + "/web-app-manifest-192x192.png" 410 - 411 w.Header().Set("Content-Type", "application/json") 412 w.Header().Set("Access-Control-Allow-Origin", "*") 413 // Limit caching to allow scope changes to propagate quickly 414 // PDS servers cache client metadata, so short max-age helps with updates 415 w.Header().Set("Cache-Control", "public, max-age=300") 416 - if err := json.NewEncoder(w).Encode(metadataMap); err != nil { 417 http.Error(w, "Failed to encode metadata", http.StatusInternalServerError) 418 } 419 })
··· 114 115 slog.Debug("Base URL for OAuth", "base_url", baseURL) 116 if testMode { 117 + slog.Info("TEST_MODE enabled - will use HTTP for local DID resolution") 118 } 119 120 // Create OAuth client app (automatically configures confidential client for production) ··· 122 oauthClientApp, err := oauth.NewClientApp(baseURL, oauthStore, desiredScopes, cfg.Server.OAuthKeyPath, cfg.Server.ClientName) 123 if err != nil { 124 return fmt.Errorf("failed to create OAuth client app: %w", err) 125 } 126 127 // Invalidate sessions with mismatched scopes on startup ··· 375 // OAuth client metadata endpoint 376 mainRouter.Get("/oauth-client-metadata.json", func(w http.ResponseWriter, r *http.Request) { 377 config := oauthClientApp.Config 378 + logoURI := cfg.Server.BaseURL + "/web-app-manifest-192x192.png" 379 + policyURI := cfg.Server.BaseURL + "/privacy" 380 + tosURI := cfg.Server.BaseURL + "/terms" 381 + 382 metadata := config.ClientMetadata() 383 + metadata.ClientName = &cfg.Server.ClientName 384 + metadata.ClientURI = &cfg.Server.BaseURL 385 + metadata.LogoURI = &logoURI 386 + metadata.PolicyURI = &policyURI 387 + metadata.TosURI = &tosURI 388 389 // For confidential clients, ensure JWKS is included 390 // The indigo library should populate this automatically, but we explicitly set it here ··· 394 metadata.JWKS = &jwks 395 } 396 397 w.Header().Set("Content-Type", "application/json") 398 w.Header().Set("Access-Control-Allow-Origin", "*") 399 // Limit caching to allow scope changes to propagate quickly 400 // PDS servers cache client metadata, so short max-age helps with updates 401 w.Header().Set("Cache-Control", "public, max-age=300") 402 + if err := json.NewEncoder(w).Encode(metadata); err != nil { 403 http.Error(w, "Failed to encode metadata", http.StatusInternalServerError) 404 } 405 })
+2 -2
cmd/credential-helper/main.go
··· 180 181 // Wait for user to complete OAuth flow, then retry 182 fmt.Fprintf(os.Stderr, "Waiting for authentication") 183 - for i := 0; i < 60; i++ { // Wait up to 2 minutes 184 time.Sleep(2 * time.Second) 185 fmt.Fprintf(os.Stderr, ".") 186 ··· 765 curParts := strings.Split(curV, ".") 766 767 // Compare each part 768 - for i := 0; i < len(newParts) && i < len(curParts); i++ { 769 newNum := 0 770 curNum := 0 771 fmt.Sscanf(newParts[i], "%d", &newNum)
··· 180 181 // Wait for user to complete OAuth flow, then retry 182 fmt.Fprintf(os.Stderr, "Waiting for authentication") 183 + for range 60 { // Wait up to 2 minutes 184 time.Sleep(2 * time.Second) 185 fmt.Fprintf(os.Stderr, ".") 186 ··· 765 curParts := strings.Split(curV, ".") 766 767 // Compare each part 768 + for i := range min(len(newParts), len(curParts)) { 769 newNum := 0 770 curNum := 0 771 fmt.Sscanf(newParts[i], "%d", &newNum)
+45 -2
cmd/hold/main.go
··· 11 "time" 12 13 "atcr.io/pkg/hold" 14 "atcr.io/pkg/hold/oci" 15 "atcr.io/pkg/hold/pds" 16 "atcr.io/pkg/logging" 17 "atcr.io/pkg/s3" 18 ··· 98 os.Exit(1) 99 } 100 101 // Create blob store adapter and XRPC handlers 102 var ociHandler *oci.XRPCHandler 103 if holdPDS != nil { ··· 116 } 117 118 // Create PDS XRPC handler (ATProto endpoints) 119 - xrpcHandler = pds.NewXRPCHandler(holdPDS, *s3Service, driver, broadcaster, nil) 120 121 // Create OCI XRPC handler (multipart upload endpoints) 122 - ociHandler = oci.NewXRPCHandler(holdPDS, *s3Service, driver, cfg.Server.DisablePresignedURLs, cfg.Registration.EnableBlueskyPosts, nil) 123 } 124 125 // Setup HTTP routes with chi router ··· 154 ociHandler.RegisterHandlers(r) 155 } 156 157 // Create server 158 server := &http.Server{ 159 Addr: cfg.Server.Addr, ··· 220 slog.Warn("Failed to close broadcaster database", "error", err) 221 } else { 222 slog.Info("Broadcaster database closed") 223 } 224 } 225
··· 11 "time" 12 13 "atcr.io/pkg/hold" 14 + "atcr.io/pkg/hold/admin" 15 "atcr.io/pkg/hold/oci" 16 "atcr.io/pkg/hold/pds" 17 + "atcr.io/pkg/hold/quota" 18 "atcr.io/pkg/logging" 19 "atcr.io/pkg/s3" 20 ··· 100 os.Exit(1) 101 } 102 103 + // Initialize quota manager from quotas.yaml 104 + quotaMgr, err := quota.NewManager("./quotas.yaml") 105 + if err != nil { 106 + slog.Error("Failed to load quota config", "error", err) 107 + os.Exit(1) 108 + } 109 + if quotaMgr.IsEnabled() { 110 + slog.Info("Quota enforcement enabled", "tiers", quotaMgr.TierCount(), "defaultTier", quotaMgr.GetDefaultTier()) 111 + } else { 112 + slog.Info("Quota enforcement disabled (no quotas.yaml found)") 113 + } 114 + 115 // Create blob store adapter and XRPC handlers 116 var ociHandler *oci.XRPCHandler 117 if holdPDS != nil { ··· 130 } 131 132 // Create PDS XRPC handler (ATProto endpoints) 133 + xrpcHandler = pds.NewXRPCHandler(holdPDS, *s3Service, driver, broadcaster, nil, quotaMgr) 134 135 // Create OCI XRPC handler (multipart upload endpoints) 136 + ociHandler = oci.NewXRPCHandler(holdPDS, *s3Service, driver, cfg.Server.DisablePresignedURLs, cfg.Registration.EnableBlueskyPosts, nil, quotaMgr) 137 } 138 139 // Setup HTTP routes with chi router ··· 168 ociHandler.RegisterHandlers(r) 169 } 170 171 + // Initialize and register admin panel if enabled 172 + var adminUI *admin.AdminUI 173 + if cfg.Admin.Enabled && holdPDS != nil { 174 + adminCfg := admin.AdminConfig{ 175 + Enabled: true, 176 + PublicURL: cfg.Server.PublicURL, 177 + } 178 + 179 + adminUI, err = admin.NewAdminUI(context.Background(), holdPDS, quotaMgr, adminCfg) 180 + if err != nil { 181 + slog.Error("Failed to initialize admin panel", "error", err) 182 + os.Exit(1) 183 + } 184 + 185 + if adminUI != nil { 186 + slog.Info("Registering admin panel routes") 187 + adminUI.RegisterRoutes(r) 188 + } 189 + } 190 + 191 // Create server 192 server := &http.Server{ 193 Addr: cfg.Server.Addr, ··· 254 slog.Warn("Failed to close broadcaster database", "error", err) 255 } else { 256 slog.Info("Broadcaster database closed") 257 + } 258 + } 259 + 260 + // Close admin panel 261 + if adminUI != nil { 262 + if err := adminUI.Close(); err != nil { 263 + slog.Warn("Failed to close admin panel", "error", err) 264 + } else { 265 + slog.Info("Admin panel closed") 266 } 267 } 268
+624
cmd/usage-report/main.go
···
··· 1 + // usage-report queries a hold service and generates a storage usage report 2 + // grouped by user, with unique layers and totals. 3 + // 4 + // Usage: 5 + // 6 + // go run ./cmd/usage-report --hold https://hold01.atcr.io 7 + // go run ./cmd/usage-report --hold https://hold01.atcr.io --from-manifests 8 + package main 9 + 10 + import ( 11 + "encoding/json" 12 + "flag" 13 + "fmt" 14 + "io" 15 + "net/http" 16 + "net/url" 17 + "os" 18 + "sort" 19 + "strings" 20 + "time" 21 + ) 22 + 23 + // LayerRecord matches the io.atcr.hold.layer record structure 24 + type LayerRecord struct { 25 + Type string `json:"$type"` 26 + Digest string `json:"digest"` 27 + Size int64 `json:"size"` 28 + MediaType string `json:"mediaType"` 29 + Manifest string `json:"manifest"` 30 + UserDID string `json:"userDid"` 31 + CreatedAt string `json:"createdAt"` 32 + } 33 + 34 + // ManifestRecord matches the io.atcr.manifest record structure 35 + type ManifestRecord struct { 36 + Type string `json:"$type"` 37 + Repository string `json:"repository"` 38 + Digest string `json:"digest"` 39 + HoldDID string `json:"holdDid"` 40 + Config *struct { 41 + Digest string `json:"digest"` 42 + Size int64 `json:"size"` 43 + } `json:"config"` 44 + Layers []struct { 45 + Digest string `json:"digest"` 46 + Size int64 `json:"size"` 47 + MediaType string `json:"mediaType"` 48 + } `json:"layers"` 49 + Manifests []struct { 50 + Digest string `json:"digest"` 51 + Size int64 `json:"size"` 52 + } `json:"manifests"` 53 + CreatedAt string `json:"createdAt"` 54 + } 55 + 56 + // CrewRecord matches the io.atcr.hold.crew record structure 57 + type CrewRecord struct { 58 + Member string `json:"member"` 59 + Role string `json:"role"` 60 + Permissions []string `json:"permissions"` 61 + AddedAt string `json:"addedAt"` 62 + } 63 + 64 + // ListRecordsResponse is the response from com.atproto.repo.listRecords 65 + type ListRecordsResponse struct { 66 + Records []struct { 67 + URI string `json:"uri"` 68 + CID string `json:"cid"` 69 + Value json.RawMessage `json:"value"` 70 + } `json:"records"` 71 + Cursor string `json:"cursor,omitempty"` 72 + } 73 + 74 + // UserUsage tracks storage for a single user 75 + type UserUsage struct { 76 + DID string 77 + Handle string 78 + UniqueLayers map[string]int64 // digest -> size 79 + TotalSize int64 80 + LayerCount int 81 + Repositories map[string]bool // unique repos 82 + } 83 + 84 + var client = &http.Client{Timeout: 30 * time.Second} 85 + 86 + func main() { 87 + holdURL := flag.String("hold", "https://hold01.atcr.io", "Hold service URL") 88 + fromManifests := flag.Bool("from-manifests", false, "Calculate usage from user manifests instead of hold layer records (more accurate but slower)") 89 + flag.Parse() 90 + 91 + // Normalize URL 92 + baseURL := strings.TrimSuffix(*holdURL, "/") 93 + 94 + fmt.Printf("Querying %s...\n\n", baseURL) 95 + 96 + // First, get the hold's DID 97 + holdDID, err := getHoldDID(baseURL) 98 + if err != nil { 99 + fmt.Fprintf(os.Stderr, "Failed to get hold DID: %v\n", err) 100 + os.Exit(1) 101 + } 102 + fmt.Printf("Hold DID: %s\n\n", holdDID) 103 + 104 + var userUsage map[string]*UserUsage 105 + 106 + if *fromManifests { 107 + fmt.Println("=== Calculating from user manifests (bypasses layer record bug) ===") 108 + userUsage, err = calculateFromManifests(baseURL, holdDID) 109 + } else { 110 + fmt.Println("=== Calculating from hold layer records ===") 111 + fmt.Println("NOTE: May undercount app-password users due to layer record bug") 112 + fmt.Println(" Use --from-manifests for more accurate results") 113 + 114 + userUsage, err = calculateFromLayerRecords(baseURL, holdDID) 115 + } 116 + 117 + if err != nil { 118 + fmt.Fprintf(os.Stderr, "Failed to calculate usage: %v\n", err) 119 + os.Exit(1) 120 + } 121 + 122 + // Resolve DIDs to handles 123 + fmt.Println("\n\nResolving DIDs to handles...") 124 + for _, usage := range userUsage { 125 + handle, err := resolveDIDToHandle(usage.DID) 126 + if err != nil { 127 + usage.Handle = usage.DID 128 + } else { 129 + usage.Handle = handle 130 + } 131 + } 132 + 133 + // Convert to slice and sort by total size (descending) 134 + var sorted []*UserUsage 135 + for _, u := range userUsage { 136 + sorted = append(sorted, u) 137 + } 138 + sort.Slice(sorted, func(i, j int) bool { 139 + return sorted[i].TotalSize > sorted[j].TotalSize 140 + }) 141 + 142 + // Print report 143 + fmt.Println("\n========================================") 144 + fmt.Println("STORAGE USAGE REPORT") 145 + fmt.Println("========================================") 146 + 147 + var grandTotal int64 148 + var grandLayers int 149 + for _, u := range sorted { 150 + grandTotal += u.TotalSize 151 + grandLayers += u.LayerCount 152 + } 153 + 154 + fmt.Printf("\nTotal Users: %d\n", len(sorted)) 155 + fmt.Printf("Total Unique Layers: %d\n", grandLayers) 156 + fmt.Printf("Total Storage: %s\n\n", humanSize(grandTotal)) 157 + 158 + fmt.Println("BY USER (sorted by storage):") 159 + fmt.Println("----------------------------------------") 160 + for i, u := range sorted { 161 + fmt.Printf("%3d. %s\n", i+1, u.Handle) 162 + fmt.Printf(" DID: %s\n", u.DID) 163 + fmt.Printf(" Unique Layers: %d\n", u.LayerCount) 164 + fmt.Printf(" Total Size: %s\n", humanSize(u.TotalSize)) 165 + if len(u.Repositories) > 0 { 166 + var repos []string 167 + for r := range u.Repositories { 168 + repos = append(repos, r) 169 + } 170 + sort.Strings(repos) 171 + fmt.Printf(" Repositories: %s\n", strings.Join(repos, ", ")) 172 + } 173 + pct := float64(0) 174 + if grandTotal > 0 { 175 + pct = float64(u.TotalSize) / float64(grandTotal) * 100 176 + } 177 + fmt.Printf(" Share: %.1f%%\n\n", pct) 178 + } 179 + 180 + // Output CSV format for easy analysis 181 + fmt.Println("\n========================================") 182 + fmt.Println("CSV FORMAT") 183 + fmt.Println("========================================") 184 + fmt.Println("handle,did,unique_layers,total_bytes,total_human,repositories") 185 + for _, u := range sorted { 186 + var repos []string 187 + for r := range u.Repositories { 188 + repos = append(repos, r) 189 + } 190 + sort.Strings(repos) 191 + fmt.Printf("%s,%s,%d,%d,%s,\"%s\"\n", u.Handle, u.DID, u.LayerCount, u.TotalSize, humanSize(u.TotalSize), strings.Join(repos, ";")) 192 + } 193 + } 194 + 195 + // calculateFromLayerRecords uses the hold's layer records (original method) 196 + func calculateFromLayerRecords(baseURL, holdDID string) (map[string]*UserUsage, error) { 197 + layers, err := fetchAllLayerRecords(baseURL, holdDID) 198 + if err != nil { 199 + return nil, err 200 + } 201 + 202 + fmt.Printf("Fetched %d layer records\n", len(layers)) 203 + 204 + userUsage := make(map[string]*UserUsage) 205 + for _, layer := range layers { 206 + if layer.UserDID == "" { 207 + continue 208 + } 209 + 210 + usage, exists := userUsage[layer.UserDID] 211 + if !exists { 212 + usage = &UserUsage{ 213 + DID: layer.UserDID, 214 + UniqueLayers: make(map[string]int64), 215 + Repositories: make(map[string]bool), 216 + } 217 + userUsage[layer.UserDID] = usage 218 + } 219 + 220 + if _, seen := usage.UniqueLayers[layer.Digest]; !seen { 221 + usage.UniqueLayers[layer.Digest] = layer.Size 222 + usage.TotalSize += layer.Size 223 + usage.LayerCount++ 224 + } 225 + } 226 + 227 + return userUsage, nil 228 + } 229 + 230 + // calculateFromManifests queries crew members and fetches their manifests from their PDSes 231 + func calculateFromManifests(baseURL, holdDID string) (map[string]*UserUsage, error) { 232 + // Get all crew members 233 + crewDIDs, err := fetchCrewMembers(baseURL, holdDID) 234 + if err != nil { 235 + return nil, fmt.Errorf("failed to fetch crew: %w", err) 236 + } 237 + 238 + // Also get captain 239 + captainDID, err := fetchCaptain(baseURL, holdDID) 240 + if err == nil && captainDID != "" { 241 + // Add captain to list if not already there 242 + found := false 243 + for _, d := range crewDIDs { 244 + if d == captainDID { 245 + found = true 246 + break 247 + } 248 + } 249 + if !found { 250 + crewDIDs = append(crewDIDs, captainDID) 251 + } 252 + } 253 + 254 + fmt.Printf("Found %d users (crew + captain)\n", len(crewDIDs)) 255 + 256 + userUsage := make(map[string]*UserUsage) 257 + 258 + for _, did := range crewDIDs { 259 + fmt.Printf(" Checking manifests for %s...", did) 260 + 261 + // Resolve DID to PDS 262 + pdsEndpoint, err := resolveDIDToPDS(did) 263 + if err != nil { 264 + fmt.Printf(" (failed to resolve PDS: %v)\n", err) 265 + continue 266 + } 267 + 268 + // Fetch manifests that use this hold 269 + manifests, err := fetchUserManifestsForHold(pdsEndpoint, did, holdDID) 270 + if err != nil { 271 + fmt.Printf(" (failed to fetch manifests: %v)\n", err) 272 + continue 273 + } 274 + 275 + if len(manifests) == 0 { 276 + fmt.Printf(" 0 manifests\n") 277 + continue 278 + } 279 + 280 + // Calculate unique layers across all manifests 281 + usage := &UserUsage{ 282 + DID: did, 283 + UniqueLayers: make(map[string]int64), 284 + Repositories: make(map[string]bool), 285 + } 286 + 287 + for _, m := range manifests { 288 + usage.Repositories[m.Repository] = true 289 + 290 + // Add config blob 291 + if m.Config != nil { 292 + if _, seen := usage.UniqueLayers[m.Config.Digest]; !seen { 293 + usage.UniqueLayers[m.Config.Digest] = m.Config.Size 294 + usage.TotalSize += m.Config.Size 295 + usage.LayerCount++ 296 + } 297 + } 298 + 299 + // Add layers 300 + for _, layer := range m.Layers { 301 + if _, seen := usage.UniqueLayers[layer.Digest]; !seen { 302 + usage.UniqueLayers[layer.Digest] = layer.Size 303 + usage.TotalSize += layer.Size 304 + usage.LayerCount++ 305 + } 306 + } 307 + } 308 + 309 + fmt.Printf(" %d manifests, %d unique layers, %s\n", len(manifests), usage.LayerCount, humanSize(usage.TotalSize)) 310 + 311 + if usage.LayerCount > 0 { 312 + userUsage[did] = usage 313 + } 314 + } 315 + 316 + return userUsage, nil 317 + } 318 + 319 + // fetchCrewMembers gets all crew member DIDs from the hold 320 + func fetchCrewMembers(baseURL, holdDID string) ([]string, error) { 321 + var dids []string 322 + seen := make(map[string]bool) 323 + 324 + cursor := "" 325 + for { 326 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", baseURL) 327 + params := url.Values{} 328 + params.Set("repo", holdDID) 329 + params.Set("collection", "io.atcr.hold.crew") 330 + params.Set("limit", "100") 331 + if cursor != "" { 332 + params.Set("cursor", cursor) 333 + } 334 + 335 + resp, err := client.Get(u + "?" + params.Encode()) 336 + if err != nil { 337 + return nil, err 338 + } 339 + 340 + var listResp ListRecordsResponse 341 + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { 342 + resp.Body.Close() 343 + return nil, err 344 + } 345 + resp.Body.Close() 346 + 347 + for _, rec := range listResp.Records { 348 + var crew CrewRecord 349 + if err := json.Unmarshal(rec.Value, &crew); err != nil { 350 + continue 351 + } 352 + if crew.Member != "" && !seen[crew.Member] { 353 + seen[crew.Member] = true 354 + dids = append(dids, crew.Member) 355 + } 356 + } 357 + 358 + if listResp.Cursor == "" || len(listResp.Records) < 100 { 359 + break 360 + } 361 + cursor = listResp.Cursor 362 + } 363 + 364 + return dids, nil 365 + } 366 + 367 + // fetchCaptain gets the captain DID from the hold 368 + func fetchCaptain(baseURL, holdDID string) (string, error) { 369 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.getRecord?repo=%s&collection=io.atcr.hold.captain&rkey=self", 370 + baseURL, url.QueryEscape(holdDID)) 371 + 372 + resp, err := client.Get(u) 373 + if err != nil { 374 + return "", err 375 + } 376 + defer resp.Body.Close() 377 + 378 + if resp.StatusCode != http.StatusOK { 379 + return "", fmt.Errorf("status %d", resp.StatusCode) 380 + } 381 + 382 + var result struct { 383 + Value struct { 384 + Owner string `json:"owner"` 385 + } `json:"value"` 386 + } 387 + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 388 + return "", err 389 + } 390 + 391 + return result.Value.Owner, nil 392 + } 393 + 394 + // fetchUserManifestsForHold fetches all manifests from a user's PDS that use the specified hold 395 + func fetchUserManifestsForHold(pdsEndpoint, userDID, holdDID string) ([]ManifestRecord, error) { 396 + var manifests []ManifestRecord 397 + cursor := "" 398 + 399 + for { 400 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", pdsEndpoint) 401 + params := url.Values{} 402 + params.Set("repo", userDID) 403 + params.Set("collection", "io.atcr.manifest") 404 + params.Set("limit", "100") 405 + if cursor != "" { 406 + params.Set("cursor", cursor) 407 + } 408 + 409 + resp, err := client.Get(u + "?" + params.Encode()) 410 + if err != nil { 411 + return nil, err 412 + } 413 + 414 + if resp.StatusCode != http.StatusOK { 415 + resp.Body.Close() 416 + return nil, fmt.Errorf("status %d", resp.StatusCode) 417 + } 418 + 419 + var listResp ListRecordsResponse 420 + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { 421 + resp.Body.Close() 422 + return nil, err 423 + } 424 + resp.Body.Close() 425 + 426 + for _, rec := range listResp.Records { 427 + var m ManifestRecord 428 + if err := json.Unmarshal(rec.Value, &m); err != nil { 429 + continue 430 + } 431 + // Only include manifests for this hold 432 + if m.HoldDID == holdDID { 433 + manifests = append(manifests, m) 434 + } 435 + } 436 + 437 + if listResp.Cursor == "" || len(listResp.Records) < 100 { 438 + break 439 + } 440 + cursor = listResp.Cursor 441 + } 442 + 443 + return manifests, nil 444 + } 445 + 446 + // getHoldDID fetches the hold's DID from /.well-known/atproto-did 447 + func getHoldDID(baseURL string) (string, error) { 448 + resp, err := http.Get(baseURL + "/.well-known/atproto-did") 449 + if err != nil { 450 + return "", err 451 + } 452 + defer resp.Body.Close() 453 + 454 + if resp.StatusCode != http.StatusOK { 455 + return "", fmt.Errorf("unexpected status: %d", resp.StatusCode) 456 + } 457 + 458 + body, err := io.ReadAll(resp.Body) 459 + if err != nil { 460 + return "", err 461 + } 462 + 463 + return strings.TrimSpace(string(body)), nil 464 + } 465 + 466 + // fetchAllLayerRecords fetches all layer records with pagination 467 + func fetchAllLayerRecords(baseURL, holdDID string) ([]LayerRecord, error) { 468 + var allLayers []LayerRecord 469 + cursor := "" 470 + limit := 100 471 + 472 + for { 473 + u := fmt.Sprintf("%s/xrpc/com.atproto.repo.listRecords", baseURL) 474 + params := url.Values{} 475 + params.Set("repo", holdDID) 476 + params.Set("collection", "io.atcr.hold.layer") 477 + params.Set("limit", fmt.Sprintf("%d", limit)) 478 + if cursor != "" { 479 + params.Set("cursor", cursor) 480 + } 481 + 482 + fullURL := u + "?" + params.Encode() 483 + fmt.Printf(" Fetching: %s\n", fullURL) 484 + 485 + resp, err := client.Get(fullURL) 486 + if err != nil { 487 + return nil, fmt.Errorf("request failed: %w", err) 488 + } 489 + 490 + if resp.StatusCode != http.StatusOK { 491 + body, _ := io.ReadAll(resp.Body) 492 + resp.Body.Close() 493 + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) 494 + } 495 + 496 + var listResp ListRecordsResponse 497 + if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil { 498 + resp.Body.Close() 499 + return nil, fmt.Errorf("decode failed: %w", err) 500 + } 501 + resp.Body.Close() 502 + 503 + for _, rec := range listResp.Records { 504 + var layer LayerRecord 505 + if err := json.Unmarshal(rec.Value, &layer); err != nil { 506 + fmt.Fprintf(os.Stderr, "Warning: failed to parse layer record: %v\n", err) 507 + continue 508 + } 509 + allLayers = append(allLayers, layer) 510 + } 511 + 512 + fmt.Printf(" Got %d records (total: %d)\n", len(listResp.Records), len(allLayers)) 513 + 514 + if listResp.Cursor == "" || len(listResp.Records) < limit { 515 + break 516 + } 517 + cursor = listResp.Cursor 518 + } 519 + 520 + return allLayers, nil 521 + } 522 + 523 + // resolveDIDToHandle resolves a DID to a handle using the PLC directory or did:web 524 + func resolveDIDToHandle(did string) (string, error) { 525 + if strings.HasPrefix(did, "did:web:") { 526 + return strings.TrimPrefix(did, "did:web:"), nil 527 + } 528 + 529 + if strings.HasPrefix(did, "did:plc:") { 530 + plcURL := "https://plc.directory/" + did 531 + resp, err := client.Get(plcURL) 532 + if err != nil { 533 + return "", fmt.Errorf("PLC query failed: %w", err) 534 + } 535 + defer resp.Body.Close() 536 + 537 + if resp.StatusCode != http.StatusOK { 538 + return "", fmt.Errorf("PLC returned status %d", resp.StatusCode) 539 + } 540 + 541 + var plcDoc struct { 542 + AlsoKnownAs []string `json:"alsoKnownAs"` 543 + } 544 + if err := json.NewDecoder(resp.Body).Decode(&plcDoc); err != nil { 545 + return "", fmt.Errorf("failed to parse PLC response: %w", err) 546 + } 547 + 548 + for _, aka := range plcDoc.AlsoKnownAs { 549 + if strings.HasPrefix(aka, "at://") { 550 + return strings.TrimPrefix(aka, "at://"), nil 551 + } 552 + } 553 + 554 + return did, nil 555 + } 556 + 557 + return did, nil 558 + } 559 + 560 + // resolveDIDToPDS resolves a DID to its PDS endpoint 561 + func resolveDIDToPDS(did string) (string, error) { 562 + if strings.HasPrefix(did, "did:web:") { 563 + // did:web:example.com -> https://example.com 564 + domain := strings.TrimPrefix(did, "did:web:") 565 + return "https://" + domain, nil 566 + } 567 + 568 + if strings.HasPrefix(did, "did:plc:") { 569 + plcURL := "https://plc.directory/" + did 570 + resp, err := client.Get(plcURL) 571 + if err != nil { 572 + return "", fmt.Errorf("PLC query failed: %w", err) 573 + } 574 + defer resp.Body.Close() 575 + 576 + if resp.StatusCode != http.StatusOK { 577 + return "", fmt.Errorf("PLC returned status %d", resp.StatusCode) 578 + } 579 + 580 + var plcDoc struct { 581 + Service []struct { 582 + ID string `json:"id"` 583 + Type string `json:"type"` 584 + ServiceEndpoint string `json:"serviceEndpoint"` 585 + } `json:"service"` 586 + } 587 + if err := json.NewDecoder(resp.Body).Decode(&plcDoc); err != nil { 588 + return "", fmt.Errorf("failed to parse PLC response: %w", err) 589 + } 590 + 591 + for _, svc := range plcDoc.Service { 592 + if svc.Type == "AtprotoPersonalDataServer" { 593 + return svc.ServiceEndpoint, nil 594 + } 595 + } 596 + 597 + return "", fmt.Errorf("no PDS found in DID document") 598 + } 599 + 600 + return "", fmt.Errorf("unsupported DID method") 601 + } 602 + 603 + // humanSize converts bytes to human-readable format 604 + func humanSize(bytes int64) string { 605 + const ( 606 + KB = 1024 607 + MB = 1024 * KB 608 + GB = 1024 * MB 609 + TB = 1024 * GB 610 + ) 611 + 612 + switch { 613 + case bytes >= TB: 614 + return fmt.Sprintf("%.2f TB", float64(bytes)/TB) 615 + case bytes >= GB: 616 + return fmt.Sprintf("%.2f GB", float64(bytes)/GB) 617 + case bytes >= MB: 618 + return fmt.Sprintf("%.2f MB", float64(bytes)/MB) 619 + case bytes >= KB: 620 + return fmt.Sprintf("%.2f KB", float64(bytes)/KB) 621 + default: 622 + return fmt.Sprintf("%d B", bytes) 623 + } 624 + }
+2
deploy/docker-compose.prod.yml
··· 91 container_name: atcr-hold 92 restart: unless-stopped 93 environment: 94 # Hold service configuration (derived from HOLD_DOMAIN) 95 HOLD_PUBLIC_URL: ${HOLD_PUBLIC_URL:-https://${HOLD_DOMAIN:-hold01.atcr.io}} 96 HOLD_SERVER_ADDR: :8080 ··· 123 volumes: 124 # PDS data (carstore SQLite + signing keys) 125 - atcr-hold-data:/var/lib/atcr-hold 126 networks: 127 - atcr-network 128 healthcheck:
··· 91 container_name: atcr-hold 92 restart: unless-stopped 93 environment: 94 + HOLD_ADMIN_ENABLED: true 95 # Hold service configuration (derived from HOLD_DOMAIN) 96 HOLD_PUBLIC_URL: ${HOLD_PUBLIC_URL:-https://${HOLD_DOMAIN:-hold01.atcr.io}} 97 HOLD_SERVER_ADDR: :8080 ··· 124 volumes: 125 # PDS data (carstore SQLite + signing keys) 126 - atcr-hold-data:/var/lib/atcr-hold 127 + - ./quotas.yaml:/quotas.yaml:ro 128 networks: 129 - atcr-network 130 healthcheck:
+35
deploy/quotas.yaml
···
··· 1 + # ATCR Hold Service Quota Configuration 2 + # Copy this file to quotas.yaml to enable quota enforcement. 3 + # If quotas.yaml doesn't exist, quotas are disabled (unlimited for all users). 4 + 5 + # Tiers define quota levels using nautical crew ranks. 6 + # Each tier has a quota limit specified in human-readable format. 7 + # Supported units: B, KB, MB, GB, TB, PB (case-insensitive) 8 + tiers: 9 + # Entry-level crew - suitable for new or casual users 10 + deckhand: 11 + quota: 5GB 12 + 13 + # Mid-level crew - for regular contributors 14 + bosun: 15 + quota: 10GB 16 + 17 + # Senior crew - for power users or trusted contributors 18 + quartermaster: 19 + quota: 50GB 20 + 21 + # You can add custom tiers with any name: 22 + # unlimited_crew: 23 + # quota: 1TB 24 + 25 + defaults: 26 + # Default tier assigned to new crew members who don't have an explicit tier. 27 + # This tier must exist in the tiers section above. 28 + new_crew_tier: deckhand 29 + 30 + # Notes: 31 + # - The hold captain (owner) always has unlimited quota regardless of tiers. 32 + # - Crew members can be assigned a specific tier in their crew record. 33 + # - If a crew member's tier doesn't exist in config, they fall back to the default. 34 + # - Quota is calculated per-user by summing unique blob sizes (deduplicated). 35 + # - Quota is checked when pushing manifests (after blobs are already uploaded).
+11 -2
docker-compose.yml
··· 45 env_file: 46 - ../atcr-secrets.env # Load S3/Storj credentials from external file 47 environment: 48 HOLD_PUBLIC_URL: http://172.28.0.3:8080 49 HOLD_OWNER: did:plc:pddp4xt5lgnv2qsegbzzs4xg 50 HOLD_PUBLIC: false 51 # STORAGE_DRIVER: filesystem 52 # STORAGE_ROOT_DIR: /var/lib/atcr/hold 53 TEST_MODE: true ··· 57 # Storage config comes from env_file (STORAGE_DRIVER, AWS_*, S3_*) 58 build: 59 context: . 60 - dockerfile: Dockerfile.hold 61 - image: atcr-hold:latest 62 container_name: atcr-hold 63 ports: 64 - "8080:8080" 65 volumes: 66 # PDS data (carstore SQLite + signing keys) 67 - atcr-hold:/var/lib/atcr-hold 68 restart: unless-stopped 69 dns: 70 - 8.8.8.8
··· 45 env_file: 46 - ../atcr-secrets.env # Load S3/Storj credentials from external file 47 environment: 48 + HOLD_ADMIN_ENABLED: true 49 HOLD_PUBLIC_URL: http://172.28.0.3:8080 50 HOLD_OWNER: did:plc:pddp4xt5lgnv2qsegbzzs4xg 51 HOLD_PUBLIC: false 52 + HOLD_ALLOW_ALL_CREW: true 53 # STORAGE_DRIVER: filesystem 54 # STORAGE_ROOT_DIR: /var/lib/atcr/hold 55 TEST_MODE: true ··· 59 # Storage config comes from env_file (STORAGE_DRIVER, AWS_*, S3_*) 60 build: 61 context: . 62 + dockerfile: Dockerfile.dev 63 + args: 64 + AIR_CONFIG: .air.hold.toml 65 + image: atcr-hold-dev:latest 66 container_name: atcr-hold 67 ports: 68 - "8080:8080" 69 volumes: 70 + # Mount source code for Air hot reload 71 + - .:/app 72 + # Cache go modules between rebuilds 73 + - go-mod-cache:/go/pkg/mod 74 # PDS data (carstore SQLite + signing keys) 75 - atcr-hold:/var/lib/atcr-hold 76 + - ./deploy/quotas.yaml:/app/quotas.yaml:ro 77 restart: unless-stopped 78 dns: 79 - 8.8.8.8
+1399
docs/ADMIN_PANEL.md
···
··· 1 + # Hold Admin Panel Implementation Plan 2 + 3 + This document describes the implementation plan for adding an owner-only admin web UI to the ATCR hold service. The admin panel will be embedded directly in the hold service binary for simplified deployment. 4 + 5 + ## Table of Contents 6 + 7 + 1. [Overview](#overview) 8 + 2. [Requirements](#requirements) 9 + 3. [Architecture](#architecture) 10 + 4. [File Structure](#file-structure) 11 + 5. [Authentication](#authentication) 12 + 6. [Session Management](#session-management) 13 + 7. [Route Structure](#route-structure) 14 + 8. [Feature Implementations](#feature-implementations) 15 + 9. [Templates](#templates) 16 + 10. [Environment Variables](#environment-variables) 17 + 11. [Security Considerations](#security-considerations) 18 + 12. [Implementation Phases](#implementation-phases) 19 + 13. [Testing Strategy](#testing-strategy) 20 + 21 + --- 22 + 23 + ## Overview 24 + 25 + The hold admin panel provides a web-based interface for hold owners to: 26 + 27 + - **Manage crew members**: Add, remove, edit permissions and quota tiers 28 + - **Configure hold settings**: Toggle public access, open registration, Bluesky posting 29 + - **View usage metrics**: Storage usage per user, top users, repository statistics 30 + - **Monitor quota utilization**: Track tier distribution and usage percentages 31 + 32 + The admin panel is owner-only - only the DID that matches `captain.Owner` can access it. 33 + 34 + --- 35 + 36 + ## Requirements 37 + 38 + ### Functional Requirements 39 + 40 + 1. **Crew Management** 41 + - List all crew members with their DID, role, permissions, tier, and storage usage 42 + - Add new crew members with specified permissions and tier 43 + - Edit existing crew member permissions and tier 44 + - Remove crew members (with confirmation) 45 + - Display each crew member's quota utilization percentage 46 + 47 + 2. **Quota/Tier Management** 48 + - Display available tiers from `quotas.yaml` 49 + - Show tier limits and descriptions 50 + - Allow changing crew member tiers 51 + - Display current vs limit usage for each user 52 + 53 + 3. **Usage Metrics** 54 + - Total storage used across all users 55 + - Total unique blobs (deduplicated) 56 + - Number of crew members 57 + - Top 10/50/100 users by storage consumption 58 + - Per-repository statistics (pulls, pushes) 59 + 60 + 4. **Hold Settings** 61 + - Toggle `public` (allow anonymous blob reads) 62 + - Toggle `allowAllCrew` (allow any authenticated user to join) 63 + - Toggle `enableBlueskyPosts` (post to Bluesky on image push) 64 + 65 + ### Non-Functional Requirements 66 + 67 + - **Single binary**: Embedded in hold service, no separate deployment 68 + - **Responsive UI**: Works on desktop and mobile browsers 69 + - **Low latency**: Dashboard loads in <500ms for typical data volumes 70 + - **Minimal dependencies**: Uses Go templates, HTMX for interactivity 71 + 72 + --- 73 + 74 + ## Architecture 75 + 76 + ### High-Level Design 77 + 78 + ``` 79 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 80 + โ”‚ Hold Service โ”‚ 81 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 82 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 83 + โ”‚ โ”‚ XRPC/PDS โ”‚ โ”‚ OCI XRPC โ”‚ โ”‚ Admin Panel โ”‚ โ”‚ 84 + โ”‚ โ”‚ Handlers โ”‚ โ”‚ Handlers โ”‚ โ”‚ Handlers โ”‚ โ”‚ 85 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 86 + โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 87 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 88 + โ”‚ โ”‚ Chi Router โ”‚ โ”‚ 89 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 90 + โ”‚ โ”‚ โ”‚ 91 + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 92 + โ”‚ โ”‚ Embedded PDS โ”‚ โ”‚ 93 + โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ 94 + โ”‚ โ”‚ โ”‚ Captain โ”‚ โ”‚ Crew โ”‚ โ”‚ Layer โ”‚ โ”‚ โ”‚ 95 + โ”‚ โ”‚ โ”‚ Records โ”‚ โ”‚ Records โ”‚ โ”‚ Records โ”‚ โ”‚ โ”‚ 96 + โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ 97 + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ 98 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 99 + ``` 100 + 101 + ### Components 102 + 103 + 1. **AdminUI** - Main struct containing all admin dependencies 104 + 2. **Session Store** - SQLite-backed session management (separate from carstore) 105 + 3. **OAuth Client** - Reuses `pkg/auth/oauth/` for browser-based login 106 + 4. **Auth Middleware** - Validates owner-only access 107 + 5. **Handlers** - HTTP handlers for each admin page 108 + 6. **Templates** - Go html/template with embed.FS 109 + 110 + --- 111 + 112 + ## File Structure 113 + 114 + ``` 115 + pkg/hold/admin/ 116 + โ”œโ”€โ”€ admin.go # Main struct, initialization, route registration 117 + โ”œโ”€โ”€ auth.go # requireOwner middleware, session validation 118 + โ”œโ”€โ”€ handlers.go # HTTP handlers for all admin pages 119 + โ”œโ”€โ”€ session.go # SQLite session store implementation 120 + โ”œโ”€โ”€ metrics.go # Metrics collection and aggregation 121 + โ”œโ”€โ”€ templates/ 122 + โ”‚ โ”œโ”€โ”€ base.html # Base layout (html, head, body wrapper) 123 + โ”‚ โ”œโ”€โ”€ components/ 124 + โ”‚ โ”‚ โ”œโ”€โ”€ head.html # CSS/JS includes (HTMX, Lucide icons) 125 + โ”‚ โ”‚ โ”œโ”€โ”€ nav.html # Admin navigation bar 126 + โ”‚ โ”‚ โ””โ”€โ”€ flash.html # Flash message component 127 + โ”‚ โ”œโ”€โ”€ pages/ 128 + โ”‚ โ”‚ โ”œโ”€โ”€ login.html # OAuth login page 129 + โ”‚ โ”‚ โ”œโ”€โ”€ dashboard.html # Metrics overview 130 + โ”‚ โ”‚ โ”œโ”€โ”€ crew.html # Crew list with management actions 131 + โ”‚ โ”‚ โ”œโ”€โ”€ crew_add.html # Add crew member form 132 + โ”‚ โ”‚ โ”œโ”€โ”€ crew_edit.html # Edit crew member form 133 + โ”‚ โ”‚ โ””โ”€โ”€ settings.html # Hold settings page 134 + โ”‚ โ””โ”€โ”€ partials/ 135 + โ”‚ โ”œโ”€โ”€ crew_row.html # Single crew row (for HTMX updates) 136 + โ”‚ โ”œโ”€โ”€ usage_stats.html # Usage stats partial 137 + โ”‚ โ””โ”€โ”€ top_users.html # Top users table partial 138 + โ””โ”€โ”€ static/ 139 + โ”œโ”€โ”€ css/ 140 + โ”‚ โ””โ”€โ”€ admin.css # Admin-specific styles 141 + โ””โ”€โ”€ js/ 142 + โ””โ”€โ”€ admin.js # Admin-specific JavaScript (if needed) 143 + ``` 144 + 145 + ### Files to Modify 146 + 147 + | File | Changes | 148 + |------|---------| 149 + | `cmd/hold/main.go` | Add admin UI initialization and route registration | 150 + | `pkg/hold/config.go` | Add `Admin.Enabled` and `Admin.SessionDuration` fields | 151 + | `.env.hold.example` | Document `HOLD_ADMIN_ENABLED`, `HOLD_ADMIN_SESSION_DURATION` | 152 + 153 + --- 154 + 155 + ## Authentication 156 + 157 + ### OAuth Flow for Admin Login 158 + 159 + The admin panel uses ATProto OAuth with DPoP for browser-based authentication: 160 + 161 + ``` 162 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 163 + โ”‚ Browser โ”‚ โ”‚ Hold โ”‚ โ”‚ PDS โ”‚ โ”‚ Owner โ”‚ 164 + โ”‚ โ”‚ โ”‚ Admin โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 165 + โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ 166 + โ”‚ โ”‚ โ”‚ โ”‚ 167 + โ”‚ GET /admin โ”‚ โ”‚ โ”‚ 168 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 169 + โ”‚ โ”‚ โ”‚ โ”‚ 170 + โ”‚ 302 /admin/auth/login โ”‚ โ”‚ 171 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 172 + โ”‚ โ”‚ โ”‚ โ”‚ 173 + โ”‚ GET /admin/auth/login โ”‚ โ”‚ 174 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 175 + โ”‚ โ”‚ โ”‚ โ”‚ 176 + โ”‚ Login page (enter handle) โ”‚ โ”‚ 177 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 178 + โ”‚ โ”‚ โ”‚ โ”‚ 179 + โ”‚ POST handle โ”‚ โ”‚ โ”‚ 180 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 181 + โ”‚ โ”‚ โ”‚ โ”‚ 182 + โ”‚ โ”‚ StartAuthFlow โ”‚ โ”‚ 183 + โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 184 + โ”‚ โ”‚ โ”‚ โ”‚ 185 + โ”‚ 302 to PDS auth URL โ”‚ โ”‚ 186 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 187 + โ”‚ โ”‚ โ”‚ โ”‚ 188 + โ”‚ Authorize in browser โ”‚ โ”‚ 189 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 190 + โ”‚ โ”‚ โ”‚ Approve? โ”‚ 191 + โ”‚ โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 192 + โ”‚ โ”‚ โ”‚ โ”‚ 193 + โ”‚ โ”‚ โ”‚ Yes โ”‚ 194 + โ”‚ โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ 195 + โ”‚ โ”‚ โ”‚ โ”‚ 196 + โ”‚ 302 callback with code โ”‚ โ”‚ 197 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ 198 + โ”‚ โ”‚ โ”‚ โ”‚ 199 + โ”‚ GET /admin/auth/oauth/callback โ”‚ โ”‚ 200 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 201 + โ”‚ โ”‚ โ”‚ โ”‚ 202 + โ”‚ โ”‚ ProcessCallbackโ”‚ โ”‚ 203 + โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 204 + โ”‚ โ”‚ โ”‚ โ”‚ 205 + โ”‚ โ”‚ OAuth tokens โ”‚ โ”‚ 206 + โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ 207 + โ”‚ โ”‚ โ”‚ โ”‚ 208 + โ”‚ โ”‚ Check: DID == captain.Owner? โ”‚ 209 + โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ 210 + โ”‚ โ”‚ โ”‚ โ”‚ 211 + โ”‚ โ”‚ YES: Create session โ”‚ 212 + โ”‚ โ”‚ โ”‚ โ”‚ 213 + โ”‚ 302 /admin + session cookie โ”‚ โ”‚ 214 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 215 + โ”‚ โ”‚ โ”‚ โ”‚ 216 + โ”‚ GET /admin (with cookie) โ”‚ โ”‚ 217 + โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 218 + โ”‚ โ”‚ โ”‚ โ”‚ 219 + โ”‚ Dashboard โ”‚ โ”‚ โ”‚ 220 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ โ”‚ 221 + ``` 222 + 223 + ### Owner Validation 224 + 225 + The callback handler performs owner validation: 226 + 227 + ```go 228 + func (ui *AdminUI) handleCallback(w http.ResponseWriter, r *http.Request) { 229 + // Process OAuth callback 230 + sessionData, err := ui.clientApp.ProcessCallback(r.Context(), r.URL.Query()) 231 + if err != nil { 232 + ui.renderError(w, "OAuth failed: " + err.Error()) 233 + return 234 + } 235 + 236 + did := sessionData.AccountDID.String() 237 + 238 + // Get captain record to check owner 239 + _, captain, err := ui.pds.GetCaptainRecord(r.Context()) 240 + if err != nil { 241 + ui.renderError(w, "Failed to verify ownership") 242 + return 243 + } 244 + 245 + // CRITICAL: Only allow the hold owner 246 + if did != captain.Owner { 247 + slog.Warn("Non-owner attempted admin access", "did", did, "owner", captain.Owner) 248 + ui.renderError(w, "Access denied: Only the hold owner can access the admin panel") 249 + return 250 + } 251 + 252 + // Create admin session 253 + sessionID, err := ui.sessionStore.Create(did, sessionData.Handle, 24*time.Hour) 254 + if err != nil { 255 + ui.renderError(w, "Failed to create session") 256 + return 257 + } 258 + 259 + // Set session cookie 260 + http.SetCookie(w, &http.Cookie{ 261 + Name: "hold_admin_session", 262 + Value: sessionID, 263 + Path: "/admin", 264 + MaxAge: 86400, // 24 hours 265 + HttpOnly: true, 266 + Secure: r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https", 267 + SameSite: http.SameSiteLaxMode, 268 + }) 269 + 270 + http.Redirect(w, r, "/admin", http.StatusFound) 271 + } 272 + ``` 273 + 274 + ### Auth Middleware 275 + 276 + ```go 277 + // requireOwner ensures the request is from the hold owner 278 + func (ui *AdminUI) requireOwner(next http.Handler) http.Handler { 279 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 280 + // Get session cookie 281 + cookie, err := r.Cookie("hold_admin_session") 282 + if err != nil { 283 + http.Redirect(w, r, "/admin/auth/login?return_to="+r.URL.Path, http.StatusFound) 284 + return 285 + } 286 + 287 + // Validate session 288 + session, err := ui.sessionStore.Get(cookie.Value) 289 + if err != nil || session == nil || session.ExpiresAt.Before(time.Now()) { 290 + // Clear invalid cookie 291 + http.SetCookie(w, &http.Cookie{ 292 + Name: "hold_admin_session", 293 + Value: "", 294 + Path: "/admin", 295 + MaxAge: -1, 296 + }) 297 + http.Redirect(w, r, "/admin/auth/login", http.StatusFound) 298 + return 299 + } 300 + 301 + // Double-check DID still matches captain.Owner 302 + // (in case ownership transferred while session active) 303 + _, captain, err := ui.pds.GetCaptainRecord(r.Context()) 304 + if err != nil || session.DID != captain.Owner { 305 + ui.sessionStore.Delete(cookie.Value) 306 + http.Error(w, "Access denied: ownership verification failed", http.StatusForbidden) 307 + return 308 + } 309 + 310 + // Add session to context for handlers 311 + ctx := context.WithValue(r.Context(), adminSessionKey, session) 312 + next.ServeHTTP(w, r.WithContext(ctx)) 313 + }) 314 + } 315 + ``` 316 + 317 + --- 318 + 319 + ## Session Management 320 + 321 + ### Session Store Schema 322 + 323 + ```sql 324 + -- Admin sessions (browser login state) 325 + CREATE TABLE IF NOT EXISTS admin_sessions ( 326 + id TEXT PRIMARY KEY, 327 + did TEXT NOT NULL, 328 + handle TEXT, 329 + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 330 + expires_at DATETIME NOT NULL, 331 + last_accessed DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 332 + ); 333 + 334 + -- Index for cleanup queries 335 + CREATE INDEX IF NOT EXISTS idx_admin_sessions_expires ON admin_sessions(expires_at); 336 + CREATE INDEX IF NOT EXISTS idx_admin_sessions_did ON admin_sessions(did); 337 + 338 + -- OAuth sessions (indigo library storage) 339 + CREATE TABLE IF NOT EXISTS admin_oauth_sessions ( 340 + session_id TEXT PRIMARY KEY, 341 + did TEXT NOT NULL, 342 + data BLOB NOT NULL, 343 + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 344 + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP 345 + ); 346 + ``` 347 + 348 + ### Session Store Interface 349 + 350 + ```go 351 + // AdminSession represents an authenticated admin session 352 + type AdminSession struct { 353 + ID string 354 + DID string 355 + Handle string 356 + CreatedAt time.Time 357 + ExpiresAt time.Time 358 + LastAccessed time.Time 359 + } 360 + 361 + // AdminSessionStore manages admin sessions 362 + type AdminSessionStore struct { 363 + db *sql.DB 364 + } 365 + 366 + func NewAdminSessionStore(dbPath string) (*AdminSessionStore, error) 367 + 368 + func (s *AdminSessionStore) Create(did, handle string, duration time.Duration) (string, error) 369 + func (s *AdminSessionStore) Get(sessionID string) (*AdminSession, error) 370 + func (s *AdminSessionStore) Delete(sessionID string) error 371 + func (s *AdminSessionStore) DeleteForDID(did string) error 372 + func (s *AdminSessionStore) Cleanup() error // Remove expired sessions 373 + func (s *AdminSessionStore) Touch(sessionID string) error // Update last_accessed 374 + ``` 375 + 376 + ### Database Location 377 + 378 + The admin database should be in the same directory as the carstore database: 379 + 380 + ```go 381 + adminDBPath := filepath.Join(cfg.Database.Path, "admin.db") 382 + ``` 383 + 384 + This keeps all hold data together while maintaining separation between the carstore (ATProto records) and admin sessions. 385 + 386 + --- 387 + 388 + ## Route Structure 389 + 390 + ### Complete Route Table 391 + 392 + | Route | Method | Auth | Handler | Description | 393 + |-------|--------|------|---------|-------------| 394 + | `/admin` | GET | Owner | `DashboardHandler` | Main dashboard with metrics | 395 + | `/admin/crew` | GET | Owner | `CrewListHandler` | List all crew members | 396 + | `/admin/crew/add` | GET | Owner | `CrewAddFormHandler` | Add crew form | 397 + | `/admin/crew/add` | POST | Owner | `CrewAddHandler` | Process add crew | 398 + | `/admin/crew/{rkey}` | GET | Owner | `CrewEditFormHandler` | Edit crew form | 399 + | `/admin/crew/{rkey}/update` | POST | Owner | `CrewUpdateHandler` | Process crew update | 400 + | `/admin/crew/{rkey}/delete` | POST | Owner | `CrewDeleteHandler` | Delete crew member | 401 + | `/admin/settings` | GET | Owner | `SettingsHandler` | Hold settings page | 402 + | `/admin/settings/update` | POST | Owner | `SettingsUpdateHandler` | Update settings | 403 + | `/admin/api/stats` | GET | Owner | `StatsAPIHandler` | JSON stats endpoint | 404 + | `/admin/api/top-users` | GET | Owner | `TopUsersAPIHandler` | JSON top users | 405 + | `/admin/auth/login` | GET | Public | `LoginHandler` | Login page | 406 + | `/admin/auth/oauth/authorize` | GET | Public | OAuth authorize | Start OAuth flow | 407 + | `/admin/auth/oauth/callback` | GET | Public | `CallbackHandler` | OAuth callback | 408 + | `/admin/auth/logout` | GET | Owner | `LogoutHandler` | Logout and clear session | 409 + | `/admin/static/*` | GET | Public | Static files | CSS, JS assets | 410 + 411 + ### Route Registration 412 + 413 + ```go 414 + func (ui *AdminUI) RegisterRoutes(r chi.Router) { 415 + // Public routes (login flow) 416 + r.Get("/admin/auth/login", ui.handleLogin) 417 + r.Get("/admin/auth/oauth/authorize", ui.handleAuthorize) 418 + r.Get("/admin/auth/oauth/callback", ui.handleCallback) 419 + 420 + // Static files (public) 421 + r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", ui.staticHandler())) 422 + 423 + // Protected routes (require owner) 424 + r.Group(func(r chi.Router) { 425 + r.Use(ui.requireOwner) 426 + 427 + // Dashboard 428 + r.Get("/admin", ui.handleDashboard) 429 + 430 + // Crew management 431 + r.Get("/admin/crew", ui.handleCrewList) 432 + r.Get("/admin/crew/add", ui.handleCrewAddForm) 433 + r.Post("/admin/crew/add", ui.handleCrewAdd) 434 + r.Get("/admin/crew/{rkey}", ui.handleCrewEditForm) 435 + r.Post("/admin/crew/{rkey}/update", ui.handleCrewUpdate) 436 + r.Post("/admin/crew/{rkey}/delete", ui.handleCrewDelete) 437 + 438 + // Settings 439 + r.Get("/admin/settings", ui.handleSettings) 440 + r.Post("/admin/settings/update", ui.handleSettingsUpdate) 441 + 442 + // API endpoints (for HTMX) 443 + r.Get("/admin/api/stats", ui.handleStatsAPI) 444 + r.Get("/admin/api/top-users", ui.handleTopUsersAPI) 445 + 446 + // Logout 447 + r.Get("/admin/auth/logout", ui.handleLogout) 448 + }) 449 + } 450 + ``` 451 + 452 + --- 453 + 454 + ## Feature Implementations 455 + 456 + ### Dashboard Handler 457 + 458 + ```go 459 + type DashboardStats struct { 460 + TotalCrewMembers int 461 + TotalBlobs int64 462 + TotalStorageBytes int64 463 + TotalStorageHuman string 464 + TierDistribution map[string]int // tier -> count 465 + RecentActivity []ActivityEntry 466 + } 467 + 468 + func (ui *AdminUI) handleDashboard(w http.ResponseWriter, r *http.Request) { 469 + ctx := r.Context() 470 + 471 + // Collect basic stats 472 + crew, _ := ui.pds.ListCrewMembers(ctx) 473 + 474 + stats := DashboardStats{ 475 + TotalCrewMembers: len(crew), 476 + TierDistribution: make(map[string]int), 477 + } 478 + 479 + // Count tier distribution 480 + for _, member := range crew { 481 + tier := member.Tier 482 + if tier == "" { 483 + tier = ui.quotaMgr.GetDefaultTier() 484 + } 485 + stats.TierDistribution[tier]++ 486 + } 487 + 488 + // Storage stats (loaded via HTMX to avoid slow initial load) 489 + // The actual calculation happens in handleStatsAPI 490 + 491 + data := struct { 492 + AdminPageData 493 + Stats DashboardStats 494 + }{ 495 + AdminPageData: ui.newPageData(r), 496 + Stats: stats, 497 + } 498 + 499 + ui.templates.ExecuteTemplate(w, "dashboard", data) 500 + } 501 + ``` 502 + 503 + ### Crew List Handler 504 + 505 + ```go 506 + type CrewMemberView struct { 507 + RKey string 508 + DID string 509 + Handle string // Resolved from DID 510 + Role string 511 + Permissions []string 512 + Tier string 513 + TierLimit string // Human-readable 514 + CurrentUsage int64 515 + UsageHuman string 516 + UsagePercent int 517 + Plankowner bool 518 + AddedAt time.Time 519 + } 520 + 521 + func (ui *AdminUI) handleCrewList(w http.ResponseWriter, r *http.Request) { 522 + ctx := r.Context() 523 + 524 + crew, err := ui.pds.ListCrewMembers(ctx) 525 + if err != nil { 526 + ui.renderError(w, "Failed to list crew: "+err.Error()) 527 + return 528 + } 529 + 530 + // Enrich with usage data 531 + var crewViews []CrewMemberView 532 + for _, member := range crew { 533 + view := CrewMemberView{ 534 + RKey: member.RKey, 535 + DID: member.Member, 536 + Role: member.Role, 537 + Permissions: member.Permissions, 538 + Tier: member.Tier, 539 + Plankowner: member.Plankowner, 540 + AddedAt: member.AddedAt, 541 + } 542 + 543 + // Get tier limit 544 + if limit := ui.quotaMgr.GetTierLimit(member.Tier); limit != nil { 545 + view.TierLimit = quota.FormatHumanBytes(*limit) 546 + } else { 547 + view.TierLimit = "Unlimited" 548 + } 549 + 550 + // Get usage (expensive - consider caching) 551 + usage, _, tier, limit, _ := ui.pds.GetQuotaForUserWithTier(ctx, member.Member, ui.quotaMgr) 552 + view.CurrentUsage = usage 553 + view.UsageHuman = quota.FormatHumanBytes(usage) 554 + if limit != nil && *limit > 0 { 555 + view.UsagePercent = int(float64(usage) / float64(*limit) * 100) 556 + } 557 + 558 + crewViews = append(crewViews, view) 559 + } 560 + 561 + // Sort by usage (highest first) 562 + sort.Slice(crewViews, func(i, j int) bool { 563 + return crewViews[i].CurrentUsage > crewViews[j].CurrentUsage 564 + }) 565 + 566 + data := struct { 567 + AdminPageData 568 + Crew []CrewMemberView 569 + Tiers []TierOption 570 + }{ 571 + AdminPageData: ui.newPageData(r), 572 + Crew: crewViews, 573 + Tiers: ui.getTierOptions(), 574 + } 575 + 576 + ui.templates.ExecuteTemplate(w, "crew", data) 577 + } 578 + ``` 579 + 580 + ### Add Crew Handler 581 + 582 + ```go 583 + func (ui *AdminUI) handleCrewAdd(w http.ResponseWriter, r *http.Request) { 584 + ctx := r.Context() 585 + 586 + if err := r.ParseForm(); err != nil { 587 + ui.setFlash(w, "error", "Invalid form data") 588 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 589 + return 590 + } 591 + 592 + did := strings.TrimSpace(r.FormValue("did")) 593 + role := r.FormValue("role") 594 + tier := r.FormValue("tier") 595 + 596 + // Parse permissions checkboxes 597 + var permissions []string 598 + if r.FormValue("perm_read") == "on" { 599 + permissions = append(permissions, "blob:read") 600 + } 601 + if r.FormValue("perm_write") == "on" { 602 + permissions = append(permissions, "blob:write") 603 + } 604 + if r.FormValue("perm_admin") == "on" { 605 + permissions = append(permissions, "crew:admin") 606 + } 607 + 608 + // Validate DID format 609 + if !strings.HasPrefix(did, "did:") { 610 + ui.setFlash(w, "error", "Invalid DID format") 611 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 612 + return 613 + } 614 + 615 + // Add crew member 616 + _, err := ui.pds.AddCrewMember(ctx, did, role, permissions) 617 + if err != nil { 618 + ui.setFlash(w, "error", "Failed to add crew member: "+err.Error()) 619 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 620 + return 621 + } 622 + 623 + // Set tier if specified 624 + if tier != "" && tier != ui.quotaMgr.GetDefaultTier() { 625 + if err := ui.pds.UpdateCrewMemberTier(ctx, did, tier); err != nil { 626 + slog.Warn("Failed to set tier for new crew member", "did", did, "tier", tier, "error", err) 627 + } 628 + } 629 + 630 + ui.setFlash(w, "success", "Crew member added successfully") 631 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 632 + } 633 + ``` 634 + 635 + ### Update Crew Handler 636 + 637 + ```go 638 + func (ui *AdminUI) handleCrewUpdate(w http.ResponseWriter, r *http.Request) { 639 + ctx := r.Context() 640 + rkey := chi.URLParam(r, "rkey") 641 + 642 + if err := r.ParseForm(); err != nil { 643 + ui.setFlash(w, "error", "Invalid form data") 644 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 645 + return 646 + } 647 + 648 + // Get current crew member 649 + current, err := ui.pds.GetCrewMemberByRKey(ctx, rkey) 650 + if err != nil { 651 + ui.setFlash(w, "error", "Crew member not found") 652 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 653 + return 654 + } 655 + 656 + // Parse new values 657 + role := r.FormValue("role") 658 + tier := r.FormValue("tier") 659 + 660 + var permissions []string 661 + if r.FormValue("perm_read") == "on" { 662 + permissions = append(permissions, "blob:read") 663 + } 664 + if r.FormValue("perm_write") == "on" { 665 + permissions = append(permissions, "blob:write") 666 + } 667 + if r.FormValue("perm_admin") == "on" { 668 + permissions = append(permissions, "crew:admin") 669 + } 670 + 671 + // Update tier if changed 672 + if tier != current.Tier { 673 + if err := ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier); err != nil { 674 + ui.setFlash(w, "error", "Failed to update tier: "+err.Error()) 675 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 676 + return 677 + } 678 + } 679 + 680 + // For role/permissions changes, need to delete and recreate 681 + // (ATProto records are immutable, updates require delete+create) 682 + if role != current.Role || !slicesEqual(permissions, current.Permissions) { 683 + // Delete old record 684 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 685 + ui.setFlash(w, "error", "Failed to update: "+err.Error()) 686 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 687 + return 688 + } 689 + 690 + // Create new record with updated values 691 + if _, err := ui.pds.AddCrewMember(ctx, current.Member, role, permissions); err != nil { 692 + ui.setFlash(w, "error", "Failed to recreate crew record: "+err.Error()) 693 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 694 + return 695 + } 696 + 697 + // Re-apply tier to new record 698 + if tier != "" { 699 + ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier) 700 + } 701 + } 702 + 703 + ui.setFlash(w, "success", "Crew member updated successfully") 704 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 705 + } 706 + ``` 707 + 708 + ### Delete Crew Handler 709 + 710 + ```go 711 + func (ui *AdminUI) handleCrewDelete(w http.ResponseWriter, r *http.Request) { 712 + ctx := r.Context() 713 + rkey := chi.URLParam(r, "rkey") 714 + 715 + // Get crew member to log who was deleted 716 + member, err := ui.pds.GetCrewMemberByRKey(ctx, rkey) 717 + if err != nil { 718 + ui.setFlash(w, "error", "Crew member not found") 719 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 720 + return 721 + } 722 + 723 + // Prevent deleting self (captain) 724 + session := getAdminSession(ctx) 725 + if member.Member == session.DID { 726 + ui.setFlash(w, "error", "Cannot remove yourself from crew") 727 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 728 + return 729 + } 730 + 731 + // Delete 732 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 733 + ui.setFlash(w, "error", "Failed to remove crew member: "+err.Error()) 734 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 735 + return 736 + } 737 + 738 + slog.Info("Crew member removed via admin panel", "did", member.Member, "by", session.DID) 739 + 740 + // For HTMX requests, return empty response (row will be removed) 741 + if r.Header.Get("HX-Request") == "true" { 742 + w.WriteHeader(http.StatusOK) 743 + return 744 + } 745 + 746 + ui.setFlash(w, "success", "Crew member removed") 747 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 748 + } 749 + ``` 750 + 751 + ### Settings Handler 752 + 753 + ```go 754 + func (ui *AdminUI) handleSettings(w http.ResponseWriter, r *http.Request) { 755 + ctx := r.Context() 756 + 757 + _, captain, err := ui.pds.GetCaptainRecord(ctx) 758 + if err != nil { 759 + ui.renderError(w, "Failed to load settings: "+err.Error()) 760 + return 761 + } 762 + 763 + data := struct { 764 + AdminPageData 765 + Settings struct { 766 + Public bool 767 + AllowAllCrew bool 768 + EnableBlueskyPosts bool 769 + OwnerDID string 770 + HoldDID string 771 + } 772 + }{ 773 + AdminPageData: ui.newPageData(r), 774 + } 775 + data.Settings.Public = captain.Public 776 + data.Settings.AllowAllCrew = captain.AllowAllCrew 777 + data.Settings.EnableBlueskyPosts = captain.EnableBlueskyPosts 778 + data.Settings.OwnerDID = captain.Owner 779 + data.Settings.HoldDID = ui.pds.DID() 780 + 781 + ui.templates.ExecuteTemplate(w, "settings", data) 782 + } 783 + 784 + func (ui *AdminUI) handleSettingsUpdate(w http.ResponseWriter, r *http.Request) { 785 + ctx := r.Context() 786 + 787 + if err := r.ParseForm(); err != nil { 788 + ui.setFlash(w, "error", "Invalid form data") 789 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 790 + return 791 + } 792 + 793 + public := r.FormValue("public") == "on" 794 + allowAllCrew := r.FormValue("allow_all_crew") == "on" 795 + enablePosts := r.FormValue("enable_bluesky_posts") == "on" 796 + 797 + _, err := ui.pds.UpdateCaptainRecord(ctx, public, allowAllCrew, enablePosts) 798 + if err != nil { 799 + ui.setFlash(w, "error", "Failed to update settings: "+err.Error()) 800 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 801 + return 802 + } 803 + 804 + ui.setFlash(w, "success", "Settings updated successfully") 805 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 806 + } 807 + ``` 808 + 809 + ### Metrics Handler (for HTMX lazy loading) 810 + 811 + ```go 812 + func (ui *AdminUI) handleStatsAPI(w http.ResponseWriter, r *http.Request) { 813 + ctx := r.Context() 814 + 815 + // Calculate total storage (expensive operation) 816 + // Iterate through all layer records 817 + records, _, err := ui.pds.RecordsIndex().ListRecords(atproto.LayerCollection, 100000, "", true) 818 + if err != nil { 819 + http.Error(w, "Failed to load stats", http.StatusInternalServerError) 820 + return 821 + } 822 + 823 + var totalSize int64 824 + uniqueDigests := make(map[string]bool) 825 + userUsage := make(map[string]int64) 826 + 827 + for _, record := range records { 828 + var layer atproto.LayerRecord 829 + if err := json.Unmarshal(record.Value, &layer); err != nil { 830 + continue 831 + } 832 + 833 + if !uniqueDigests[layer.Digest] { 834 + uniqueDigests[layer.Digest] = true 835 + totalSize += layer.Size 836 + } 837 + 838 + userUsage[layer.UserDID] += layer.Size 839 + } 840 + 841 + stats := struct { 842 + TotalBlobs int `json:"totalBlobs"` 843 + TotalSize int64 `json:"totalSize"` 844 + TotalHuman string `json:"totalHuman"` 845 + }{ 846 + TotalBlobs: len(uniqueDigests), 847 + TotalSize: totalSize, 848 + TotalHuman: quota.FormatHumanBytes(totalSize), 849 + } 850 + 851 + // If HTMX request, return HTML partial 852 + if r.Header.Get("HX-Request") == "true" { 853 + data := struct { 854 + Stats interface{} 855 + }{Stats: stats} 856 + ui.templates.ExecuteTemplate(w, "usage_stats", data) 857 + return 858 + } 859 + 860 + // Otherwise return JSON 861 + w.Header().Set("Content-Type", "application/json") 862 + json.NewEncoder(w).Encode(stats) 863 + } 864 + ``` 865 + 866 + --- 867 + 868 + ## Templates 869 + 870 + ### Base Layout (templates/base.html) 871 + 872 + ```html 873 + {{ define "base" }} 874 + <!DOCTYPE html> 875 + <html lang="en"> 876 + <head> 877 + <meta charset="UTF-8"> 878 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 879 + <title>{{ .Title }} - Hold Admin</title> 880 + {{ template "head" . }} 881 + </head> 882 + <body> 883 + {{ template "nav" . }} 884 + 885 + <main class="admin-container"> 886 + {{ template "flash" . }} 887 + {{ template "content" . }} 888 + </main> 889 + 890 + <footer class="admin-footer"> 891 + <p>Hold: {{ .HoldDID }}</p> 892 + </footer> 893 + </body> 894 + </html> 895 + {{ end }} 896 + ``` 897 + 898 + ### Head Component (templates/components/head.html) 899 + 900 + ```html 901 + {{ define "head" }} 902 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 903 + <script src="https://unpkg.com/htmx.org@1.9.10"></script> 904 + <script src="https://unpkg.com/lucide@latest"></script> 905 + {{ end }} 906 + ``` 907 + 908 + ### Navigation (templates/components/nav.html) 909 + 910 + ```html 911 + {{ define "nav" }} 912 + <nav class="admin-nav"> 913 + <div class="nav-brand"> 914 + <a href="/admin">Hold Admin</a> 915 + </div> 916 + <ul class="nav-links"> 917 + <li><a href="/admin" class="{{ if eq .ActivePage "dashboard" }}active{{ end }}">Dashboard</a></li> 918 + <li><a href="/admin/crew" class="{{ if eq .ActivePage "crew" }}active{{ end }}">Crew</a></li> 919 + <li><a href="/admin/settings" class="{{ if eq .ActivePage "settings" }}active{{ end }}">Settings</a></li> 920 + </ul> 921 + <div class="nav-user"> 922 + <span>{{ .User.Handle }}</span> 923 + <a href="/admin/auth/logout">Logout</a> 924 + </div> 925 + </nav> 926 + {{ end }} 927 + ``` 928 + 929 + ### Dashboard Page (templates/pages/dashboard.html) 930 + 931 + ```html 932 + {{ define "dashboard" }} 933 + {{ template "base" . }} 934 + {{ define "content" }} 935 + <h1>Dashboard</h1> 936 + 937 + <div class="stats-grid"> 938 + <div class="stat-card"> 939 + <h3>Crew Members</h3> 940 + <p class="stat-value">{{ .Stats.TotalCrewMembers }}</p> 941 + </div> 942 + 943 + <div class="stat-card" hx-get="/admin/api/stats" hx-trigger="load" hx-swap="innerHTML"> 944 + <p>Loading storage stats...</p> 945 + </div> 946 + </div> 947 + 948 + <section class="dashboard-section"> 949 + <h2>Tier Distribution</h2> 950 + <div class="tier-chart"> 951 + {{ range $tier, $count := .Stats.TierDistribution }} 952 + <div class="tier-bar"> 953 + <span class="tier-name">{{ $tier }}</span> 954 + <span class="tier-count">{{ $count }}</span> 955 + </div> 956 + {{ end }} 957 + </div> 958 + </section> 959 + 960 + <section class="dashboard-section"> 961 + <h2>Top Users by Storage</h2> 962 + <div hx-get="/admin/api/top-users?limit=10" hx-trigger="load" hx-swap="innerHTML"> 963 + <p>Loading top users...</p> 964 + </div> 965 + </section> 966 + {{ end }} 967 + {{ end }} 968 + ``` 969 + 970 + ### Crew List Page (templates/pages/crew.html) 971 + 972 + ```html 973 + {{ define "crew" }} 974 + {{ template "base" . }} 975 + {{ define "content" }} 976 + <div class="page-header"> 977 + <h1>Crew Members</h1> 978 + <a href="/admin/crew/add" class="btn btn-primary">Add Crew Member</a> 979 + </div> 980 + 981 + <table class="data-table"> 982 + <thead> 983 + <tr> 984 + <th>DID</th> 985 + <th>Role</th> 986 + <th>Permissions</th> 987 + <th>Tier</th> 988 + <th>Usage</th> 989 + <th>Actions</th> 990 + </tr> 991 + </thead> 992 + <tbody id="crew-list"> 993 + {{ range .Crew }} 994 + {{ template "crew_row" . }} 995 + {{ end }} 996 + </tbody> 997 + </table> 998 + {{ end }} 999 + {{ end }} 1000 + ``` 1001 + 1002 + ### Crew Row Partial (templates/partials/crew_row.html) 1003 + 1004 + ```html 1005 + {{ define "crew_row" }} 1006 + <tr id="crew-{{ .RKey }}"> 1007 + <td> 1008 + <code title="{{ .DID }}">{{ .DID | truncate 20 }}</code> 1009 + {{ if .Plankowner }}<span class="badge badge-gold">Plankowner</span>{{ end }} 1010 + </td> 1011 + <td>{{ .Role }}</td> 1012 + <td> 1013 + {{ range .Permissions }} 1014 + <span class="badge badge-perm">{{ . }}</span> 1015 + {{ end }} 1016 + </td> 1017 + <td> 1018 + <span class="badge badge-tier tier-{{ .Tier }}">{{ .Tier }}</span> 1019 + <small>({{ .TierLimit }})</small> 1020 + </td> 1021 + <td> 1022 + <div class="usage-cell"> 1023 + <span>{{ .UsageHuman }}</span> 1024 + <div class="progress-bar"> 1025 + <div class="progress-fill {{ if gt .UsagePercent 90 }}danger{{ else if gt .UsagePercent 75 }}warning{{ end }}" 1026 + style="width: {{ .UsagePercent }}%"></div> 1027 + </div> 1028 + <small>{{ .UsagePercent }}%</small> 1029 + </div> 1030 + </td> 1031 + <td> 1032 + <a href="/admin/crew/{{ .RKey }}" class="btn btn-sm">Edit</a> 1033 + <button class="btn btn-sm btn-danger" 1034 + hx-post="/admin/crew/{{ .RKey }}/delete" 1035 + hx-confirm="Are you sure you want to remove this crew member?" 1036 + hx-target="#crew-{{ .RKey }}" 1037 + hx-swap="outerHTML"> 1038 + Delete 1039 + </button> 1040 + </td> 1041 + </tr> 1042 + {{ end }} 1043 + ``` 1044 + 1045 + ### Settings Page (templates/pages/settings.html) 1046 + 1047 + ```html 1048 + {{ define "settings" }} 1049 + {{ template "base" . }} 1050 + {{ define "content" }} 1051 + <h1>Hold Settings</h1> 1052 + 1053 + <form action="/admin/settings/update" method="POST" class="settings-form"> 1054 + <div class="setting-group"> 1055 + <h2>Access Control</h2> 1056 + 1057 + <label class="toggle-setting"> 1058 + <input type="checkbox" name="public" {{ if .Settings.Public }}checked{{ end }}> 1059 + <span class="toggle-label"> 1060 + <strong>Public Hold</strong> 1061 + <small>Allow anonymous users to read blobs (no auth required for pulls)</small> 1062 + </span> 1063 + </label> 1064 + 1065 + <label class="toggle-setting"> 1066 + <input type="checkbox" name="allow_all_crew" {{ if .Settings.AllowAllCrew }}checked{{ end }}> 1067 + <span class="toggle-label"> 1068 + <strong>Open Registration</strong> 1069 + <small>Allow any authenticated user to join as crew via requestCrew</small> 1070 + </span> 1071 + </label> 1072 + </div> 1073 + 1074 + <div class="setting-group"> 1075 + <h2>Integrations</h2> 1076 + 1077 + <label class="toggle-setting"> 1078 + <input type="checkbox" name="enable_bluesky_posts" {{ if .Settings.EnableBlueskyPosts }}checked{{ end }}> 1079 + <span class="toggle-label"> 1080 + <strong>Bluesky Posts</strong> 1081 + <small>Post to Bluesky when images are pushed to this hold</small> 1082 + </span> 1083 + </label> 1084 + </div> 1085 + 1086 + <div class="setting-group"> 1087 + <h2>Hold Information</h2> 1088 + <dl> 1089 + <dt>Hold DID</dt> 1090 + <dd><code>{{ .Settings.HoldDID }}</code></dd> 1091 + <dt>Owner DID</dt> 1092 + <dd><code>{{ .Settings.OwnerDID }}</code></dd> 1093 + </dl> 1094 + </div> 1095 + 1096 + <button type="submit" class="btn btn-primary">Save Settings</button> 1097 + </form> 1098 + {{ end }} 1099 + {{ end }} 1100 + ``` 1101 + 1102 + --- 1103 + 1104 + ## Environment Variables 1105 + 1106 + Add to `.env.hold.example`: 1107 + 1108 + ```bash 1109 + # ============================================================================= 1110 + # ADMIN PANEL CONFIGURATION 1111 + # ============================================================================= 1112 + 1113 + # Enable the admin web UI (default: false) 1114 + # When enabled, accessible at /admin 1115 + HOLD_ADMIN_ENABLED=false 1116 + 1117 + # Admin session duration (default: 24h) 1118 + # How long admin sessions remain valid before requiring re-authentication 1119 + # Format: Go duration string (e.g., 24h, 168h for 1 week) 1120 + HOLD_ADMIN_SESSION_DURATION=24h 1121 + ``` 1122 + 1123 + ### Config Struct Updates 1124 + 1125 + ```go 1126 + // In pkg/hold/config.go 1127 + 1128 + type Config struct { 1129 + // ... existing fields ... 1130 + 1131 + Admin AdminConfig 1132 + } 1133 + 1134 + type AdminConfig struct { 1135 + Enabled bool `env:"HOLD_ADMIN_ENABLED" envDefault:"false"` 1136 + SessionDuration time.Duration `env:"HOLD_ADMIN_SESSION_DURATION" envDefault:"24h"` 1137 + } 1138 + ``` 1139 + 1140 + --- 1141 + 1142 + ## Security Considerations 1143 + 1144 + ### 1. Owner-Only Access 1145 + 1146 + All admin routes validate that the authenticated user's DID matches `captain.Owner`. This check happens: 1147 + - In the OAuth callback (primary gate) 1148 + - In the `requireOwner` middleware (defense in depth) 1149 + - Before destructive operations (extra validation) 1150 + 1151 + ### 2. Cookie Security 1152 + 1153 + ```go 1154 + http.SetCookie(w, &http.Cookie{ 1155 + Name: "hold_admin_session", 1156 + Value: sessionID, 1157 + Path: "/admin", // Scoped to admin paths only 1158 + MaxAge: 86400, // 24 hours 1159 + HttpOnly: true, // No JavaScript access 1160 + Secure: isHTTPS(r), // HTTPS only in production 1161 + SameSite: http.SameSiteLaxMode, // CSRF protection 1162 + }) 1163 + ``` 1164 + 1165 + ### 3. CSRF Protection 1166 + 1167 + For state-changing operations: 1168 + - Forms include hidden CSRF token 1169 + - HTMX requests include token in header 1170 + - Server validates token before processing 1171 + 1172 + ```html 1173 + <form action="/admin/crew/add" method="POST"> 1174 + <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> 1175 + ... 1176 + </form> 1177 + ``` 1178 + 1179 + ### 4. Input Validation 1180 + 1181 + - DID format validation before database operations 1182 + - Tier names validated against `quotas.yaml` 1183 + - Permission values validated against known set 1184 + - All user input sanitized before display 1185 + 1186 + ### 5. Rate Limiting 1187 + 1188 + Consider adding rate limiting for: 1189 + - Login attempts (prevent brute force) 1190 + - OAuth flow starts (prevent abuse) 1191 + - API endpoints (prevent DoS) 1192 + 1193 + ### 6. Audit Logging 1194 + 1195 + Log all administrative actions: 1196 + ```go 1197 + slog.Info("Admin action", 1198 + "action", "crew_add", 1199 + "admin_did", session.DID, 1200 + "target_did", newMemberDID, 1201 + "permissions", permissions) 1202 + ``` 1203 + 1204 + --- 1205 + 1206 + ## Implementation Phases 1207 + 1208 + ### Phase 1: Foundation (Est. 4-6 hours) 1209 + 1210 + 1. Create `pkg/hold/admin/` package structure 1211 + 2. Implement `AdminSessionStore` with SQLite 1212 + 3. Implement OAuth client setup (reuse `pkg/auth/oauth/`) 1213 + 4. Implement `requireOwner` middleware 1214 + 5. Create basic template loading with embed.FS 1215 + 6. Add env var configuration to `pkg/hold/config.go` 1216 + 1217 + **Deliverables:** 1218 + - Admin package compiles 1219 + - Can start OAuth flow 1220 + - Session store creates/validates sessions 1221 + 1222 + ### Phase 2: Authentication (Est. 3-4 hours) 1223 + 1224 + 1. Implement login page handler 1225 + 2. Implement OAuth authorize redirect 1226 + 3. Implement callback with owner validation 1227 + 4. Implement logout handler 1228 + 5. Wire up routes in `cmd/hold/main.go` 1229 + 1230 + **Deliverables:** 1231 + - Can login as hold owner 1232 + - Non-owners rejected at callback 1233 + - Sessions persist across requests 1234 + 1235 + ### Phase 3: Dashboard (Est. 3-4 hours) 1236 + 1237 + 1. Create base template and navigation 1238 + 2. Implement dashboard handler with basic stats 1239 + 3. Implement stats API for HTMX lazy loading 1240 + 4. Implement top users API 1241 + 5. Create dashboard template 1242 + 1243 + **Deliverables:** 1244 + - Dashboard shows crew count, tier distribution 1245 + - Storage stats load asynchronously 1246 + - Top users table displays 1247 + 1248 + ### Phase 4: Crew Management (Est. 4-6 hours) 1249 + 1250 + 1. Implement crew list handler 1251 + 2. Create crew list template with HTMX delete 1252 + 3. Implement add crew form and handler 1253 + 4. Implement edit crew form and handler 1254 + 5. Implement delete crew handler 1255 + 1256 + **Deliverables:** 1257 + - Full CRUD for crew members 1258 + - Tier and permission editing works 1259 + - HTMX updates without page reload 1260 + 1261 + ### Phase 5: Settings (Est. 2-3 hours) 1262 + 1263 + 1. Implement settings handler 1264 + 2. Create settings template 1265 + 3. Implement settings update handler 1266 + 1267 + **Deliverables:** 1268 + - Can toggle public/allowAllCrew/enableBlueskyPosts 1269 + - Settings persist correctly 1270 + 1271 + ### Phase 6: Polish (Est. 2-4 hours) 1272 + 1273 + 1. Add CSS styling 1274 + 2. Add flash messages 1275 + 3. Add CSRF protection 1276 + 4. Add input validation 1277 + 5. Add audit logging 1278 + 6. Update documentation 1279 + 1280 + **Deliverables:** 1281 + - Professional-looking UI 1282 + - Security hardening complete 1283 + - Documentation updated 1284 + 1285 + **Total Estimated Time: 18-27 hours** 1286 + 1287 + --- 1288 + 1289 + ## Testing Strategy 1290 + 1291 + ### Unit Tests 1292 + 1293 + ```go 1294 + // pkg/hold/admin/session_test.go 1295 + func TestSessionStore_Create(t *testing.T) { 1296 + store := newTestSessionStore(t) 1297 + 1298 + sessionID, err := store.Create("did:plc:test", "test.handle", 24*time.Hour) 1299 + require.NoError(t, err) 1300 + require.NotEmpty(t, sessionID) 1301 + 1302 + session, err := store.Get(sessionID) 1303 + require.NoError(t, err) 1304 + assert.Equal(t, "did:plc:test", session.DID) 1305 + } 1306 + 1307 + // pkg/hold/admin/auth_test.go 1308 + func TestRequireOwner_RejectsNonOwner(t *testing.T) { 1309 + pds := setupTestPDSWithOwner(t, "did:plc:owner") 1310 + store := newTestSessionStore(t) 1311 + 1312 + // Create session for non-owner 1313 + sessionID, _ := store.Create("did:plc:notowner", "notowner", 24*time.Hour) 1314 + 1315 + middleware := requireOwner(pds, store) 1316 + handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1317 + w.WriteHeader(http.StatusOK) 1318 + })) 1319 + 1320 + req := httptest.NewRequest("GET", "/admin", nil) 1321 + req.AddCookie(&http.Cookie{Name: "hold_admin_session", Value: sessionID}) 1322 + w := httptest.NewRecorder() 1323 + 1324 + handler.ServeHTTP(w, req) 1325 + 1326 + assert.Equal(t, http.StatusForbidden, w.Code) 1327 + } 1328 + ``` 1329 + 1330 + ### Integration Tests 1331 + 1332 + ```go 1333 + // pkg/hold/admin/integration_test.go 1334 + func TestAdminLoginFlow(t *testing.T) { 1335 + // Start test hold server 1336 + server := startTestHoldWithAdmin(t) 1337 + defer server.Close() 1338 + 1339 + // Verify login page accessible 1340 + resp, _ := http.Get(server.URL + "/admin/auth/login") 1341 + assert.Equal(t, http.StatusOK, resp.StatusCode) 1342 + 1343 + // Verify dashboard redirects to login 1344 + client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { 1345 + return http.ErrUseLastResponse 1346 + }} 1347 + resp, _ = client.Get(server.URL + "/admin") 1348 + assert.Equal(t, http.StatusFound, resp.StatusCode) 1349 + assert.Contains(t, resp.Header.Get("Location"), "/admin/auth/login") 1350 + } 1351 + ``` 1352 + 1353 + ### Manual Testing Checklist 1354 + 1355 + - [ ] Login as owner succeeds 1356 + - [ ] Login as non-owner fails with clear error 1357 + - [ ] Dashboard loads with correct stats 1358 + - [ ] Add crew member with all permission combinations 1359 + - [ ] Edit crew member permissions 1360 + - [ ] Change crew member tier 1361 + - [ ] Delete crew member 1362 + - [ ] Toggle public setting 1363 + - [ ] Toggle allowAllCrew setting 1364 + - [ ] Toggle enableBlueskyPosts setting 1365 + - [ ] Logout clears session 1366 + - [ ] Session expires after configured duration 1367 + - [ ] Expired session redirects to login 1368 + 1369 + --- 1370 + 1371 + ## Future Enhancements 1372 + 1373 + ### Potential Future Features 1374 + 1375 + 1. **Crew Invite Links** - Generate one-time invite URLs for adding crew 1376 + 2. **Usage Alerts** - Email/webhook when users approach quota 1377 + 3. **Bulk Operations** - Add/remove multiple crew members at once 1378 + 4. **Export Data** - Download crew list, usage reports as CSV 1379 + 5. **Activity Log** - View recent admin actions 1380 + 6. **API Keys** - Generate programmatic access keys for admin API 1381 + 7. **Backup/Restore** - Backup crew records, restore from backup 1382 + 8. **Multi-Hold Management** - Manage multiple holds from one UI (separate feature) 1383 + 1384 + ### Performance Optimizations 1385 + 1386 + 1. **Cache usage stats** - Don't recalculate on every request 1387 + 2. **Paginate crew list** - Handle holds with 1000+ crew members 1388 + 3. **Background stat refresh** - Update stats periodically in background 1389 + 4. **Batch DID resolution** - Resolve multiple DIDs in parallel 1390 + 1391 + --- 1392 + 1393 + ## References 1394 + 1395 + - [ATProto OAuth Specification](https://atproto.com/specs/oauth) 1396 + - [DPoP RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449) 1397 + - [HTMX Documentation](https://htmx.org/docs/) 1398 + - [Chi Router](https://github.com/go-chi/chi) 1399 + - [Go html/template](https://pkg.go.dev/html/template)
+1 -1
docs/INTEGRATION_STRATEGY.md
··· 251 return 252 } 253 254 - json.NewEncoder(w).Encode(map[string]interface{}{ 255 "verified": result.Verified, 256 "did": result.Signature.DID, 257 "signedAt": result.Signature.SignedAt,
··· 251 return 252 } 253 254 + json.NewEncoder(w).Encode(map[string]any{ 255 "verified": result.Verified, 256 "did": result.Signature.DID, 257 "signedAt": result.Signature.SignedAt,
+354 -1068
docs/QUOTAS.md
··· 1 # ATCR Quota System 2 3 - This document describes ATCR's storage quota implementation, inspired by Harbor's proven approach to per-project blob tracking with deduplication. 4 5 ## Table of Contents 6 7 - [Overview](#overview) 8 - - [Harbor's Approach (Reference Implementation)](#harbors-approach-reference-implementation) 9 - - [Storage Options](#storage-options) 10 - - [Quota Data Model](#quota-data-model) 11 - - [Push Flow (Detailed)](#push-flow-detailed) 12 - [Delete Flow](#delete-flow) 13 - [Garbage Collection](#garbage-collection) 14 - - [Quota Reconciliation](#quota-reconciliation) 15 - [Configuration](#configuration) 16 - - [Trade-offs & Design Decisions](#trade-offs--design-decisions) 17 - [Future Enhancements](#future-enhancements) 18 19 ## Overview 20 21 ATCR implements per-user storage quotas to: 22 1. **Limit storage consumption** on shared hold services 23 - 2. **Track actual S3 costs** (what new data was added) 24 - 3. **Benefit from deduplication** (users only pay once per layer) 25 - 4. **Provide transparency** (show users their storage usage) 26 27 - **Key principle:** Users pay for layers they've uploaded, but only ONCE per layer regardless of how many images reference it. 28 29 ### Example Scenario 30 31 ``` 32 Alice pushes myapp:v1 (layers A, B, C - each 100MB) 33 - โ†’ Alice's quota: +300MB (all new layers) 34 35 Alice pushes myapp:v2 (layers A, B, D) 36 - โ†’ Layers A, B already claimed by Alice 37 - โ†’ Layer D is new (100MB) 38 - โ†’ Alice's quota: +100MB (only D is new) 39 - โ†’ Total: 400MB 40 41 Bob pushes his-app:latest (layers A, E) 42 - โ†’ Layer A already exists in S3 (uploaded by Alice) 43 - โ†’ Bob claims it for first time โ†’ +100MB to Bob's quota 44 - โ†’ Layer E is new โ†’ +100MB to Bob's quota 45 - โ†’ Bob's quota: 200MB 46 47 - Physical S3 storage: 500MB (A, B, C, D, E) 48 - Claimed storage: 600MB (Alice: 400MB, Bob: 200MB) 49 - Deduplication savings: 100MB (layer A shared) 50 ``` 51 52 - ## Harbor's Approach (Reference Implementation) 53 54 - Harbor is built on distribution/distribution (same as ATCR) and implements quotas as middleware. Their approach: 55 56 - ### Key Insights from Harbor 57 - 58 - 1. **"Shared blobs are only computed once per project"** 59 - - Each project tracks which blobs it has uploaded 60 - - Same blob used in multiple images counts only once per project 61 - - Different projects claiming the same blob each pay for it 62 - 63 - 2. **Quota checked when manifest is pushed** 64 - - Blobs upload first (presigned URLs, can't intercept) 65 - - Manifest pushed last โ†’ quota check happens here 66 - - Can reject manifest if quota exceeded (orphaned blobs cleaned by GC) 67 68 - 3. **Middleware-based implementation** 69 - - distribution/distribution has NO built-in quota support 70 - - Harbor added it as request preprocessing middleware 71 - - Uses database (PostgreSQL) or Redis for quota storage 72 73 - 4. **Per-project ownership model** 74 - - Blobs are physically deduplicated globally 75 - - Quota accounting is logical (per-project claims) 76 - - Total claimed storage can exceed physical storage 77 78 - ### References 79 80 - - Harbor Quota Documentation: https://goharbor.io/docs/1.10/administration/configure-project-quotas/ 81 - - Harbor Source: https://github.com/goharbor/harbor (see `src/controller/quota`) 82 83 - ## Storage Options 84 85 - The hold service needs to store quota data somewhere. Two options: 86 87 - ### Option 1: S3-Based Storage (Recommended for BYOS) 88 89 - Store quota metadata alongside blobs in the same S3 bucket: 90 91 - ``` 92 - Bucket structure: 93 - /docker/registry/v2/blobs/sha256/ab/abc123.../data โ† actual blobs 94 - /atcr/quota/did:plc:alice.json โ† quota tracking 95 - /atcr/quota/did:plc:bob.json 96 ``` 97 98 - **Pros:** 99 - - โœ… No separate database needed 100 - - โœ… Single S3 bucket (better UX - no second bucket to configure) 101 - - โœ… Quota data lives with the blobs 102 - - โœ… Hold service stays relatively stateless 103 - - โœ… Works with any S3-compatible service (Storj, Minio, Upcloud, Fly.io) 104 105 - **Cons:** 106 - - โŒ Slower than local database (network round-trip) 107 - - โŒ Eventual consistency issues 108 - - โŒ Race conditions on concurrent updates 109 - - โŒ Extra S3 API costs (GET/PUT per upload) 110 - 111 - **Performance:** 112 - - Each blob upload: 1 HEAD (blob exists?) + 1 GET (quota) + 1 PUT (update quota) 113 - - Typical latency: 100-200ms total overhead 114 - - For high-throughput registries, consider SQLite 115 116 - ### Option 2: SQLite Database (Recommended for Shared Holds) 117 118 - Local database in hold service: 119 - 120 - ```bash 121 - /var/lib/atcr/hold-quota.db 122 ``` 123 - 124 - **Pros:** 125 - - โœ… Fast local queries (no network latency) 126 - - โœ… ACID transactions (no race conditions) 127 - - โœ… Efficient for high-throughput registries 128 - - โœ… Can use foreign keys and joins 129 - 130 - **Cons:** 131 - - โŒ Makes hold service stateful (persistent volume needed) 132 - - โŒ Not ideal for ephemeral BYOS deployments 133 - - โŒ Backup/restore complexity 134 - - โŒ Multi-instance scaling requires shared database 135 - 136 - **Schema:** 137 - ```sql 138 - CREATE TABLE user_quotas ( 139 - did TEXT PRIMARY KEY, 140 - quota_limit INTEGER NOT NULL DEFAULT 10737418240, -- 10GB 141 - quota_used INTEGER NOT NULL DEFAULT 0, 142 - updated_at TIMESTAMP 143 - ); 144 145 - CREATE TABLE claimed_layers ( 146 - did TEXT NOT NULL, 147 - digest TEXT NOT NULL, 148 - size INTEGER NOT NULL, 149 - claimed_at TIMESTAMP, 150 - PRIMARY KEY(did, digest) 151 - ); 152 ``` 153 154 - ### Recommendation 155 - 156 - - **BYOS (user-owned holds):** S3-based (keeps hold service ephemeral) 157 - - **Shared holds (multi-user):** SQLite (better performance and consistency) 158 - - **High-traffic production:** SQLite or PostgreSQL (Harbor uses this) 159 160 - ## Quota Data Model 161 162 - ### Quota File Format (S3-based) 163 - 164 - ```json 165 - { 166 - "did": "did:plc:alice123", 167 - "limit": 10737418240, 168 - "used": 5368709120, 169 - "claimed_layers": { 170 - "sha256:abc123...": 104857600, 171 - "sha256:def456...": 52428800, 172 - "sha256:789ghi...": 209715200 173 - }, 174 - "last_updated": "2025-10-09T12:34:56Z", 175 - "version": 1 176 - } 177 ``` 178 179 - **Fields:** 180 - - `did`: User's ATProto DID 181 - - `limit`: Maximum storage in bytes (default: 10GB) 182 - - `used`: Current storage usage in bytes (sum of claimed_layers) 183 - - `claimed_layers`: Map of digest โ†’ size for all layers user has uploaded 184 - - `last_updated`: Timestamp of last quota update 185 - - `version`: Schema version for future migrations 186 - 187 - ### Why Track Individual Layers? 188 - 189 - **Q: Can't we just track a counter?** 190 - 191 - **A: We need layer tracking for:** 192 - 193 - 1. **Deduplication detection** 194 - - Check if user already claimed a layer โ†’ free upload 195 - - Example: Updating an image reuses most layers 196 - 197 - 2. **Accurate deletes** 198 - - When manifest deleted, only decrement unclaimed layers 199 - - User may have 5 images sharing layer A - deleting 1 image doesn't free layer A 200 - 201 - 3. **Quota reconciliation** 202 - - Verify quota matches reality by listing user's manifests 203 - - Recalculate from layers in manifests vs claimed_layers map 204 - 205 - 4. **Auditing** 206 - - "Show me what I'm storing" 207 - - Users can see which layers consume their quota 208 - 209 - ## Push Flow (Detailed) 210 - 211 - ### Step-by-Step: User Pushes Image 212 - 213 - ``` 214 - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 215 - โ”‚ Client โ”‚ โ”‚ Hold โ”‚ โ”‚ S3 โ”‚ 216 - โ”‚ (Docker) โ”‚ โ”‚ Service โ”‚ โ”‚ Bucket โ”‚ 217 - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 218 - โ”‚ โ”‚ โ”‚ 219 - โ”‚ 1. PUT /v2/.../blobs/ โ”‚ โ”‚ 220 - โ”‚ upload?digest=sha256:abcโ”‚ โ”‚ 221 - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 222 - โ”‚ โ”‚ โ”‚ 223 - โ”‚ โ”‚ 2. Check if blob exists โ”‚ 224 - โ”‚ โ”‚ (Stat/HEAD request) โ”‚ 225 - โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 226 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 227 - โ”‚ โ”‚ 200 OK (exists) or โ”‚ 228 - โ”‚ โ”‚ 404 Not Found โ”‚ 229 - โ”‚ โ”‚ โ”‚ 230 - โ”‚ โ”‚ 3. Read user quota โ”‚ 231 - โ”‚ โ”‚ GET /atcr/quota/{did} โ”‚ 232 - โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 233 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 234 - โ”‚ โ”‚ quota.json โ”‚ 235 - โ”‚ โ”‚ โ”‚ 236 - โ”‚ โ”‚ 4. Calculate quota impact โ”‚ 237 - โ”‚ โ”‚ - If digest in โ”‚ 238 - โ”‚ โ”‚ claimed_layers: 0 โ”‚ 239 - โ”‚ โ”‚ - Else: size โ”‚ 240 - โ”‚ โ”‚ โ”‚ 241 - โ”‚ โ”‚ 5. Check quota limit โ”‚ 242 - โ”‚ โ”‚ used + impact <= limit? โ”‚ 243 - โ”‚ โ”‚ โ”‚ 244 - โ”‚ โ”‚ 6. Update quota โ”‚ 245 - โ”‚ โ”‚ PUT /atcr/quota/{did} โ”‚ 246 - โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 247 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 248 - โ”‚ โ”‚ 200 OK โ”‚ 249 - โ”‚ โ”‚ โ”‚ 250 - โ”‚ 7. Presigned URL โ”‚ โ”‚ 251 - โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 252 - โ”‚ {url: "https://s3..."} โ”‚ โ”‚ 253 - โ”‚ โ”‚ โ”‚ 254 - โ”‚ 8. Upload blob to S3 โ”‚ โ”‚ 255 - โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 256 - โ”‚ โ”‚ โ”‚ 257 - โ”‚ 9. 200 OK โ”‚ โ”‚ 258 - โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 259 - โ”‚ โ”‚ โ”‚ 260 - ``` 261 262 - ### Implementation (Pseudocode) 263 264 ```go 265 - // cmd/hold/main.go - HandlePutPresignedURL 266 - 267 - func (s *HoldService) HandlePutPresignedURL(w http.ResponseWriter, r *http.Request) { 268 - var req PutPresignedURLRequest 269 - json.NewDecoder(r.Body).Decode(&req) 270 - 271 - // Step 1: Check if blob already exists in S3 272 - blobPath := fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/%s/data", 273 - algorithm, digest[:2], digest) 274 275 - _, err := s.driver.Stat(ctx, blobPath) 276 - blobExists := (err == nil) 277 278 - // Step 2: Read quota from S3 (or SQLite) 279 - quota, err := s.quotaManager.GetQuota(req.DID) 280 if err != nil { 281 - // First upload - create quota with defaults 282 - quota = &Quota{ 283 - DID: req.DID, 284 - Limit: s.config.QuotaDefaultLimit, 285 - Used: 0, 286 - ClaimedLayers: make(map[string]int64), 287 - } 288 } 289 290 - // Step 3: Calculate quota impact 291 - quotaImpact := req.Size // Default: assume new layer 292 - 293 - if _, alreadyClaimed := quota.ClaimedLayers[req.Digest]; alreadyClaimed { 294 - // User already uploaded this layer before 295 - quotaImpact = 0 296 - log.Printf("Layer %s already claimed by %s, no quota impact", 297 - req.Digest, req.DID) 298 - } else if blobExists { 299 - // Blob exists in S3 (uploaded by another user) 300 - // But this user is claiming it for first time 301 - // Still counts against their quota 302 - log.Printf("Layer %s exists globally but new to %s, quota impact: %d", 303 - req.Digest, req.DID, quotaImpact) 304 - } else { 305 - // Brand new blob - will be uploaded to S3 306 - log.Printf("New layer %s for %s, quota impact: %d", 307 - req.Digest, req.DID, quotaImpact) 308 } 309 310 - // Step 4: Check quota limit 311 - if quota.Used + quotaImpact > quota.Limit { 312 - http.Error(w, fmt.Sprintf( 313 - "quota exceeded: used=%d, impact=%d, limit=%d", 314 - quota.Used, quotaImpact, quota.Limit, 315 - ), http.StatusPaymentRequired) // 402 316 - return 317 } 318 319 - // Step 5: Update quota (optimistic - before upload completes) 320 - quota.Used += quotaImpact 321 - if quotaImpact > 0 { 322 - quota.ClaimedLayers[req.Digest] = req.Size 323 - } 324 - quota.LastUpdated = time.Now() 325 326 - if err := s.quotaManager.SaveQuota(quota); err != nil { 327 - http.Error(w, "failed to update quota", http.StatusInternalServerError) 328 - return 329 - } 330 - 331 - // Step 6: Generate presigned URL 332 - presignedURL, err := s.getUploadURL(ctx, req.Digest, req.Size, req.DID) 333 if err != nil { 334 - // Rollback quota update on error 335 - quota.Used -= quotaImpact 336 - delete(quota.ClaimedLayers, req.Digest) 337 - s.quotaManager.SaveQuota(quota) 338 - 339 - http.Error(w, "failed to generate presigned URL", http.StatusInternalServerError) 340 - return 341 - } 342 - 343 - // Step 7: Return presigned URL + quota info 344 - resp := PutPresignedURLResponse{ 345 - URL: presignedURL, 346 - ExpiresAt: time.Now().Add(15 * time.Minute), 347 - QuotaInfo: QuotaInfo{ 348 - Used: quota.Used, 349 - Limit: quota.Limit, 350 - Available: quota.Limit - quota.Used, 351 - Impact: quotaImpact, 352 - AlreadyClaimed: quotaImpact == 0, 353 - }, 354 } 355 356 - w.Header().Set("Content-Type", "application/json") 357 - json.NewEncoder(w).Encode(resp) 358 } 359 ``` 360 361 - ### Race Condition Handling 362 - 363 - **Problem:** Two concurrent uploads of the same blob 364 365 - ``` 366 - Time User A User B 367 - 0ms Upload layer X (100MB) 368 - 10ms Upload layer X (100MB) 369 - 20ms Check exists: NO Check exists: NO 370 - 30ms Quota impact: 100MB Quota impact: 100MB 371 - 40ms Update quota A: +100MB Update quota B: +100MB 372 - 50ms Generate presigned URL Generate presigned URL 373 - 100ms Upload to S3 completes Upload to S3 (overwrites A's) 374 ``` 375 376 - **Result:** Both users charged 100MB, but only 100MB stored in S3. 377 - 378 - **Mitigation strategies:** 379 - 380 - 1. **Accept eventual consistency** (recommended for S3-based) 381 - - Run periodic reconciliation to fix discrepancies 382 - - Small inconsistency window (minutes) is acceptable 383 - - Reconciliation uses PDS as source of truth 384 - 385 - 2. **Optimistic locking** (S3 ETags) 386 - ```go 387 - // Use S3 ETags for conditional writes 388 - oldETag := getQuotaFileETag(did) 389 - err := putQuotaFileWithCondition(quota, oldETag) 390 - if err == PreconditionFailed { 391 - // Retry with fresh read 392 - } 393 - ``` 394 - 395 - 3. **Database transactions** (SQLite-based) 396 - ```sql 397 - BEGIN TRANSACTION; 398 - SELECT * FROM user_quotas WHERE did = ? FOR UPDATE; 399 - UPDATE user_quotas SET used = used + ? WHERE did = ?; 400 - COMMIT; 401 - ``` 402 - 403 - ## Delete Flow 404 - 405 - ### Manifest Deletion via AppView UI 406 407 - When a user deletes a manifest through the AppView web interface: 408 409 ``` 410 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 411 - โ”‚ User โ”‚ โ”‚ AppView โ”‚ โ”‚ Hold โ”‚ โ”‚ PDS โ”‚ 412 - โ”‚ UI โ”‚ โ”‚ Database โ”‚ โ”‚ Service โ”‚ โ”‚ โ”‚ 413 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 414 โ”‚ โ”‚ โ”‚ โ”‚ 415 - โ”‚ DELETE manifest โ”‚ โ”‚ โ”‚ 416 โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 417 โ”‚ โ”‚ โ”‚ โ”‚ 418 - โ”‚ โ”‚ 1. Get manifest โ”‚ โ”‚ 419 - โ”‚ โ”‚ and layers โ”‚ โ”‚ 420 โ”‚ โ”‚ โ”‚ โ”‚ 421 - โ”‚ โ”‚ 2. Check which โ”‚ โ”‚ 422 - โ”‚ โ”‚ layers still โ”‚ โ”‚ 423 - โ”‚ โ”‚ referenced by โ”‚ โ”‚ 424 - โ”‚ โ”‚ user's other โ”‚ โ”‚ 425 - โ”‚ โ”‚ manifests โ”‚ โ”‚ 426 โ”‚ โ”‚ โ”‚ โ”‚ 427 - โ”‚ โ”‚ 3. DELETE manifest โ”‚ โ”‚ 428 - โ”‚ โ”‚ from PDS โ”‚ โ”‚ 429 โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 430 โ”‚ โ”‚ โ”‚ โ”‚ 431 - โ”‚ โ”‚ 4. POST /quota/decrement โ”‚ 432 โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 433 - โ”‚ โ”‚ {layers: [...]} โ”‚ โ”‚ 434 โ”‚ โ”‚ โ”‚ โ”‚ 435 - โ”‚ โ”‚ โ”‚ 5. Update quota โ”‚ 436 - โ”‚ โ”‚ โ”‚ Remove unclaimed โ”‚ 437 - โ”‚ โ”‚ โ”‚ layers โ”‚ 438 - โ”‚ โ”‚ โ”‚ โ”‚ 439 - โ”‚ โ”‚ 6. 200 OK โ”‚ โ”‚ 440 - โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 441 - โ”‚ โ”‚ โ”‚ โ”‚ 442 - โ”‚ โ”‚ 7. Delete from DB โ”‚ โ”‚ 443 - โ”‚ โ”‚ โ”‚ โ”‚ 444 - โ”‚ 8. Success โ”‚ โ”‚ โ”‚ 445 โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ 446 - โ”‚ โ”‚ โ”‚ โ”‚ 447 ``` 448 449 - ### AppView Implementation 450 451 ```go 452 - // pkg/appview/handlers/manifest.go 453 454 - func (h *ManifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) { 455 - did := r.Context().Value("auth.did").(string) 456 - repository := chi.URLParam(r, "repository") 457 - digest := chi.URLParam(r, "digest") 458 - 459 - // Step 1: Get manifest and its layers from database 460 - manifest, err := db.GetManifest(h.db, digest) 461 - if err != nil { 462 - http.Error(w, "manifest not found", 404) 463 - return 464 - } 465 466 - layers, err := db.GetLayersForManifest(h.db, manifest.ID) 467 if err != nil { 468 - http.Error(w, "failed to get layers", 500) 469 - return 470 } 471 472 - // Step 2: For each layer, check if user still references it 473 - // in other manifests 474 - layersToDecrement := []LayerInfo{} 475 - 476 for _, layer := range layers { 477 - // Query: does this user have other manifests using this layer? 478 - stillReferenced, err := db.CheckLayerReferencedByUser( 479 - h.db, did, repository, layer.Digest, manifest.ID, 480 - ) 481 - 482 - if err != nil { 483 - http.Error(w, "failed to check layer references", 500) 484 - return 485 - } 486 - 487 - if !stillReferenced { 488 - // This layer is no longer used by user 489 - layersToDecrement = append(layersToDecrement, LayerInfo{ 490 - Digest: layer.Digest, 491 - Size: layer.Size, 492 - }) 493 } 494 } 495 496 - // Step 3: Delete manifest from user's PDS 497 - atprotoClient := atproto.NewClient(manifest.PDSEndpoint, did, accessToken) 498 - err = atprotoClient.DeleteRecord(ctx, atproto.ManifestCollection, manifestRKey) 499 if err != nil { 500 - http.Error(w, "failed to delete from PDS", 500) 501 - return 502 } 503 - 504 - // Step 4: Notify hold service to decrement quota 505 - if len(layersToDecrement) > 0 { 506 - holdClient := &http.Client{} 507 - 508 - decrementReq := QuotaDecrementRequest{ 509 - DID: did, 510 - Layers: layersToDecrement, 511 - } 512 - 513 - body, _ := json.Marshal(decrementReq) 514 - resp, err := holdClient.Post( 515 - manifest.HoldEndpoint + "/quota/decrement", 516 - "application/json", 517 - bytes.NewReader(body), 518 - ) 519 - 520 - if err != nil || resp.StatusCode != 200 { 521 - log.Printf("Warning: failed to update quota on hold service: %v", err) 522 - // Continue anyway - GC reconciliation will fix it 523 - } 524 } 525 526 - // Step 5: Delete from AppView database 527 - err = db.DeleteManifest(h.db, did, repository, digest) 528 if err != nil { 529 - http.Error(w, "failed to delete from database", 500) 530 - return 531 } 532 533 - w.WriteHeader(http.StatusNoContent) 534 - } 535 - ``` 536 - 537 - ### Hold Service Decrement Endpoint 538 - 539 - ```go 540 - // cmd/hold/main.go 541 - 542 - type QuotaDecrementRequest struct { 543 - DID string `json:"did"` 544 - Layers []LayerInfo `json:"layers"` 545 - } 546 - 547 - type LayerInfo struct { 548 - Digest string `json:"digest"` 549 - Size int64 `json:"size"` 550 - } 551 - 552 - func (s *HoldService) HandleQuotaDecrement(w http.ResponseWriter, r *http.Request) { 553 - var req QuotaDecrementRequest 554 - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 555 - http.Error(w, "invalid request", 400) 556 - return 557 - } 558 - 559 - // Read current quota 560 - quota, err := s.quotaManager.GetQuota(req.DID) 561 - if err != nil { 562 - http.Error(w, "quota not found", 404) 563 - return 564 - } 565 - 566 - // Decrement quota for each layer 567 - for _, layer := range req.Layers { 568 - if size, claimed := quota.ClaimedLayers[layer.Digest]; claimed { 569 - // Remove from claimed layers 570 - delete(quota.ClaimedLayers, layer.Digest) 571 - quota.Used -= size 572 - 573 - log.Printf("Decremented quota for %s: layer %s (%d bytes)", 574 - req.DID, layer.Digest, size) 575 - } else { 576 - log.Printf("Warning: layer %s not in claimed_layers for %s", 577 - layer.Digest, req.DID) 578 } 579 } 580 581 - // Ensure quota.Used doesn't go negative (defensive) 582 - if quota.Used < 0 { 583 - log.Printf("Warning: quota.Used went negative for %s, resetting to 0", req.DID) 584 - quota.Used = 0 585 - } 586 - 587 - // Save updated quota 588 - quota.LastUpdated = time.Now() 589 - if err := s.quotaManager.SaveQuota(quota); err != nil { 590 - http.Error(w, "failed to save quota", 500) 591 - return 592 - } 593 - 594 - // Return updated quota info 595 - json.NewEncoder(w).Encode(map[string]any{ 596 - "used": quota.Used, 597 - "limit": quota.Limit, 598 - }) 599 } 600 ``` 601 602 - ### SQL Query: Check Layer References 603 - 604 - ```sql 605 - -- pkg/appview/db/queries.go 606 607 - -- Check if user still references this layer in other manifests 608 - SELECT COUNT(*) 609 - FROM layers l 610 - JOIN manifests m ON l.manifest_id = m.id 611 - WHERE m.did = ? -- User's DID 612 - AND l.digest = ? -- Layer digest 613 - AND m.id != ? -- Exclude the manifest being deleted 614 - ``` 615 616 - ## Garbage Collection 617 618 - ### Background: Orphaned Blobs 619 620 - Orphaned blobs accumulate when: 621 - 1. Manifest push fails after blobs uploaded (presigned URLs bypass hold) 622 - 2. Quota exceeded - manifest rejected, blobs already in S3 623 - 3. User deletes manifest - blobs no longer referenced 624 625 - **GC periodically cleans these up.** 626 627 - ### GC Cron Implementation 628 629 - Similar to AppView's backfill worker, the hold service can run periodic GC: 630 631 ```go 632 - // cmd/hold/gc/gc.go 633 634 - type GarbageCollector struct { 635 - driver storagedriver.StorageDriver 636 - appviewURL string 637 - holdURL string 638 - quotaManager *quota.Manager 639 - } 640 641 - // Run garbage collection 642 - func (gc *GarbageCollector) Run(ctx context.Context) error { 643 - log.Println("Starting garbage collection...") 644 - 645 - // Step 1: Get list of referenced blobs from AppView 646 - referenced, err := gc.getReferencedBlobs() 647 - if err != nil { 648 - return fmt.Errorf("failed to get referenced blobs: %w", err) 649 - } 650 - 651 - referencedSet := make(map[string]bool) 652 - for _, digest := range referenced { 653 - referencedSet[digest] = true 654 - } 655 - 656 - log.Printf("AppView reports %d referenced blobs", len(referenced)) 657 - 658 - // Step 2: Walk S3 blobs 659 - deletedCount := 0 660 - reclaimedBytes := int64(0) 661 - 662 - err = gc.driver.Walk(ctx, "/docker/registry/v2/blobs", func(fileInfo storagedriver.FileInfo) error { 663 - if fileInfo.IsDir() { 664 - return nil // Skip directories 665 - } 666 - 667 - // Extract digest from path 668 - // Path: /docker/registry/v2/blobs/sha256/ab/abc123.../data 669 - digest := extractDigestFromPath(fileInfo.Path()) 670 - 671 - if !referencedSet[digest] { 672 - // Unreferenced blob - delete it 673 - size := fileInfo.Size() 674 - 675 - if err := gc.driver.Delete(ctx, fileInfo.Path()); err != nil { 676 - log.Printf("Failed to delete blob %s: %v", digest, err) 677 - return nil // Continue anyway 678 - } 679 - 680 - deletedCount++ 681 - reclaimedBytes += size 682 - 683 - log.Printf("GC: Deleted unreferenced blob %s (%d bytes)", digest, size) 684 - } 685 - 686 - return nil 687 - }) 688 - 689 - if err != nil { 690 - return fmt.Errorf("failed to walk blobs: %w", err) 691 - } 692 - 693 - log.Printf("GC complete: deleted %d blobs, reclaimed %d bytes", 694 - deletedCount, reclaimedBytes) 695 - 696 - return nil 697 - } 698 - 699 - // Get referenced blobs from AppView 700 - func (gc *GarbageCollector) getReferencedBlobs() ([]string, error) { 701 - // Query AppView for all blobs referenced by manifests 702 - // stored in THIS hold service 703 - url := fmt.Sprintf("%s/internal/blobs/referenced?hold=%s", 704 - gc.appviewURL, url.QueryEscape(gc.holdURL)) 705 - 706 - resp, err := http.Get(url) 707 - if err != nil { 708 - return nil, err 709 - } 710 - defer resp.Body.Close() 711 712 - var result struct { 713 - Blobs []string `json:"blobs"` 714 } 715 716 - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 717 - return nil, err 718 } 719 720 - return result.Blobs, nil 721 } 722 ``` 723 724 - ### AppView Internal API 725 726 ```go 727 - // pkg/appview/handlers/internal.go 728 729 - // Get all referenced blobs for a specific hold 730 - func (h *InternalHandler) GetReferencedBlobs(w http.ResponseWriter, r *http.Request) { 731 - holdEndpoint := r.URL.Query().Get("hold") 732 - if holdEndpoint == "" { 733 - http.Error(w, "missing hold parameter", 400) 734 - return 735 - } 736 - 737 - // Query database for all layers in manifests stored in this hold 738 - query := ` 739 - SELECT DISTINCT l.digest 740 - FROM layers l 741 - JOIN manifests m ON l.manifest_id = m.id 742 - WHERE m.hold_endpoint = ? 743 - ` 744 - 745 - rows, err := h.db.Query(query, holdEndpoint) 746 if err != nil { 747 - http.Error(w, "database error", 500) 748 - return 749 } 750 - defer rows.Close() 751 752 - blobs := []string{} 753 - for rows.Next() { 754 - var digest string 755 - if err := rows.Scan(&digest); err != nil { 756 continue 757 } 758 - blobs = append(blobs, digest) 759 - } 760 - 761 - json.NewEncoder(w).Encode(map[string]any{ 762 - "blobs": blobs, 763 - "count": len(blobs), 764 - "hold": holdEndpoint, 765 - }) 766 - } 767 - ``` 768 - 769 - ### GC Cron Schedule 770 - 771 - ```go 772 - // cmd/hold/main.go 773 - 774 - func main() { 775 - // ... service setup ... 776 - 777 - // Start GC cron if enabled 778 - if os.Getenv("GC_ENABLED") == "true" { 779 - gcInterval := 24 * time.Hour // Daily by default 780 - 781 - go func() { 782 - ticker := time.NewTicker(gcInterval) 783 - defer ticker.Stop() 784 - 785 - for range ticker.C { 786 - if err := garbageCollector.Run(context.Background()); err != nil { 787 - log.Printf("GC error: %v", err) 788 - } 789 } 790 - }() 791 - 792 - log.Printf("GC cron started: runs every %v", gcInterval) 793 } 794 795 - // Start server... 796 } 797 ``` 798 799 - ## Quota Reconciliation 800 801 - ### PDS as Source of Truth 802 803 - **Key insight:** Manifest records in PDS are publicly readable (no OAuth needed for reads). 804 805 - Each manifest contains: 806 - - Repository name 807 - - Digest 808 - - Layers array with digest + size 809 - - Hold endpoint 810 811 - The hold service can query the PDS to calculate the user's true quota: 812 - 813 - ``` 814 - 1. List all io.atcr.manifest records for user 815 - 2. Filter manifests where holdEndpoint == this hold service 816 - 3. Extract unique layers (deduplicate by digest) 817 - 4. Sum layer sizes = true quota usage 818 - 5. Compare to quota file 819 - 6. Fix discrepancies 820 - ``` 821 822 - ### Implementation 823 824 ```go 825 - // cmd/hold/quota/reconcile.go 826 827 - type Reconciler struct { 828 - quotaManager *Manager 829 - atprotoResolver *atproto.Resolver 830 - holdURL string 831 - } 832 - 833 - // ReconcileUser recalculates quota from PDS manifests 834 - func (r *Reconciler) ReconcileUser(ctx context.Context, did string) error { 835 - log.Printf("Reconciling quota for %s", did) 836 - 837 - // Step 1: Resolve user's PDS endpoint 838 - identity, err := r.atprotoResolver.ResolveIdentity(ctx, did) 839 if err != nil { 840 - return fmt.Errorf("failed to resolve DID: %w", err) 841 - } 842 - 843 - // Step 2: Create unauthenticated ATProto client 844 - // (manifest records are public - no OAuth needed) 845 - client := atproto.NewClient(identity.PDSEndpoint, did, "") 846 - 847 - // Step 3: List all manifest records for this user 848 - manifests, err := client.ListRecords(ctx, atproto.ManifestCollection, 1000) 849 - if err != nil { 850 - return fmt.Errorf("failed to list manifests: %w", err) 851 } 852 853 - // Step 4: Filter manifests stored in THIS hold service 854 - // and extract unique layers 855 - uniqueLayers := make(map[string]int64) // digest -> size 856 - 857 - for _, record := range manifests { 858 - var manifest atproto.ManifestRecord 859 - if err := json.Unmarshal(record.Value, &manifest); err != nil { 860 - log.Printf("Warning: failed to parse manifest: %v", err) 861 - continue 862 - } 863 - 864 - // Only count manifests stored in this hold 865 - if manifest.HoldEndpoint != r.holdURL { 866 continue 867 } 868 - 869 - // Add config blob 870 - if manifest.Config.Digest != "" { 871 - uniqueLayers[manifest.Config.Digest] = manifest.Config.Size 872 - } 873 - 874 - // Add layer blobs 875 - for _, layer := range manifest.Layers { 876 - uniqueLayers[layer.Digest] = layer.Size 877 - } 878 } 879 880 - // Step 5: Calculate true quota usage 881 - trueUsage := int64(0) 882 - for _, size := range uniqueLayers { 883 - trueUsage += size 884 - } 885 886 - log.Printf("User %s true usage from PDS: %d bytes (%d unique layers)", 887 - did, trueUsage, len(uniqueLayers)) 888 - 889 - // Step 6: Compare with current quota file 890 - quota, err := r.quotaManager.GetQuota(did) 891 - if err != nil { 892 - log.Printf("No existing quota for %s, creating new", did) 893 - quota = &Quota{ 894 - DID: did, 895 - Limit: r.quotaManager.DefaultLimit, 896 - ClaimedLayers: make(map[string]int64), 897 } 898 - } 899 900 - // Step 7: Fix discrepancies 901 - if quota.Used != trueUsage || len(quota.ClaimedLayers) != len(uniqueLayers) { 902 - log.Printf("Quota mismatch for %s: recorded=%d, actual=%d (diff=%d)", 903 - did, quota.Used, trueUsage, trueUsage - quota.Used) 904 - 905 - // Update quota to match PDS truth 906 - quota.Used = trueUsage 907 - quota.ClaimedLayers = uniqueLayers 908 - quota.LastUpdated = time.Now() 909 - 910 - if err := r.quotaManager.SaveQuota(quota); err != nil { 911 - return fmt.Errorf("failed to save reconciled quota: %w", err) 912 } 913 - 914 - log.Printf("Reconciled quota for %s: %d bytes", did, trueUsage) 915 - } else { 916 - log.Printf("Quota for %s is accurate", did) 917 - } 918 - 919 - return nil 920 - } 921 - 922 - // ReconcileAll reconciles all users (run periodically) 923 - func (r *Reconciler) ReconcileAll(ctx context.Context) error { 924 - // Get list of all users with quota files 925 - users, err := r.quotaManager.ListUsers() 926 - if err != nil { 927 - return err 928 - } 929 - 930 - log.Printf("Starting reconciliation for %d users", len(users)) 931 - 932 - for _, did := range users { 933 - if err := r.ReconcileUser(ctx, did); err != nil { 934 - log.Printf("Failed to reconcile %s: %v", did, err) 935 - // Continue with other users 936 - } 937 - } 938 939 - log.Println("Reconciliation complete") 940 - return nil 941 } 942 ``` 943 944 - ### Reconciliation Cron 945 946 - ```go 947 - // cmd/hold/main.go 948 - 949 - func main() { 950 - // ... setup ... 951 - 952 - // Start reconciliation cron 953 - if os.Getenv("QUOTA_RECONCILE_ENABLED") == "true" { 954 - reconcileInterval := 24 * time.Hour // Daily 955 - 956 - go func() { 957 - ticker := time.NewTicker(reconcileInterval) 958 - defer ticker.Stop() 959 - 960 - for range ticker.C { 961 - if err := reconciler.ReconcileAll(context.Background()); err != nil { 962 - log.Printf("Reconciliation error: %v", err) 963 - } 964 - } 965 - }() 966 - 967 - log.Printf("Quota reconciliation cron started: runs every %v", reconcileInterval) 968 - } 969 - 970 - // ... start server ... 971 - } 972 ``` 973 - 974 - ### Why PDS as Source of Truth Works 975 - 976 - 1. **Manifests are canonical** - If manifest exists in PDS, user owns those layers 977 - 2. **Public reads** - No OAuth needed, just resolve DID โ†’ PDS endpoint 978 - 3. **ATProto durability** - PDS is user's authoritative data store 979 - 4. **AppView is cache** - AppView database might lag or have inconsistencies 980 - 5. **Reconciliation fixes drift** - Periodic sync from PDS ensures accuracy 981 - 982 - **Example reconciliation scenarios:** 983 - 984 - - **Orphaned quota entries:** User deleted manifest from PDS, but hold quota still has it 985 - โ†’ Reconciliation removes from claimed_layers 986 - 987 - - **Missing quota entries:** User pushed manifest, but quota update failed 988 - โ†’ Reconciliation adds to claimed_layers 989 - 990 - - **Race condition duplicates:** Two concurrent pushes double-counted a layer 991 - โ†’ Reconciliation fixes to actual usage 992 993 ## Configuration 994 ··· 997 ```bash 998 # .env.hold 999 1000 - # ============================================================================ 1001 # Quota Configuration 1002 - # ============================================================================ 1003 - 1004 - # Enable quota enforcement 1005 QUOTA_ENABLED=true 1006 - 1007 - # Default quota limit per user (bytes) 1008 - # 10GB = 10737418240 1009 - # 50GB = 53687091200 1010 - # 100GB = 107374182400 1011 - QUOTA_DEFAULT_LIMIT=10737418240 1012 - 1013 - # Storage backend for quota data 1014 - # Options: s3, sqlite 1015 - QUOTA_STORAGE_BACKEND=s3 1016 - 1017 - # For S3-based storage: 1018 - # Quota files stored in same bucket as blobs 1019 - QUOTA_STORAGE_PREFIX=/atcr/quota/ 1020 - 1021 - # For SQLite-based storage: 1022 - QUOTA_DB_PATH=/var/lib/atcr/hold-quota.db 1023 1024 - # ============================================================================ 1025 # Garbage Collection 1026 - # ============================================================================ 1027 - 1028 - # Enable periodic garbage collection 1029 GC_ENABLED=true 1030 - 1031 - # GC interval (default: 24h) 1032 GC_INTERVAL=24h 1033 - 1034 - # AppView URL for GC reference checking 1035 - APPVIEW_URL=https://atcr.io 1036 - 1037 - # ============================================================================ 1038 - # Quota Reconciliation 1039 - # ============================================================================ 1040 - 1041 - # Enable quota reconciliation from PDS 1042 - QUOTA_RECONCILE_ENABLED=true 1043 - 1044 - # Reconciliation interval (default: 24h) 1045 - QUOTA_RECONCILE_INTERVAL=24h 1046 - 1047 - # ============================================================================ 1048 - # Hold Service Identity (Required) 1049 - # ============================================================================ 1050 - 1051 - # Public URL of this hold service 1052 - HOLD_PUBLIC_URL=https://hold1.example.com 1053 - 1054 - # Owner DID (for auto-registration) 1055 - HOLD_OWNER=did:plc:xyz123 1056 ``` 1057 1058 - ### AppView Configuration 1059 1060 - ```bash 1061 - # .env.appview 1062 - 1063 - # Internal API endpoint for hold services 1064 - # Used for GC reference checking 1065 - ATCR_INTERNAL_API_ENABLED=true 1066 - 1067 - # Optional: authentication token for internal APIs 1068 - ATCR_INTERNAL_API_TOKEN=secret123 1069 - ``` 1070 - 1071 - ## Trade-offs & Design Decisions 1072 - 1073 - ### 1. Claimed Storage vs Physical Storage 1074 - 1075 - **Decision:** Track claimed storage (logical accounting) 1076 - 1077 - **Why:** 1078 - - Predictable for users: "you pay for what you upload" 1079 - - No complex cross-user dependencies 1080 - - Delete always gives you quota back 1081 - - Matches Harbor's proven model 1082 - 1083 - **Trade-off:** 1084 - - Total claimed can exceed physical storage 1085 - - Users might complain "I uploaded 10GB but S3 only has 6GB" 1086 - 1087 - **Mitigation:** 1088 - - Show deduplication savings metric 1089 - - Educate users: "You claimed 10GB, but deduplication saved 4GB" 1090 - 1091 - ### 2. S3 vs SQLite for Quota Storage 1092 - 1093 - **Decision:** Support both, recommend based on use case 1094 - 1095 - **S3 Pros:** 1096 - - No database to manage 1097 - - Quota data lives with blobs 1098 - - Better for ephemeral BYOS 1099 - 1100 - **SQLite Pros:** 1101 - - Faster (no network) 1102 - - ACID transactions (no race conditions) 1103 - - Better for high-traffic shared holds 1104 - 1105 - **Trade-off:** 1106 - - S3: eventual consistency, race conditions 1107 - - SQLite: stateful service, scaling challenges 1108 - 1109 - **Mitigation:** 1110 - - Reconciliation fixes S3 inconsistencies 1111 - - SQLite can use shared DB for multi-instance 1112 - 1113 - ### 3. Optimistic Quota Update 1114 - 1115 - **Decision:** Update quota BEFORE upload completes 1116 - 1117 - **Why:** 1118 - - Prevent race conditions (two users uploading simultaneously) 1119 - - Can reject before presigned URL generated 1120 - - Simpler flow 1121 - 1122 - **Trade-off:** 1123 - - If upload fails, quota already incremented (user "paid" for nothing) 1124 - 1125 - **Mitigation:** 1126 - - Reconciliation from PDS fixes orphaned quota entries 1127 - - Acceptable for MVP (upload failures are rare) 1128 - 1129 - ### 4. AppView as Intermediary 1130 - 1131 - **Decision:** AppView notifies hold service on deletes 1132 - 1133 - **Why:** 1134 - - AppView already has manifest/layer database 1135 - - Can efficiently check if layer still referenced 1136 - - Hold service doesn't need to query PDS on every delete 1137 - 1138 - **Trade-off:** 1139 - - AppView โ†’ Hold dependency 1140 - - Network hop on delete 1141 - 1142 - **Mitigation:** 1143 - - If notification fails, reconciliation fixes quota 1144 - - Eventually consistent is acceptable 1145 - 1146 - ### 5. PDS as Source of Truth 1147 - 1148 - **Decision:** Use PDS manifests for reconciliation 1149 - 1150 - **Why:** 1151 - - Manifests in PDS are canonical user data 1152 - - Public reads (no OAuth for reconciliation) 1153 - - AppView database might lag or be inconsistent 1154 - 1155 - **Trade-off:** 1156 - - Reconciliation requires PDS queries (slower) 1157 - - Limited to 1000 manifests per query 1158 - 1159 - **Mitigation:** 1160 - - Run reconciliation daily (not real-time) 1161 - - Paginate if user has >1000 manifests 1162 1163 ## Future Enhancements 1164 1165 ### 1. Quota API Endpoints 1166 1167 ``` 1168 - GET /quota/usage - Get current user's quota 1169 - GET /quota/breakdown - Get storage by repository 1170 - POST /quota/limit - Update user's quota limit (admin) 1171 - GET /quota/stats - Get hold-wide statistics 1172 ``` 1173 1174 ### 2. Quota Alerts 1175 1176 - Notify users when approaching limit: 1177 - - Email/webhook at 80%, 90%, 95% 1178 - - Reject uploads at 100% (currently implemented) 1179 - - Grace period: allow 105% temporarily 1180 1181 - ### 3. Tiered Quotas 1182 1183 - Different limits based on user tier: 1184 - - Free: 10GB 1185 - - Pro: 100GB 1186 - - Enterprise: unlimited 1187 1188 - ### 4. Quota Purchasing 1189 1190 - Allow users to buy additional storage: 1191 - - Stripe integration 1192 - - $0.10/GB/month pricing 1193 - - Dynamic limit updates 1194 1195 - ### 5. Cross-Hold Deduplication 1196 1197 - If multiple holds share same S3 bucket: 1198 - - Track blob ownership globally 1199 - - Split costs proportionally 1200 - - More complex, but maximizes deduplication 1201 1202 - ### 6. Manifest-Based Quota (Alternative Model) 1203 - 1204 - Instead of tracking layers, track manifests: 1205 - - Simpler: just count manifest sizes 1206 - - No deduplication benefits for users 1207 - - Might be acceptable for some use cases 1208 - 1209 - ### 7. Redis-Based Quota (High Performance) 1210 - 1211 - For high-traffic registries: 1212 - - Use Redis instead of S3/SQLite 1213 - - Sub-millisecond quota checks 1214 - - Harbor-proven approach 1215 - 1216 - ### 8. Quota Visualizations 1217 - 1218 - Web UI showing: 1219 - - Storage usage over time 1220 - - Top consumers by repository 1221 - - Deduplication savings graph 1222 - - Layer size distribution 1223 - 1224 - ## Appendix: SQL Queries 1225 - 1226 - ### Check if User Still References Layer 1227 - 1228 - ```sql 1229 - -- After deleting manifest, check if user has other manifests using this layer 1230 - SELECT COUNT(*) 1231 - FROM layers l 1232 - JOIN manifests m ON l.manifest_id = m.id 1233 - WHERE m.did = ? -- User's DID 1234 - AND l.digest = ? -- Layer digest to check 1235 - AND m.id != ? -- Exclude the manifest being deleted 1236 - ``` 1237 - 1238 - ### Get All Unique Layers for User 1239 - 1240 - ```sql 1241 - -- Calculate true quota usage for a user 1242 - SELECT DISTINCT l.digest, l.size 1243 - FROM layers l 1244 - JOIN manifests m ON l.manifest_id = m.id 1245 - WHERE m.did = ? 1246 - AND m.hold_endpoint = ? 1247 ``` 1248 1249 - ### Get Referenced Blobs for Hold 1250 1251 - ```sql 1252 - -- For GC: get all blobs still referenced by any user of this hold 1253 - SELECT DISTINCT l.digest 1254 - FROM layers l 1255 - JOIN manifests m ON l.manifest_id = m.id 1256 - WHERE m.hold_endpoint = ? 1257 - ``` 1258 1259 - ### Get Storage Stats by Repository 1260 1261 - ```sql 1262 - -- User's storage broken down by repository 1263 - SELECT 1264 - m.repository, 1265 - COUNT(DISTINCT m.id) as manifest_count, 1266 - COUNT(DISTINCT l.digest) as unique_layers, 1267 - SUM(l.size) as total_size 1268 - FROM manifests m 1269 - JOIN layers l ON l.manifest_id = m.id 1270 - WHERE m.did = ? 1271 - AND m.hold_endpoint = ? 1272 - GROUP BY m.repository 1273 - ORDER BY total_size DESC 1274 - ``` 1275 1276 ## References 1277 1278 - **Harbor Quotas:** https://goharbor.io/docs/1.10/administration/configure-project-quotas/ 1279 - - **Harbor Source:** https://github.com/goharbor/harbor 1280 - **ATProto Spec:** https://atproto.com/specs/record 1281 - **OCI Distribution Spec:** https://github.com/opencontainers/distribution-spec 1282 - - **S3 API Reference:** https://docs.aws.amazon.com/AmazonS3/latest/API/ 1283 - - **Distribution GC:** https://github.com/distribution/distribution/blob/main/registry/storage/garbagecollect.go 1284 1285 --- 1286 1287 - **Document Version:** 1.0 1288 - **Last Updated:** 2025-10-09 1289 - **Author:** Generated from implementation research and Harbor analysis
··· 1 # ATCR Quota System 2 3 + This document describes ATCR's storage quota implementation using ATProto records for per-user layer tracking. 4 5 ## Table of Contents 6 7 - [Overview](#overview) 8 + - [Quota Model](#quota-model) 9 + - [Layer Record Schema](#layer-record-schema) 10 + - [Quota Calculation](#quota-calculation) 11 + - [Push Flow](#push-flow) 12 - [Delete Flow](#delete-flow) 13 - [Garbage Collection](#garbage-collection) 14 - [Configuration](#configuration) 15 - [Future Enhancements](#future-enhancements) 16 17 ## Overview 18 19 ATCR implements per-user storage quotas to: 20 1. **Limit storage consumption** on shared hold services 21 + 2. **Provide transparency** (show users their storage usage) 22 + 3. **Enable fair billing** (users pay for what they use) 23 24 + **Key principle:** Users pay for layers they reference, deduplicated per-user. If you push the same layer in multiple images, you only pay once. 25 26 ### Example Scenario 27 28 ``` 29 Alice pushes myapp:v1 (layers A, B, C - each 100MB) 30 + โ†’ Creates 3 layer records in hold's PDS 31 + โ†’ Alice's quota: 300MB (3 unique layers) 32 33 Alice pushes myapp:v2 (layers A, B, D) 34 + โ†’ Creates 3 more layer records (A, B again, plus D) 35 + โ†’ Alice's quota: 400MB (4 unique layers: A, B, C, D) 36 + โ†’ Layers A, B appear twice in records but deduplicated in quota calc 37 38 Bob pushes his-app:latest (layers A, E) 39 + โ†’ Creates 2 layer records for Bob 40 + โ†’ Bob's quota: 200MB (2 unique layers: A, E) 41 + โ†’ Layer A shared with Alice in S3, but Bob pays for his own usage 42 43 + Physical S3 storage: 500MB (A, B, C, D, E - deduplicated globally) 44 + Alice's quota: 400MB 45 + Bob's quota: 200MB 46 ``` 47 48 + ## Quota Model 49 50 + ### Everyone Pays for What They Upload 51 52 + Each user is charged for all unique layers they reference, regardless of whether those layers exist in S3 from other users' uploads. 53 54 + **Why this model?** 55 + - **Simple mental model**: "I pushed 500MB of layers, I use 500MB of quota" 56 + - **Predictable**: Your quota doesn't change based on others' actions 57 + - **Clean deletion**: Delete manifest โ†’ layer records removed โ†’ quota freed 58 + - **No cross-user dependencies**: Users are isolated 59 60 + **Trade-off:** 61 + - Total claimed storage can exceed physical S3 storage 62 + - This is acceptable - deduplication is an operational benefit for ATCR, not a billing feature 63 64 + ### ATProto-Native Storage 65 66 + Layer tracking uses ATProto records stored in the hold's embedded PDS: 67 + - **Collection**: `io.atcr.hold.layer` 68 + - **Repository**: Hold's DID (e.g., `did:web:hold01.atcr.io`) 69 + - **Records**: One per manifest-layer relationship (TID-based keys) 70 71 + This approach: 72 + - Keeps quota data in ATProto (no separate database) 73 + - Enables standard ATProto sync/query mechanisms 74 + - Provides full audit trail of layer usage 75 76 + ## Layer Record Schema 77 78 + ### LayerRecord 79 80 + ```go 81 + // pkg/atproto/lexicon.go 82 83 + type LayerRecord struct { 84 + Type string `json:"$type"` // "io.atcr.hold.layer" 85 + Digest string `json:"digest"` // Layer digest (sha256:abc123...) 86 + Size int64 `json:"size"` // Size in bytes 87 + MediaType string `json:"mediaType"` // e.g., "application/vnd.oci.image.layer.v1.tar+gzip" 88 + Manifest string `json:"manifest"` // at://did:plc:alice/io.atcr.manifest/abc123 89 + UserDID string `json:"userDid"` // User's DID for quota grouping 90 + CreatedAt string `json:"createdAt"` // ISO 8601 timestamp 91 + } 92 ``` 93 94 + ### Record Key 95 96 + Records use TID (timestamp-based ID) as the rkey. This means: 97 + - Multiple records can exist for the same layer (from different manifests) 98 + - Deduplication happens at query time, not storage time 99 + - Simple append-only writes on manifest push 100 101 + ### Example Records 102 103 ``` 104 + Manifest A (layers X, Y, Z) โ†’ creates 3 records 105 + Manifest B (layers X, W) โ†’ creates 2 records 106 107 + io.atcr.hold.layer collection: 108 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 109 + โ”‚ rkey (TID) โ”‚ digest โ”‚ size โ”‚ manifest โ”‚ userDid โ”‚ 110 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค 111 + โ”‚ 3jui7...001 โ”‚ X โ”‚ 100 โ”‚ at://did:plc:alice/.../manifestA โ”‚ did:plc:alice โ”‚ 112 + โ”‚ 3jui7...002 โ”‚ Y โ”‚ 200 โ”‚ at://did:plc:alice/.../manifestA โ”‚ did:plc:alice โ”‚ 113 + โ”‚ 3jui7...003 โ”‚ Z โ”‚ 150 โ”‚ at://did:plc:alice/.../manifestA โ”‚ did:plc:alice โ”‚ 114 + โ”‚ 3jui7...004 โ”‚ X โ”‚ 100 โ”‚ at://did:plc:alice/.../manifestB โ”‚ did:plc:alice โ”‚ โ† duplicate digest 115 + โ”‚ 3jui7...005 โ”‚ W โ”‚ 300 โ”‚ at://did:plc:alice/.../manifestB โ”‚ did:plc:alice โ”‚ 116 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 117 ``` 118 119 + ## Quota Calculation 120 121 + ### Query: User's Unique Storage 122 123 + ```sql 124 + -- Calculate quota by deduplicating layers 125 + SELECT SUM(size) FROM ( 126 + SELECT DISTINCT digest, size 127 + FROM io.atcr.hold.layer 128 + WHERE userDid = ? 129 + ) 130 ``` 131 132 + Using the example above: 133 + - Layer X appears twice but counted once: 100 134 + - Layers Y, Z, W counted once each: 200 + 150 + 300 135 + - **Total: 750 bytes** 136 137 + ### Implementation 138 139 ```go 140 + // pkg/hold/quota/quota.go 141 142 + type QuotaManager struct { 143 + pds *pds.Server // Hold's embedded PDS 144 + } 145 146 + // GetUsage calculates a user's current quota usage 147 + func (q *QuotaManager) GetUsage(ctx context.Context, userDID string) (int64, error) { 148 + // List all layer records for this user 149 + records, err := q.pds.ListRecords(ctx, LayerCollection, userDID) 150 if err != nil { 151 + return 0, err 152 } 153 154 + // Deduplicate by digest 155 + uniqueLayers := make(map[string]int64) // digest -> size 156 + for _, record := range records { 157 + var layer LayerRecord 158 + if err := json.Unmarshal(record.Value, &layer); err != nil { 159 + continue 160 + } 161 + if layer.UserDID == userDID { 162 + uniqueLayers[layer.Digest] = layer.Size 163 + } 164 } 165 166 + // Sum unique layer sizes 167 + var total int64 168 + for _, size := range uniqueLayers { 169 + total += size 170 } 171 172 + return total, nil 173 + } 174 175 + // CheckQuota returns true if user has space for additional bytes 176 + func (q *QuotaManager) CheckQuota(ctx context.Context, userDID string, additional int64, limit int64) (bool, int64, error) { 177 + current, err := q.GetUsage(ctx, userDID) 178 if err != nil { 179 + return false, 0, err 180 } 181 182 + return current+additional <= limit, current, nil 183 } 184 ``` 185 186 + ### Quota Response 187 188 + ```go 189 + type QuotaInfo struct { 190 + Used int64 `json:"used"` // Current usage (deduplicated) 191 + Limit int64 `json:"limit"` // User's quota limit 192 + Available int64 `json:"available"` // Remaining space 193 + } 194 ``` 195 196 + ## Push Flow 197 198 + ### Step-by-Step: User Pushes Image 199 200 ``` 201 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 202 + โ”‚ Client โ”‚ โ”‚ AppView โ”‚ โ”‚ Hold โ”‚ โ”‚ User PDS โ”‚ 203 + โ”‚ (Docker) โ”‚ โ”‚ โ”‚ โ”‚ Service โ”‚ โ”‚ โ”‚ 204 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 205 โ”‚ โ”‚ โ”‚ โ”‚ 206 + โ”‚ 1. Upload blobs โ”‚ โ”‚ โ”‚ 207 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 208 + โ”‚ โ”‚ 2. Route to hold โ”‚ โ”‚ 209 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 210 + โ”‚ โ”‚ โ”‚ 3. Store in S3 โ”‚ 211 + โ”‚ โ”‚ โ”‚ โ”‚ 212 + โ”‚ 4. PUT manifest โ”‚ โ”‚ โ”‚ 213 โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 214 โ”‚ โ”‚ โ”‚ โ”‚ 215 + โ”‚ โ”‚ 5. Calculate quota โ”‚ โ”‚ 216 + โ”‚ โ”‚ impact for new โ”‚ โ”‚ 217 + โ”‚ โ”‚ layers โ”‚ โ”‚ 218 โ”‚ โ”‚ โ”‚ โ”‚ 219 + โ”‚ โ”‚ 6. Check quota limit โ”‚ โ”‚ 220 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 221 + โ”‚ โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ 222 โ”‚ โ”‚ โ”‚ โ”‚ 223 + โ”‚ โ”‚ 7. Store manifest โ”‚ โ”‚ 224 โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 225 โ”‚ โ”‚ โ”‚ โ”‚ 226 + โ”‚ โ”‚ 8. Create layer โ”‚ โ”‚ 227 + โ”‚ โ”‚ records โ”‚ โ”‚ 228 โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 229 + โ”‚ โ”‚ โ”‚ 9. Write to โ”‚ 230 + โ”‚ โ”‚ โ”‚ hold's PDS โ”‚ 231 โ”‚ โ”‚ โ”‚ โ”‚ 232 + โ”‚ 10. 201 Created โ”‚ โ”‚ โ”‚ 233 โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ 234 ``` 235 236 + ### Implementation 237 238 ```go 239 + // pkg/appview/storage/routing_repository.go 240 241 + func (r *RoutingRepository) PutManifest(ctx context.Context, manifest distribution.Manifest) error { 242 + // Parse manifest to get layers 243 + layers := extractLayers(manifest) 244 245 + // Get user's current unique layers from hold 246 + existingLayers, err := r.holdClient.GetUserLayers(ctx, r.userDID) 247 if err != nil { 248 + return err 249 } 250 + existingSet := makeDigestSet(existingLayers) 251 252 + // Calculate quota impact (only new unique layers) 253 + var quotaImpact int64 254 for _, layer := range layers { 255 + if !existingSet[layer.Digest] { 256 + quotaImpact += layer.Size 257 } 258 } 259 260 + // Check quota 261 + ok, current, err := r.quotaManager.CheckQuota(ctx, r.userDID, quotaImpact, r.quotaLimit) 262 if err != nil { 263 + return err 264 } 265 + if !ok { 266 + return fmt.Errorf("quota exceeded: used=%d, impact=%d, limit=%d", 267 + current, quotaImpact, r.quotaLimit) 268 } 269 270 + // Store manifest in user's PDS 271 + manifestURI, err := r.atprotoClient.PutManifest(ctx, manifest) 272 if err != nil { 273 + return err 274 } 275 276 + // Create layer records in hold's PDS 277 + for _, layer := range layers { 278 + record := LayerRecord{ 279 + Type: "io.atcr.hold.layer", 280 + Digest: layer.Digest, 281 + Size: layer.Size, 282 + MediaType: layer.MediaType, 283 + Manifest: manifestURI, 284 + UserDID: r.userDID, 285 + CreatedAt: time.Now().Format(time.RFC3339), 286 + } 287 + if err := r.holdClient.CreateLayerRecord(ctx, record); err != nil { 288 + log.Printf("Warning: failed to create layer record: %v", err) 289 + // Continue - reconciliation will fix 290 } 291 } 292 293 + return nil 294 } 295 ``` 296 297 + ### Quota Check Timing 298 299 + Quota is checked when the **manifest is pushed** (after blobs are uploaded): 300 + - Blobs upload first via presigned URLs 301 + - Manifest pushed last triggers quota check 302 + - If quota exceeded, manifest is rejected (orphaned blobs cleaned by GC) 303 304 + This matches Harbor's approach and is the industry standard. 305 306 + ## Delete Flow 307 308 + ### Manifest Deletion 309 310 + When a user deletes a manifest: 311 312 + ``` 313 + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 314 + โ”‚ User โ”‚ โ”‚ AppView โ”‚ โ”‚ Hold โ”‚ โ”‚ User PDS โ”‚ 315 + โ”‚ UI โ”‚ โ”‚ โ”‚ โ”‚ Service โ”‚ โ”‚ โ”‚ 316 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 317 + โ”‚ โ”‚ โ”‚ โ”‚ 318 + โ”‚ DELETE manifest โ”‚ โ”‚ โ”‚ 319 + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ โ”‚ 320 + โ”‚ โ”‚ โ”‚ โ”‚ 321 + โ”‚ โ”‚ 1. Delete manifest โ”‚ โ”‚ 322 + โ”‚ โ”‚ from user's PDS โ”‚ โ”‚ 323 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 324 + โ”‚ โ”‚ โ”‚ โ”‚ 325 + โ”‚ โ”‚ 2. Delete layer โ”‚ โ”‚ 326 + โ”‚ โ”‚ records for this โ”‚ โ”‚ 327 + โ”‚ โ”‚ manifest โ”‚ โ”‚ 328 + โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ 329 + โ”‚ โ”‚ โ”‚ 3. Remove records โ”‚ 330 + โ”‚ โ”‚ โ”‚ where manifest โ”‚ 331 + โ”‚ โ”‚ โ”‚ == deleted URI โ”‚ 332 + โ”‚ โ”‚ โ”‚ โ”‚ 333 + โ”‚ 4. 204 No Content โ”‚ โ”‚ โ”‚ 334 + โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ 335 + ``` 336 337 + ### Implementation 338 339 ```go 340 + // pkg/appview/handlers/manifest.go 341 342 + func (h *ManifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Request) { 343 + userDID := auth.GetDID(r.Context()) 344 + repository := chi.URLParam(r, "repository") 345 + digest := chi.URLParam(r, "digest") 346 347 + // Get manifest URI before deletion 348 + manifestURI := fmt.Sprintf("at://%s/%s/%s", userDID, ManifestCollection, digest) 349 350 + // Delete manifest from user's PDS 351 + if err := h.atprotoClient.DeleteRecord(ctx, ManifestCollection, digest); err != nil { 352 + http.Error(w, "failed to delete manifest", 500) 353 + return 354 } 355 356 + // Delete associated layer records from hold's PDS 357 + if err := h.holdClient.DeleteLayerRecords(ctx, manifestURI); err != nil { 358 + log.Printf("Warning: failed to delete layer records: %v", err) 359 + // Continue - reconciliation will clean up 360 } 361 362 + w.WriteHeader(http.StatusNoContent) 363 } 364 ``` 365 366 + ### Hold Service: Delete Layer Records 367 368 ```go 369 + // pkg/hold/pds/xrpc.go 370 371 + func (s *Server) DeleteLayerRecords(ctx context.Context, manifestURI string) error { 372 + // List all layer records 373 + records, err := s.ListRecords(ctx, LayerCollection, "") 374 if err != nil { 375 + return err 376 } 377 378 + // Delete records matching this manifest 379 + for _, record := range records { 380 + var layer LayerRecord 381 + if err := json.Unmarshal(record.Value, &layer); err != nil { 382 continue 383 } 384 + if layer.Manifest == manifestURI { 385 + if err := s.DeleteRecord(ctx, LayerCollection, record.RKey); err != nil { 386 + log.Printf("Failed to delete layer record %s: %v", record.RKey, err) 387 } 388 + } 389 } 390 391 + return nil 392 } 393 ``` 394 395 + ### Quota After Deletion 396 397 + After deleting a manifest: 398 + - Layer records for that manifest are removed 399 + - Quota recalculated with `SELECT DISTINCT` query 400 + - If layer was only in deleted manifest โ†’ quota decreases 401 + - If layer exists in other manifests โ†’ quota unchanged (still deduplicated) 402 403 + ## Garbage Collection 404 405 + ### Orphaned Blobs 406 407 + Orphaned blobs accumulate when: 408 + 1. Manifest push fails after blobs uploaded 409 + 2. Quota exceeded - manifest rejected 410 + 3. User deletes manifest - blobs may no longer be referenced 411 412 + ### GC Process 413 414 ```go 415 + // pkg/hold/gc/gc.go 416 417 + func (gc *GarbageCollector) Run(ctx context.Context) error { 418 + // Step 1: Get all referenced digests from layer records 419 + records, err := gc.pds.ListRecords(ctx, LayerCollection, "") 420 if err != nil { 421 + return err 422 } 423 424 + referenced := make(map[string]bool) 425 + for _, record := range records { 426 + var layer LayerRecord 427 + if err := json.Unmarshal(record.Value, &layer); err != nil { 428 continue 429 } 430 + referenced[layer.Digest] = true 431 } 432 433 + log.Printf("Found %d referenced blobs", len(referenced)) 434 435 + // Step 2: Walk S3 blobs and delete unreferenced 436 + var deleted, reclaimed int64 437 + err = gc.driver.Walk(ctx, "/docker/registry/v2/blobs", func(fi storagedriver.FileInfo) error { 438 + if fi.IsDir() { 439 + return nil 440 } 441 442 + digest := extractDigestFromPath(fi.Path()) 443 + if !referenced[digest] { 444 + size := fi.Size() 445 + if err := gc.driver.Delete(ctx, fi.Path()); err != nil { 446 + log.Printf("Failed to delete %s: %v", digest, err) 447 + return nil 448 + } 449 + deleted++ 450 + reclaimed += size 451 + log.Printf("GC: deleted %s (%d bytes)", digest, size) 452 } 453 + return nil 454 + }) 455 456 + log.Printf("GC complete: deleted %d blobs, reclaimed %d bytes", deleted, reclaimed) 457 + return err 458 } 459 ``` 460 461 + ### GC Schedule 462 463 + ```bash 464 + # Environment variable 465 + GC_ENABLED=true 466 + GC_INTERVAL=24h # Daily by default 467 ``` 468 469 ## Configuration 470 ··· 473 ```bash 474 # .env.hold 475 476 # Quota Configuration 477 QUOTA_ENABLED=true 478 + QUOTA_DEFAULT_LIMIT=10737418240 # 10GB in bytes 479 480 # Garbage Collection 481 GC_ENABLED=true 482 GC_INTERVAL=24h 483 ``` 484 485 + ### Quota Limits by Bytes 486 487 + | Size | Bytes | 488 + |------|-------| 489 + | 1 GB | 1073741824 | 490 + | 5 GB | 5368709120 | 491 + | 10 GB | 10737418240 | 492 + | 50 GB | 53687091200 | 493 + | 100 GB | 107374182400 | 494 495 ## Future Enhancements 496 497 ### 1. Quota API Endpoints 498 499 ``` 500 + GET /xrpc/io.atcr.hold.getQuota?did={userDID} - Get user's quota usage 501 + GET /xrpc/io.atcr.hold.getQuotaBreakdown - Storage by repository 502 ``` 503 504 ### 2. Quota Alerts 505 506 + - Warning thresholds at 80%, 90%, 95% 507 + - Email/webhook notifications 508 + - Grace period before hard enforcement 509 510 + ### 3. Tier-Based Quotas (Implemented) 511 512 + ATCR uses quota tiers to limit storage per crew member, configured via `quotas.yaml`: 513 514 + ```yaml 515 + # quotas.yaml 516 + tiers: 517 + deckhand: # Entry-level crew 518 + quota: 5GB 519 + bosun: # Mid-level crew 520 + quota: 50GB 521 + quartermaster: # High-level crew 522 + quota: 100GB 523 524 + defaults: 525 + new_crew_tier: deckhand # Default tier for new crew members 526 + ``` 527 528 + | Tier | Limit | Description | 529 + |------|-------|-------------| 530 + | deckhand | 5 GB | Entry-level crew member | 531 + | bosun | 50 GB | Mid-level crew member | 532 + | quartermaster | 100 GB | Senior crew member | 533 + | owner (captain) | Unlimited | Hold owner always has unlimited | 534 535 + **Tier Resolution:** 536 + 1. If user is captain (owner) โ†’ unlimited 537 + 2. If crew member has explicit tier โ†’ use that tier's limit 538 + 3. If crew member has no tier โ†’ use `defaults.new_crew_tier` 539 + 4. If default tier not found โ†’ unlimited 540 541 + **Crew Record Example:** 542 + ```json 543 + { 544 + "$type": "io.atcr.hold.crew", 545 + "member": "did:plc:alice123", 546 + "role": "writer", 547 + "permissions": ["blob:write"], 548 + "tier": "bosun", 549 + "addedAt": "2026-01-04T12:00:00Z" 550 + } 551 ``` 552 553 + ### 4. Rate Limiting 554 555 + Pull rate limits (Docker Hub style): 556 + - Anonymous: 100 pulls per 6 hours per IP 557 + - Authenticated: 200 pulls per 6 hours 558 + - Paid: Unlimited 559 560 + ### 5. Quota Purchasing 561 562 + - Stripe integration for additional storage 563 + - $0.10/GB/month pricing (industry standard) 564 565 ## References 566 567 - **Harbor Quotas:** https://goharbor.io/docs/1.10/administration/configure-project-quotas/ 568 - **ATProto Spec:** https://atproto.com/specs/record 569 - **OCI Distribution Spec:** https://github.com/opencontainers/distribution-spec 570 571 --- 572 573 + **Document Version:** 2.0 574 + **Last Updated:** 2026-01-04 575 + **Model:** Per-user layer tracking with ATProto records
+4 -4
docs/SIGNATURE_INTEGRATION.md
··· 545 Name: v.name, 546 Type: v.Type(), 547 Message: fmt.Sprintf("Verified for DID %s", sigData.ATProto.DID), 548 - Extensions: map[string]interface{}{ 549 "did": sigData.ATProto.DID, 550 "handle": sigData.ATProto.Handle, 551 "signedAt": sigData.ATProto.SignedAt, ··· 673 674 type ProviderResponse struct { 675 SystemError string `json:"system_error,omitempty"` 676 - Responses []map[string]interface{} `json:"responses"` 677 } 678 679 func handleProvide(w http.ResponseWriter, r *http.Request) { ··· 684 } 685 686 // Verify each image 687 - responses := make([]map[string]interface{}, 0, len(req.Values)) 688 for _, image := range req.Values { 689 result, err := verifier.Verify(context.Background(), image) 690 691 - response := map[string]interface{}{ 692 "image": image, 693 "verified": false, 694 }
··· 545 Name: v.name, 546 Type: v.Type(), 547 Message: fmt.Sprintf("Verified for DID %s", sigData.ATProto.DID), 548 + Extensions: map[string]any{ 549 "did": sigData.ATProto.DID, 550 "handle": sigData.ATProto.Handle, 551 "signedAt": sigData.ATProto.SignedAt, ··· 673 674 type ProviderResponse struct { 675 SystemError string `json:"system_error,omitempty"` 676 + Responses []map[string]any `json:"responses"` 677 } 678 679 func handleProvide(w http.ResponseWriter, r *http.Request) { ··· 684 } 685 686 // Verify each image 687 + responses := make([]map[string]any, 0, len(req.Values)) 688 for _, image := range req.Values { 689 result, err := verifier.Verify(context.Background(), image) 690 691 + response := map[string]any{ 692 "image": image, 693 "verified": false, 694 }
+4 -4
examples/plugins/gatekeeper-provider/main.go.temp
··· 35 // ProviderResponse is the response format to Gatekeeper. 36 type ProviderResponse struct { 37 SystemError string `json:"system_error,omitempty"` 38 - Responses []map[string]interface{} `json:"responses"` 39 } 40 41 // VerificationResult holds the result of verifying a single image. ··· 110 log.Printf("INFO: received verification request for %d images", len(req.Values)) 111 112 // Verify each image 113 - responses := make([]map[string]interface{}, 0, len(req.Values)) 114 for _, image := range req.Values { 115 result := s.verifyImage(r.Context(), image) 116 responses = append(responses, structToMap(result)) ··· 186 } 187 188 // structToMap converts a struct to a map for JSON encoding. 189 - func structToMap(v interface{}) map[string]interface{} { 190 data, _ := json.Marshal(v) 191 - var m map[string]interface{} 192 json.Unmarshal(data, &m) 193 return m 194 }
··· 35 // ProviderResponse is the response format to Gatekeeper. 36 type ProviderResponse struct { 37 SystemError string `json:"system_error,omitempty"` 38 + Responses []map[string]any `json:"responses"` 39 } 40 41 // VerificationResult holds the result of verifying a single image. ··· 110 log.Printf("INFO: received verification request for %d images", len(req.Values)) 111 112 // Verify each image 113 + responses := make([]map[string]any, 0, len(req.Values)) 114 for _, image := range req.Values { 115 result := s.verifyImage(r.Context(), image) 116 responses = append(responses, structToMap(result)) ··· 186 } 187 188 // structToMap converts a struct to a map for JSON encoding. 189 + func structToMap(v any) map[string]any { 190 data, _ := json.Marshal(v) 191 + var m map[string]any 192 json.Unmarshal(data, &m) 193 return m 194 }
+1 -1
examples/plugins/ratify-verifier/README.md
··· 196 Name string 197 Type string 198 Message string 199 - Extensions map[string]interface{} 200 } 201 ``` 202
··· 196 Name string 197 Type string 198 Message string 199 + Extensions map[string]any 200 } 201 ``` 202
+2 -2
examples/plugins/ratify-verifier/verifier.go.temp
··· 166 Name: v.name, 167 Type: v.Type(), 168 Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID), 169 - Extensions: map[string]interface{}{ 170 "did": sigData.ATProto.DID, 171 "handle": sigData.ATProto.Handle, 172 "signedAt": sigData.ATProto.SignedAt, ··· 203 Name: v.name, 204 Type: v.Type(), 205 Message: message, 206 - Extensions: map[string]interface{}{ 207 "error": message, 208 }, 209 }
··· 166 Name: v.name, 167 Type: v.Type(), 168 Message: fmt.Sprintf("Successfully verified ATProto signature for DID %s", sigData.ATProto.DID), 169 + Extensions: map[string]any{ 170 "did": sigData.ATProto.DID, 171 "handle": sigData.ATProto.Handle, 172 "signedAt": sigData.ATProto.SignedAt, ··· 203 Name: v.name, 204 Type: v.Type(), 205 Message: message, 206 + Extensions: map[string]any{ 207 "error": message, 208 }, 209 }
+5
lexicons/io/atcr/hold/crew.json
··· 29 "maxLength": 64 30 } 31 }, 32 "addedAt": { 33 "type": "string", 34 "format": "datetime",
··· 29 "maxLength": 64 30 } 31 }, 32 + "tier": { 33 + "type": "string", 34 + "description": "Optional tier for quota limits (e.g., 'deckhand', 'bosun', 'quartermaster'). If empty, uses defaults.new_crew_tier from quotas.yaml.", 35 + "maxLength": 32 36 + }, 37 "addedAt": { 38 "type": "string", 39 "format": "datetime",
+4 -9
lexicons/io/atcr/hold/layer.json
··· 8 "description": "Represents metadata about a container layer stored in the hold. Stored in the hold's embedded PDS for tracking and analytics.", 9 "record": { 10 "type": "object", 11 - "required": ["digest", "size", "mediaType", "repository", "userDid", "userHandle", "createdAt"], 12 "properties": { 13 "digest": { 14 "type": "string", ··· 24 "description": "Media type (e.g., application/vnd.oci.image.layer.v1.tar+gzip)", 25 "maxLength": 128 26 }, 27 - "repository": { 28 "type": "string", 29 - "description": "Repository this layer belongs to", 30 - "maxLength": 255 31 }, 32 "userDid": { 33 "type": "string", 34 "format": "did", 35 "description": "DID of user who uploaded this layer" 36 - }, 37 - "userHandle": { 38 - "type": "string", 39 - "format": "handle", 40 - "description": "Handle of user (for display purposes)" 41 }, 42 "createdAt": { 43 "type": "string",
··· 8 "description": "Represents metadata about a container layer stored in the hold. Stored in the hold's embedded PDS for tracking and analytics.", 9 "record": { 10 "type": "object", 11 + "required": ["digest", "size", "mediaType", "manifest", "userDid", "createdAt"], 12 "properties": { 13 "digest": { 14 "type": "string", ··· 24 "description": "Media type (e.g., application/vnd.oci.image.layer.v1.tar+gzip)", 25 "maxLength": 128 26 }, 27 + "manifest": { 28 "type": "string", 29 + "format": "at-uri", 30 + "description": "AT-URI of the manifest that included this layer (e.g., at://did:plc:xyz/io.atcr.manifest/abc123)" 31 }, 32 "userDid": { 33 "type": "string", 34 "format": "did", 35 "description": "DID of user who uploaded this layer" 36 }, 37 "createdAt": { 38 "type": "string",
+1 -2
pkg/appview/config.go
··· 388 return checksums 389 } 390 391 - pairs := strings.Split(checksumsStr, ",") 392 - for _, pair := range pairs { 393 parts := strings.SplitN(strings.TrimSpace(pair), ":", 2) 394 if len(parts) == 2 { 395 platform := strings.TrimSpace(parts[0])
··· 388 return checksums 389 } 390 391 + for pair := range strings.SplitSeq(checksumsStr, ",") { 392 parts := strings.SplitN(strings.TrimSpace(pair), ":", 2) 393 if len(parts) == 2 { 394 platform := strings.TrimSpace(parts[0])
+4 -2
pkg/appview/db/device_store.go
··· 365 } 366 367 // UpdateLastUsed updates the last used timestamp 368 - func (s *DeviceStore) UpdateLastUsed(secretHash string) error { 369 _, err := s.db.Exec(` 370 UPDATE devices 371 SET last_used = ? 372 WHERE secret_hash = ? 373 `, time.Now(), secretHash) 374 375 - return err 376 } 377 378 // CleanupExpired removes expired pending authorizations
··· 365 } 366 367 // UpdateLastUsed updates the last used timestamp 368 + func (s *DeviceStore) UpdateLastUsed(secretHash string) { 369 _, err := s.db.Exec(` 370 UPDATE devices 371 SET last_used = ? 372 WHERE secret_hash = ? 373 `, time.Now(), secretHash) 374 375 + if err != nil { 376 + slog.Warn("Failed to update device last used timestamp", "component", "device_store", "error", err) 377 + } 378 } 379 380 // CleanupExpired removes expired pending authorizations
+4 -10
pkg/appview/db/device_store_test.go
··· 56 func TestGenerateUserCode(t *testing.T) { 57 // Generate multiple codes to test 58 codes := make(map[string]bool) 59 - for i := 0; i < 100; i++ { 60 code := generateUserCode() 61 62 // Test format: XXXX-XXXX ··· 372 return 373 } 374 if !tt.wantErr { 375 - if device == nil { 376 - t.Error("Expected device, got nil") 377 - } 378 if device.DID != "did:plc:alice123" { 379 t.Errorf("DID = %v, want did:plc:alice123", device.DID) 380 } ··· 399 } 400 401 // Create 3 devices 402 - for i := 0; i < 3; i++ { 403 pending, err := store.CreatePendingAuth("Device "+string(rune('A'+i)), "192.168.1.1", "Agent") 404 if err != nil { 405 t.Fatalf("CreatePendingAuth() error = %v", err) ··· 417 } 418 419 // Verify they're sorted by created_at DESC (newest first) 420 - for i := 0; i < len(devices)-1; i++ { 421 if devices[i].CreatedAt.Before(devices[i+1].CreatedAt) { 422 t.Error("Devices should be sorted by created_at DESC") 423 } ··· 521 time.Sleep(10 * time.Millisecond) 522 523 // Update last used 524 - err = store.UpdateLastUsed(device.SecretHash) 525 - if err != nil { 526 - t.Errorf("UpdateLastUsed() error = %v", err) 527 - } 528 529 // Verify it was updated 530 device2, err := store.ValidateDeviceSecret(secret)
··· 56 func TestGenerateUserCode(t *testing.T) { 57 // Generate multiple codes to test 58 codes := make(map[string]bool) 59 + for range 100 { 60 code := generateUserCode() 61 62 // Test format: XXXX-XXXX ··· 372 return 373 } 374 if !tt.wantErr { 375 if device.DID != "did:plc:alice123" { 376 t.Errorf("DID = %v, want did:plc:alice123", device.DID) 377 } ··· 396 } 397 398 // Create 3 devices 399 + for i := range 3 { 400 pending, err := store.CreatePendingAuth("Device "+string(rune('A'+i)), "192.168.1.1", "Agent") 401 if err != nil { 402 t.Fatalf("CreatePendingAuth() error = %v", err) ··· 414 } 415 416 // Verify they're sorted by created_at DESC (newest first) 417 + for i := range len(devices) - 1 { 418 if devices[i].CreatedAt.Before(devices[i+1].CreatedAt) { 419 t.Error("Devices should be sorted by created_at DESC") 420 } ··· 518 time.Sleep(10 * time.Millisecond) 519 520 // Update last used 521 + store.UpdateLastUsed(device.SecretHash) 522 523 // Verify it was updated 524 device2, err := store.ValidateDeviceSecret(secret)
+2 -1
pkg/appview/db/models.go
··· 154 Tag 155 Platforms []PlatformInfo 156 IsMultiArch bool 157 - HasAttestations bool // true if manifest list contains attestation references 158 } 159 160 // ManifestWithMetadata extends Manifest with tags and platform information
··· 154 Tag 155 Platforms []PlatformInfo 156 IsMultiArch bool 157 + HasAttestations bool // true if manifest list contains attestation references 158 + ArtifactType string // container-image, helm-chart, unknown 159 } 160 161 // ManifestWithMetadata extends Manifest with tags and platform information
+21 -41
pkg/appview/db/oauth_store.go
··· 8 "log/slog" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/auth/oauth" 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 ) ··· 213 } 214 215 // CleanupOldSessions removes sessions older than the specified duration 216 - func (s *OAuthStore) CleanupOldSessions(ctx context.Context, olderThan time.Duration) error { 217 cutoff := time.Now().Add(-olderThan) 218 219 result, err := s.db.ExecContext(ctx, ` ··· 222 `, cutoff) 223 224 if err != nil { 225 - return fmt.Errorf("failed to cleanup old sessions: %w", err) 226 } 227 228 deleted, _ := result.RowsAffected() 229 if deleted > 0 { 230 slog.Info("Cleaned up old OAuth sessions", "count", deleted, "older_than", olderThan) 231 } 232 - 233 - return nil 234 } 235 236 // CleanupExpiredAuthRequests removes auth requests older than 10 minutes 237 - func (s *OAuthStore) CleanupExpiredAuthRequests(ctx context.Context) error { 238 cutoff := time.Now().Add(-10 * time.Minute) 239 240 result, err := s.db.ExecContext(ctx, ` ··· 243 `, cutoff) 244 245 if err != nil { 246 - return fmt.Errorf("failed to cleanup auth requests: %w", err) 247 } 248 249 deleted, _ := result.RowsAffected() 250 if deleted > 0 { 251 slog.Info("Cleaned up expired auth requests", "count", deleted) 252 } 253 - 254 - return nil 255 } 256 257 // InvalidateSessionsWithMismatchedScopes removes all sessions whose scopes don't match the desired scopes ··· 285 continue 286 } 287 288 - // Check if scopes match (need to import oauth package for ScopesMatch) 289 - // Since we're in db package, we can't import oauth (circular dependency) 290 - // So we'll implement a simple scope comparison here 291 - if !scopesMatch(sessionData.Scopes, desiredScopes) { 292 sessionsToDelete = append(sessionsToDelete, sessionKey) 293 } 294 } ··· 313 return len(sessionsToDelete), nil 314 } 315 316 - // scopesMatch checks if two scope lists are equivalent (order-independent) 317 - // Local implementation to avoid circular dependency with oauth package 318 - func scopesMatch(stored, desired []string) bool { 319 - if len(stored) == 0 && len(desired) == 0 { 320 - return true 321 - } 322 - if len(stored) != len(desired) { 323 - return false 324 - } 325 - 326 - desiredMap := make(map[string]bool, len(desired)) 327 - for _, scope := range desired { 328 - desiredMap[scope] = true 329 - } 330 - 331 - for _, scope := range stored { 332 - if !desiredMap[scope] { 333 - return false 334 - } 335 - } 336 - 337 - return true 338 - } 339 - 340 // GetSessionStats returns statistics about stored OAuth sessions 341 // Useful for monitoring and debugging session health 342 - func (s *OAuthStore) GetSessionStats(ctx context.Context) (map[string]interface{}, error) { 343 - stats := make(map[string]interface{}) 344 345 // Total sessions 346 var totalSessions int ··· 392 393 // ListSessionsForMonitoring returns a list of all sessions with basic info for monitoring 394 // Returns: DID, session age (minutes), last update time 395 - func (s *OAuthStore) ListSessionsForMonitoring(ctx context.Context) ([]map[string]interface{}, error) { 396 rows, err := s.db.QueryContext(ctx, ` 397 SELECT 398 account_did, ··· 408 } 409 defer rows.Close() 410 411 - var sessions []map[string]interface{} 412 for rows.Next() { 413 var did, sessionID, createdAt, updatedAt string 414 var idleMinutes int ··· 418 continue 419 } 420 421 - sessions = append(sessions, map[string]interface{}{ 422 "did": did, 423 "session_id": sessionID, 424 "created_at": createdAt,
··· 8 "log/slog" 9 "time" 10 11 + atoauth "atcr.io/pkg/auth/oauth" 12 "github.com/bluesky-social/indigo/atproto/auth/oauth" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 ) ··· 214 } 215 216 // CleanupOldSessions removes sessions older than the specified duration 217 + func (s *OAuthStore) CleanupOldSessions(ctx context.Context, olderThan time.Duration) { 218 cutoff := time.Now().Add(-olderThan) 219 220 result, err := s.db.ExecContext(ctx, ` ··· 223 `, cutoff) 224 225 if err != nil { 226 + slog.Warn("Failed to cleanup old OAuth sessions", "component", "oauth_store", "error", err) 227 + return 228 } 229 230 deleted, _ := result.RowsAffected() 231 if deleted > 0 { 232 slog.Info("Cleaned up old OAuth sessions", "count", deleted, "older_than", olderThan) 233 } 234 } 235 236 // CleanupExpiredAuthRequests removes auth requests older than 10 minutes 237 + func (s *OAuthStore) CleanupExpiredAuthRequests(ctx context.Context) { 238 cutoff := time.Now().Add(-10 * time.Minute) 239 240 result, err := s.db.ExecContext(ctx, ` ··· 243 `, cutoff) 244 245 if err != nil { 246 + slog.Warn("Failed to cleanup expired auth requests", "component", "oauth_store", "error", err) 247 + return 248 } 249 250 deleted, _ := result.RowsAffected() 251 if deleted > 0 { 252 slog.Info("Cleaned up expired auth requests", "count", deleted) 253 } 254 } 255 256 // InvalidateSessionsWithMismatchedScopes removes all sessions whose scopes don't match the desired scopes ··· 284 continue 285 } 286 287 + // Check if scopes match (expands include: scopes before comparing) 288 + if !atoauth.ScopesMatch(sessionData.Scopes, desiredScopes) { 289 + slog.Debug("Session has mismatched scopes", 290 + "component", "oauth/store", 291 + "session_key", sessionKey, 292 + "account_did", accountDID, 293 + "session_scopes", sessionData.Scopes, 294 + "desired_scopes", desiredScopes, 295 + ) 296 sessionsToDelete = append(sessionsToDelete, sessionKey) 297 } 298 } ··· 317 return len(sessionsToDelete), nil 318 } 319 320 // GetSessionStats returns statistics about stored OAuth sessions 321 // Useful for monitoring and debugging session health 322 + func (s *OAuthStore) GetSessionStats(ctx context.Context) (map[string]any, error) { 323 + stats := make(map[string]any) 324 325 // Total sessions 326 var totalSessions int ··· 372 373 // ListSessionsForMonitoring returns a list of all sessions with basic info for monitoring 374 // Returns: DID, session age (minutes), last update time 375 + func (s *OAuthStore) ListSessionsForMonitoring(ctx context.Context) ([]map[string]any, error) { 376 rows, err := s.db.QueryContext(ctx, ` 377 SELECT 378 account_did, ··· 388 } 389 defer rows.Close() 390 391 + var sessions []map[string]any 392 for rows.Next() { 393 var did, sessionID, createdAt, updatedAt string 394 var idleMinutes int ··· 398 continue 399 } 400 401 + sessions = append(sessions, map[string]any{ 402 "did": did, 403 "session_id": sessionID, 404 "created_at": createdAt,
+17 -6
pkg/appview/db/oauth_store_test.go
··· 5 "testing" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/auth/oauth" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 ) ··· 161 } 162 163 func TestScopesMatch(t *testing.T) { 164 - // Test the local scopesMatch function to ensure it matches the oauth.ScopesMatch behavior 165 tests := []struct { 166 name string 167 stored []string ··· 204 desired: []string{}, 205 expected: true, 206 }, 207 } 208 209 for _, tt := range tests { 210 t.Run(tt.name, func(t *testing.T) { 211 - result := scopesMatch(tt.stored, tt.desired) 212 if result != tt.expected { 213 - t.Errorf("scopesMatch(%v, %v) = %v, want %v", 214 tt.stored, tt.desired, result, tt.expected) 215 } 216 }) ··· 353 } 354 355 // Run cleanup (remove sessions older than 30 days) 356 - if err := store.CleanupOldSessions(ctx, 30*24*time.Hour); err != nil { 357 - t.Fatalf("Failed to cleanup old sessions: %v", err) 358 - } 359 360 // Verify old session was deleted 361 _, err = store.GetSession(ctx, did1, "old_session")
··· 5 "testing" 6 "time" 7 8 + atcroauth "atcr.io/pkg/auth/oauth" 9 "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 ) ··· 162 } 163 164 func TestScopesMatch(t *testing.T) { 165 + // Test oauth.ScopesMatch function including include: scope expansion 166 tests := []struct { 167 name string 168 stored []string ··· 205 desired: []string{}, 206 expected: true, 207 }, 208 + { 209 + name: "include scope expansion", 210 + stored: []string{ 211 + "atproto", 212 + "repo?collection=io.atcr.manifest&collection=io.atcr.repo.page&collection=io.atcr.sailor.profile&collection=io.atcr.sailor.star&collection=io.atcr.tag", 213 + }, 214 + desired: []string{ 215 + "atproto", 216 + "include:io.atcr.authFullApp", 217 + }, 218 + expected: true, 219 + }, 220 } 221 222 for _, tt := range tests { 223 t.Run(tt.name, func(t *testing.T) { 224 + result := atcroauth.ScopesMatch(tt.stored, tt.desired) 225 if result != tt.expected { 226 + t.Errorf("ScopesMatch(%v, %v) = %v, want %v", 227 tt.stored, tt.desired, result, tt.expected) 228 } 229 }) ··· 366 } 367 368 // Run cleanup (remove sessions older than 30 days) 369 + store.CleanupOldSessions(ctx, 30*24*time.Hour) 370 371 // Verify old session was deleted 372 _, err = store.GetSession(ctx, did1, "old_session")
+6 -4
pkg/appview/db/queries.go
··· 653 t.digest, 654 t.created_at, 655 m.media_type, 656 COALESCE(mr.platform_os, '') as platform_os, 657 COALESCE(mr.platform_architecture, '') as platform_architecture, 658 COALESCE(mr.platform_variant, '') as platform_variant, ··· 676 677 for rows.Next() { 678 var t Tag 679 - var mediaType, platformOS, platformArch, platformVariant, platformOSVersion string 680 var isAttestation bool 681 682 if err := rows.Scan(&t.ID, &t.DID, &t.Repository, &t.Tag, &t.Digest, &t.CreatedAt, 683 - &mediaType, &platformOS, &platformArch, &platformVariant, &platformOSVersion, &isAttestation); err != nil { 684 return nil, err 685 } 686 ··· 688 tagKey := t.Tag 689 if _, exists := tagMap[tagKey]; !exists { 690 tagMap[tagKey] = &TagWithPlatforms{ 691 - Tag: t, 692 - Platforms: []PlatformInfo{}, 693 } 694 tagOrder = append(tagOrder, tagKey) 695 }
··· 653 t.digest, 654 t.created_at, 655 m.media_type, 656 + m.artifact_type, 657 COALESCE(mr.platform_os, '') as platform_os, 658 COALESCE(mr.platform_architecture, '') as platform_architecture, 659 COALESCE(mr.platform_variant, '') as platform_variant, ··· 677 678 for rows.Next() { 679 var t Tag 680 + var mediaType, artifactType, platformOS, platformArch, platformVariant, platformOSVersion string 681 var isAttestation bool 682 683 if err := rows.Scan(&t.ID, &t.DID, &t.Repository, &t.Tag, &t.Digest, &t.CreatedAt, 684 + &mediaType, &artifactType, &platformOS, &platformArch, &platformVariant, &platformOSVersion, &isAttestation); err != nil { 685 return nil, err 686 } 687 ··· 689 tagKey := t.Tag 690 if _, exists := tagMap[tagKey]; !exists { 691 tagMap[tagKey] = &TagWithPlatforms{ 692 + Tag: t, 693 + Platforms: []PlatformInfo{}, 694 + ArtifactType: artifactType, 695 } 696 tagOrder = append(tagOrder, tagKey) 697 }
+64 -11
pkg/appview/db/schema.go
··· 37 return nil, err 38 } 39 40 - // Create schema from embedded SQL file 41 - if _, err := db.Exec(schemaSQL); err != nil { 42 - return nil, err 43 } 44 45 // Run migrations unless skipped 46 if !skipMigrations { 47 - if err := runMigrations(db); err != nil { 48 return nil, err 49 } 50 } ··· 52 return db, nil 53 } 54 55 // Migration represents a database migration 56 type Migration struct { 57 Version int ··· 61 } 62 63 // runMigrations applies any pending database migrations 64 - func runMigrations(db *sql.DB) error { 65 // Load migrations from files 66 migrations, err := loadMigrations() 67 if err != nil { ··· 86 continue 87 } 88 89 - // Apply migration in a transaction 90 slog.Info("Applying migration", "version", m.Version, "name", m.Name, "description", m.Description) 91 92 tx, err := db.Begin() ··· 169 var statements []string 170 171 // Split on semicolons 172 - parts := strings.Split(query, ";") 173 - 174 - for _, part := range parts { 175 // Trim whitespace 176 stmt := strings.TrimSpace(part) 177 ··· 181 } 182 183 // Skip comment-only statements 184 - lines := strings.Split(stmt, "\n") 185 hasCode := false 186 - for _, line := range lines { 187 trimmed := strings.TrimSpace(line) 188 if trimmed != "" && !strings.HasPrefix(trimmed, "--") { 189 hasCode = true
··· 37 return nil, err 38 } 39 40 + // Check if this is an existing database with migrations applied 41 + isExisting, err := hasAppliedMigrations(db) 42 + if err != nil { 43 + return nil, fmt.Errorf("failed to check database state: %w", err) 44 + } 45 + 46 + if isExisting { 47 + // Existing database: skip schema.sql, only run pending migrations 48 + slog.Debug("Existing database detected, skipping schema.sql") 49 + } else { 50 + // Fresh database: apply schema.sql 51 + slog.Info("Fresh database detected, applying schema") 52 + if err := applySchema(db); err != nil { 53 + return nil, err 54 + } 55 } 56 57 // Run migrations unless skipped 58 + // For fresh databases, migrations are recorded but not executed (schema.sql is already complete) 59 if !skipMigrations { 60 + if err := runMigrations(db, !isExisting); err != nil { 61 return nil, err 62 } 63 } ··· 65 return db, nil 66 } 67 68 + // hasAppliedMigrations checks if this is an existing database with migrations applied 69 + func hasAppliedMigrations(db *sql.DB) (bool, error) { 70 + // Check if schema_migrations table exists 71 + var count int 72 + err := db.QueryRow(` 73 + SELECT COUNT(*) FROM sqlite_master 74 + WHERE type='table' AND name='schema_migrations' 75 + `).Scan(&count) 76 + if err != nil { 77 + return false, err 78 + } 79 + if count == 0 { 80 + return false, nil // No migrations table = fresh DB 81 + } 82 + 83 + // Table exists, check if it has entries 84 + err = db.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&count) 85 + if err != nil { 86 + return false, err 87 + } 88 + return count > 0, nil 89 + } 90 + 91 + // applySchema executes schema.sql for fresh databases 92 + func applySchema(db *sql.DB) error { 93 + for _, stmt := range splitSQLStatements(schemaSQL) { 94 + if _, err := db.Exec(stmt); err != nil { 95 + return fmt.Errorf("failed to apply schema: %w", err) 96 + } 97 + } 98 + return nil 99 + } 100 + 101 // Migration represents a database migration 102 type Migration struct { 103 Version int ··· 107 } 108 109 // runMigrations applies any pending database migrations 110 + // If freshDB is true, migrations are recorded but not executed (schema.sql already includes their changes) 111 + func runMigrations(db *sql.DB, freshDB bool) error { 112 // Load migrations from files 113 migrations, err := loadMigrations() 114 if err != nil { ··· 133 continue 134 } 135 136 + if freshDB { 137 + // Fresh database: schema.sql already has everything, just record the migration 138 + slog.Debug("Recording migration as applied (fresh DB)", "version", m.Version, "name", m.Name) 139 + if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", m.Version); err != nil { 140 + return fmt.Errorf("failed to record migration %d: %w", m.Version, err) 141 + } 142 + continue 143 + } 144 + 145 + // Existing database: apply migration in a transaction 146 slog.Info("Applying migration", "version", m.Version, "name", m.Name, "description", m.Description) 147 148 tx, err := db.Begin() ··· 225 var statements []string 226 227 // Split on semicolons 228 + for part := range strings.SplitSeq(query, ";") { 229 // Trim whitespace 230 stmt := strings.TrimSpace(part) 231 ··· 235 } 236 237 // Skip comment-only statements 238 hasCode := false 239 + for line := range strings.SplitSeq(stmt, "\n") { 240 trimmed := strings.TrimSpace(line) 241 if trimmed != "" && !strings.HasPrefix(trimmed, "--") { 242 hasCode = true
+2 -2
pkg/appview/db/session_store_test.go
··· 252 253 // Create multiple sessions for alice 254 sessionIDs := make([]string, 3) 255 - for i := 0; i < 3; i++ { 256 id, err := store.Create(did, "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 257 if err != nil { 258 t.Fatalf("Create() error = %v", err) ··· 516 517 // Generate multiple session IDs 518 ids := make(map[string]bool) 519 - for i := 0; i < 100; i++ { 520 id, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 521 if err != nil { 522 t.Fatalf("Create() error = %v", err)
··· 252 253 // Create multiple sessions for alice 254 sessionIDs := make([]string, 3) 255 + for i := range 3 { 256 id, err := store.Create(did, "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 257 if err != nil { 258 t.Fatalf("Create() error = %v", err) ··· 516 517 // Generate multiple session IDs 518 ids := make(map[string]bool) 519 + for range 100 { 520 id, err := store.Create("did:plc:alice123", "alice.bsky.social", "https://pds.example.com", 1*time.Hour) 521 if err != nil { 522 t.Fatalf("Create() error = %v", err)
+1 -1
pkg/appview/handlers/images.go
··· 95 96 w.Header().Set("Content-Type", "application/json") 97 w.WriteHeader(http.StatusConflict) 98 - json.NewEncoder(w).Encode(map[string]interface{}{ 99 "error": "confirmation_required", 100 "message": "This manifest has associated tags that will also be deleted", 101 "tags": tags,
··· 95 96 w.Header().Set("Content-Type", "application/json") 97 w.WriteHeader(http.StatusConflict) 98 + json.NewEncoder(w).Encode(map[string]any{ 99 "error": "confirmation_required", 100 "message": "This manifest has associated tags that will also be deleted", 101 "tags": tags,
+44
pkg/appview/handlers/legal.go
···
··· 1 + package handlers 2 + 3 + import ( 4 + "html/template" 5 + "net/http" 6 + ) 7 + 8 + // PrivacyPolicyHandler handles the /privacy page 9 + type PrivacyPolicyHandler struct { 10 + Templates *template.Template 11 + RegistryURL string 12 + } 13 + 14 + func (h *PrivacyPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 15 + data := struct { 16 + PageData 17 + }{ 18 + PageData: NewPageData(r, h.RegistryURL), 19 + } 20 + 21 + if err := h.Templates.ExecuteTemplate(w, "privacy", data); err != nil { 22 + http.Error(w, err.Error(), http.StatusInternalServerError) 23 + return 24 + } 25 + } 26 + 27 + // TermsOfServiceHandler handles the /terms page 28 + type TermsOfServiceHandler struct { 29 + Templates *template.Template 30 + RegistryURL string 31 + } 32 + 33 + func (h *TermsOfServiceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 34 + data := struct { 35 + PageData 36 + }{ 37 + PageData: NewPageData(r, h.RegistryURL), 38 + } 39 + 40 + if err := h.Templates.ExecuteTemplate(w, "terms", data); err != nil { 41 + http.Error(w, err.Error(), http.StatusInternalServerError) 42 + return 43 + } 44 + }
+1 -1
pkg/appview/handlers/opengraph.go
··· 105 106 if licenses != "" { 107 // Show first license if multiple 108 - license := strings.Split(licenses, ",")[0] 109 license = strings.TrimSpace(license) 110 card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText) 111 }
··· 105 106 if licenses != "" { 107 // Show first license if multiple 108 + license, _, _ := strings.Cut(licenses, ",") 109 license = strings.TrimSpace(license) 110 card.DrawBadge(license, badgeX, badgeY, ogcard.FontBadge, ogcard.ColorBadgeBg, ogcard.ColorText) 111 }
+17 -17
pkg/appview/handlers/repository.go
··· 89 continue 90 } 91 92 - wg.Add(1) 93 - go func(idx int) { 94 - defer wg.Done() 95 - 96 - endpoint := manifests[idx].HoldEndpoint 97 98 // Try to get cached status first (instant) 99 if cached := h.HealthChecker.GetCachedStatus(endpoint); cached != nil { 100 mu.Lock() 101 - manifests[idx].Reachable = cached.Reachable 102 - manifests[idx].Pending = false 103 mu.Unlock() 104 return 105 } ··· 110 mu.Lock() 111 if ctx.Err() == context.DeadlineExceeded { 112 // Timeout - mark as pending for HTMX polling 113 - manifests[idx].Reachable = false 114 - manifests[idx].Pending = true 115 } else if err != nil { 116 // Error - mark as unreachable 117 - manifests[idx].Reachable = false 118 - manifests[idx].Pending = false 119 } else { 120 // Success 121 - manifests[idx].Reachable = reachable 122 - manifests[idx].Pending = false 123 } 124 mu.Unlock() 125 - }(i) 126 } 127 128 // Wait for all checks to complete or timeout ··· 231 } 232 } 233 234 - // Determine dominant artifact type from manifests 235 artifactType := "container-image" 236 - if len(manifests) > 0 { 237 - // Use the most recent manifest's artifact type 238 artifactType = manifests[0].ArtifactType 239 } 240
··· 89 continue 90 } 91 92 + wg.Go(func() { 93 + endpoint := manifests[i].HoldEndpoint 94 95 // Try to get cached status first (instant) 96 if cached := h.HealthChecker.GetCachedStatus(endpoint); cached != nil { 97 mu.Lock() 98 + manifests[i].Reachable = cached.Reachable 99 + manifests[i].Pending = false 100 mu.Unlock() 101 return 102 } ··· 107 mu.Lock() 108 if ctx.Err() == context.DeadlineExceeded { 109 // Timeout - mark as pending for HTMX polling 110 + manifests[i].Reachable = false 111 + manifests[i].Pending = true 112 } else if err != nil { 113 // Error - mark as unreachable 114 + manifests[i].Reachable = false 115 + manifests[i].Pending = false 116 } else { 117 // Success 118 + manifests[i].Reachable = reachable 119 + manifests[i].Pending = false 120 } 121 mu.Unlock() 122 + }) 123 } 124 125 // Wait for all checks to complete or timeout ··· 228 } 229 } 230 231 + // Determine artifact type for header section from first tag 232 + // This is used for the "Pull this image/chart" header command 233 artifactType := "container-image" 234 + if len(tagsWithPlatforms) > 0 { 235 + artifactType = tagsWithPlatforms[0].ArtifactType 236 + } else if len(manifests) > 0 { 237 + // Fallback to manifests if no tags 238 artifactType = manifests[0].ArtifactType 239 } 240
+153
pkg/appview/handlers/storage.go
···
··· 1 + package handlers 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "html/template" 7 + "log/slog" 8 + "net/http" 9 + 10 + "atcr.io/pkg/appview/middleware" 11 + "atcr.io/pkg/appview/storage" 12 + "atcr.io/pkg/atproto" 13 + "atcr.io/pkg/auth/oauth" 14 + ) 15 + 16 + // StorageHandler handles the storage quota API endpoint 17 + // Returns an HTML partial for HTMX to swap into the settings page 18 + type StorageHandler struct { 19 + Templates *template.Template 20 + Refresher *oauth.Refresher 21 + } 22 + 23 + // QuotaStats mirrors the hold service response 24 + type QuotaStats struct { 25 + UserDID string `json:"userDid"` 26 + UniqueBlobs int `json:"uniqueBlobs"` 27 + TotalSize int64 `json:"totalSize"` 28 + Limit *int64 `json:"limit,omitempty"` // nil = unlimited 29 + Tier string `json:"tier,omitempty"` // e.g., "deckhand", "bosun", "owner" 30 + } 31 + 32 + func (h *StorageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 33 + user := middleware.GetUser(r) 34 + if user == nil { 35 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 36 + return 37 + } 38 + 39 + // Create ATProto client with session provider 40 + client := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher) 41 + 42 + // Get user's sailor profile to find their default hold 43 + profile, err := storage.GetProfile(r.Context(), client) 44 + if err != nil { 45 + slog.Warn("Failed to get profile for storage quota", "did", user.DID, "error", err) 46 + h.renderError(w, "Failed to load profile") 47 + return 48 + } 49 + 50 + if profile == nil || profile.DefaultHold == "" { 51 + // No default hold configured - can't check quota 52 + h.renderNoHold(w) 53 + return 54 + } 55 + 56 + // Resolve hold URL from DID 57 + holdURL := atproto.ResolveHoldURL(profile.DefaultHold) 58 + if holdURL == "" { 59 + slog.Warn("Failed to resolve hold URL", "did", user.DID, "holdDid", profile.DefaultHold) 60 + h.renderError(w, "Failed to resolve hold service") 61 + return 62 + } 63 + 64 + // Call the hold's quota endpoint 65 + quotaURL := fmt.Sprintf("%s%s?userDid=%s", holdURL, atproto.HoldGetQuota, user.DID) 66 + resp, err := http.Get(quotaURL) 67 + if err != nil { 68 + slog.Warn("Failed to fetch quota from hold", "did", user.DID, "holdURL", holdURL, "error", err) 69 + h.renderError(w, "Failed to connect to hold service") 70 + return 71 + } 72 + defer resp.Body.Close() 73 + 74 + if resp.StatusCode != http.StatusOK { 75 + slog.Warn("Hold returned error for quota", "did", user.DID, "status", resp.StatusCode) 76 + h.renderError(w, "Hold service returned an error") 77 + return 78 + } 79 + 80 + var stats QuotaStats 81 + if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil { 82 + slog.Warn("Failed to decode quota response", "did", user.DID, "error", err) 83 + h.renderError(w, "Failed to parse quota data") 84 + return 85 + } 86 + 87 + // Render the stats partial 88 + h.renderStats(w, stats) 89 + } 90 + 91 + func (h *StorageHandler) renderStats(w http.ResponseWriter, stats QuotaStats) { 92 + // Calculate usage percentage if limit exists 93 + var usagePercent int 94 + var hasLimit bool 95 + var humanLimit string 96 + 97 + if stats.Limit != nil && *stats.Limit > 0 { 98 + hasLimit = true 99 + humanLimit = humanizeBytes(*stats.Limit) 100 + usagePercent = int(float64(stats.TotalSize) / float64(*stats.Limit) * 100) 101 + if usagePercent > 100 { 102 + usagePercent = 100 103 + } 104 + } 105 + 106 + data := struct { 107 + UniqueBlobs int 108 + TotalSize int64 109 + HumanSize string 110 + HasLimit bool 111 + HumanLimit string 112 + UsagePercent int 113 + Tier string 114 + }{ 115 + UniqueBlobs: stats.UniqueBlobs, 116 + TotalSize: stats.TotalSize, 117 + HumanSize: humanizeBytes(stats.TotalSize), 118 + HasLimit: hasLimit, 119 + HumanLimit: humanLimit, 120 + UsagePercent: usagePercent, 121 + Tier: stats.Tier, 122 + } 123 + 124 + w.Header().Set("Content-Type", "text/html") 125 + if err := h.Templates.ExecuteTemplate(w, "storage_stats", data); err != nil { 126 + slog.Error("Failed to render storage stats template", "error", err) 127 + http.Error(w, "Failed to render template", http.StatusInternalServerError) 128 + } 129 + } 130 + 131 + func (h *StorageHandler) renderError(w http.ResponseWriter, message string) { 132 + w.Header().Set("Content-Type", "text/html") 133 + fmt.Fprintf(w, `<div class="storage-error"><i data-lucide="alert-circle"></i> %s</div>`, message) 134 + } 135 + 136 + func (h *StorageHandler) renderNoHold(w http.ResponseWriter) { 137 + w.Header().Set("Content-Type", "text/html") 138 + fmt.Fprint(w, `<div class="storage-info"><i data-lucide="info"></i> No hold configured. Set a default hold above to see storage usage.</div>`) 139 + } 140 + 141 + // humanizeBytes converts bytes to human-readable format 142 + func humanizeBytes(bytes int64) string { 143 + const unit = 1024 144 + if bytes < unit { 145 + return fmt.Sprintf("%d B", bytes) 146 + } 147 + div, exp := int64(unit), 0 148 + for n := bytes / unit; n >= unit; n /= unit { 149 + div *= unit 150 + exp++ 151 + } 152 + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) 153 + }
+7 -14
pkg/appview/holdhealth/worker.go
··· 53 54 // Start begins the background worker 55 func (w *Worker) Start(ctx context.Context) { 56 - w.wg.Add(1) 57 - go func() { 58 - defer w.wg.Done() 59 - 60 slog.Info("Hold health worker starting background health checks") 61 62 // Wait for services to be ready (Docker startup race condition) ··· 89 w.checker.Cleanup() 90 } 91 } 92 - }() 93 } 94 95 // Stop gracefully stops the worker ··· 154 var statsMu sync.Mutex 155 156 for _, endpoint := range uniqueEndpoints { 157 - wg.Add(1) 158 - 159 - go func(ep string) { 160 - defer wg.Done() 161 - 162 // Acquire semaphore 163 sem <- struct{}{} 164 defer func() { <-sem }() 165 166 // Check health 167 - isReachable, err := w.checker.CheckHealth(ctx, ep) 168 169 // Update cache 170 - w.checker.SetStatus(ep, isReachable, err) 171 172 // Update stats 173 statsMu.Lock() ··· 175 reachable++ 176 } else { 177 unreachable++ 178 - slog.Warn("Hold health worker hold unreachable", "endpoint", ep, "error", err) 179 } 180 statsMu.Unlock() 181 - }(endpoint) 182 } 183 184 // Wait for all checks to complete
··· 53 54 // Start begins the background worker 55 func (w *Worker) Start(ctx context.Context) { 56 + w.wg.Go(func() { 57 slog.Info("Hold health worker starting background health checks") 58 59 // Wait for services to be ready (Docker startup race condition) ··· 86 w.checker.Cleanup() 87 } 88 } 89 + }) 90 } 91 92 // Stop gracefully stops the worker ··· 151 var statsMu sync.Mutex 152 153 for _, endpoint := range uniqueEndpoints { 154 + wg.Go(func() { 155 // Acquire semaphore 156 sem <- struct{}{} 157 defer func() { <-sem }() 158 159 // Check health 160 + isReachable, err := w.checker.CheckHealth(ctx, endpoint) 161 162 // Update cache 163 + w.checker.SetStatus(endpoint, isReachable, err) 164 165 // Update stats 166 statsMu.Lock() ··· 168 reachable++ 169 } else { 170 unreachable++ 171 + slog.Warn("Hold health worker hold unreachable", "endpoint", endpoint, "error", err) 172 } 173 statsMu.Unlock() 174 + }) 175 } 176 177 // Wait for all checks to complete
-13
pkg/appview/holdhealth/worker_test.go
··· 1 - package holdhealth 2 - 3 - import "testing" 4 - 5 - func TestWorker_Struct(t *testing.T) { 6 - // Simple struct test 7 - worker := &Worker{} 8 - if worker == nil { 9 - t.Error("Expected non-nil worker") 10 - } 11 - } 12 - 13 - // TODO: Add background health check tests
···
+1 -1
pkg/appview/jetstream/processor_test.go
··· 675 } 676 677 // Test 5: Process multiple deactivation events (idempotent) 678 - for i := 0; i < 3; i++ { 679 err = processor.ProcessAccount(context.Background(), testDID, false, "deactivated") 680 if err != nil { 681 t.Logf("Expected cache invalidation error on iteration %d: %v", i, err)
··· 675 } 676 677 // Test 5: Process multiple deactivation events (idempotent) 678 + for i := range 3 { 679 err = processor.ProcessAccount(context.Background(), testDID, false, "deactivated") 680 if err != nil { 681 t.Logf("Expected cache invalidation error on iteration %d: %v", i, err)
+1 -2
pkg/appview/jetstream/worker.go
··· 128 129 // Reset read deadline - we know connection is alive 130 // Allow 90 seconds for next pong (3x ping interval) 131 - conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 132 - return nil 133 }) 134 135 // Set initial read deadline
··· 128 129 // Reset read deadline - we know connection is alive 130 // Allow 90 seconds for next pong (3x ping interval) 131 + return conn.SetReadDeadline(time.Now().Add(90 * time.Second)) 132 }) 133 134 // Set initial read deadline
+1 -3
pkg/appview/licenses/licenses.go
··· 129 licensesStr = strings.ReplaceAll(licensesStr, " OR ", ",") 130 licensesStr = strings.ReplaceAll(licensesStr, ";", ",") 131 132 - parts := strings.Split(licensesStr, ",") 133 - 134 var result []LicenseInfo 135 seen := make(map[string]bool) // Deduplicate 136 137 - for _, part := range parts { 138 part = strings.TrimSpace(part) 139 if part == "" { 140 continue
··· 129 licensesStr = strings.ReplaceAll(licensesStr, " OR ", ",") 130 licensesStr = strings.ReplaceAll(licensesStr, ";", ",") 131 132 var result []LicenseInfo 133 seen := make(map[string]bool) // Deduplicate 134 135 + for part := range strings.SplitSeq(licensesStr, ",") { 136 part = strings.TrimSpace(part) 137 if part == "" { 138 continue
+6 -9
pkg/appview/middleware/auth_test.go
··· 318 // Pre-create all users and sessions before concurrent access 319 // This ensures database is fully initialized before goroutines start 320 sessionIDs := make([]string, 10) 321 - for i := 0; i < 10; i++ { 322 did := fmt.Sprintf("did:plc:user%d", i) 323 handle := fmt.Sprintf("user%d.bsky.social", i) 324 ··· 358 var wg sync.WaitGroup 359 var mu sync.Mutex // Protect results map 360 361 - for i := 0; i < 10; i++ { 362 - wg.Add(1) 363 - go func(index int, sessionID string) { 364 - defer wg.Done() 365 - 366 req := httptest.NewRequest("GET", "/test", nil) 367 req.AddCookie(&http.Cookie{ 368 Name: "atcr_session", 369 - Value: sessionID, 370 }) 371 w := httptest.NewRecorder() 372 373 wrappedHandler.ServeHTTP(w, req) 374 375 mu.Lock() 376 - results[index] = w.Code 377 mu.Unlock() 378 - }(i, sessionIDs[i]) 379 } 380 381 wg.Wait()
··· 318 // Pre-create all users and sessions before concurrent access 319 // This ensures database is fully initialized before goroutines start 320 sessionIDs := make([]string, 10) 321 + for i := range 10 { 322 did := fmt.Sprintf("did:plc:user%d", i) 323 handle := fmt.Sprintf("user%d.bsky.social", i) 324 ··· 358 var wg sync.WaitGroup 359 var mu sync.Mutex // Protect results map 360 361 + for i := range results { 362 + wg.Go(func() { 363 req := httptest.NewRequest("GET", "/test", nil) 364 req.AddCookie(&http.Cookie{ 365 Name: "atcr_session", 366 + Value: sessionIDs[i], 367 }) 368 w := httptest.NewRecorder() 369 370 wrappedHandler.ServeHTTP(w, req) 371 372 mu.Lock() 373 + results[i] = w.Code 374 mu.Unlock() 375 + }) 376 } 377 378 wg.Wait()
+1 -1
pkg/appview/middleware/registry.go
··· 555 556 // Store HTTP method in context for routing decisions 557 // This is used by routing_repository.go to distinguish pull (GET/HEAD) from push (PUT/POST) 558 - ctx = context.WithValue(ctx, "http.request.method", r.Method) 559 560 // Extract Authorization header 561 authHeader := r.Header.Get("Authorization")
··· 555 556 // Store HTTP method in context for routing decisions 557 // This is used by routing_repository.go to distinguish pull (GET/HEAD) from push (PUT/POST) 558 + ctx = context.WithValue(ctx, storage.HTTPRequestMethod, r.Method) 559 560 // Extract Authorization header 561 authHeader := r.Header.Get("Authorization")
-6
pkg/appview/middleware/registry_test.go
··· 45 return nil 46 } 47 48 - // mockRepository is a minimal mock implementation 49 - type mockRepository struct { 50 - distribution.Repository 51 - name string 52 - } 53 - 54 func TestSetGlobalRefresher(t *testing.T) { 55 // Test that SetGlobalRefresher doesn't panic 56 SetGlobalRefresher(nil)
··· 45 return nil 46 } 47 48 func TestSetGlobalRefresher(t *testing.T) { 49 // Test that SetGlobalRefresher doesn't panic 50 SetGlobalRefresher(nil)
+14 -13
pkg/appview/ogcard/card.go
··· 143 defer face.Close() 144 145 textWidth := font.MeasureString(face, text).Round() 146 - if align == AlignCenter { 147 x -= float64(textWidth) / 2 148 - } else if align == AlignRight { 149 x -= float64(textWidth) 150 } 151 } ··· 292 // DrawRoundedRect draws a filled rounded rectangle 293 func (c *Card) DrawRoundedRect(x, y, w, h, radius int, col color.Color) { 294 // Draw main rectangle (without corners) 295 - for dy := radius; dy < h-radius; dy++ { 296 - for dx := 0; dx < w; dx++ { 297 - c.img.Set(x+dx, y+dy, col) 298 } 299 } 300 // Draw top and bottom strips (without corners) 301 - for dy := 0; dy < radius; dy++ { 302 - for dx := radius; dx < w-radius; dx++ { 303 - c.img.Set(x+dx, y+dy, col) 304 - c.img.Set(x+dx, y+h-1-dy, col) 305 } 306 } 307 // Draw rounded corners 308 - for dy := 0; dy < radius; dy++ { 309 - for dx := 0; dx < radius; dx++ { 310 // Check if point is within circle 311 cx := radius - dx - 1 312 cy := radius - dy - 1 ··· 388 centerX := radius 389 centerY := radius 390 391 - for y := 0; y < diameter; y++ { 392 - for x := 0; x < diameter; x++ { 393 dx := x - centerX 394 dy := y - centerY 395 if dx*dx+dy*dy <= radius*radius {
··· 143 defer face.Close() 144 145 textWidth := font.MeasureString(face, text).Round() 146 + switch align { 147 + case AlignCenter: 148 x -= float64(textWidth) / 2 149 + case AlignRight: 150 x -= float64(textWidth) 151 } 152 } ··· 293 // DrawRoundedRect draws a filled rounded rectangle 294 func (c *Card) DrawRoundedRect(x, y, w, h, radius int, col color.Color) { 295 // Draw main rectangle (without corners) 296 + for dy := range h - 2*radius { 297 + for dx := range w { 298 + c.img.Set(x+dx, y+radius+dy, col) 299 } 300 } 301 // Draw top and bottom strips (without corners) 302 + for dy := range radius { 303 + for dx := range w - 2*radius { 304 + c.img.Set(x+radius+dx, y+dy, col) 305 + c.img.Set(x+radius+dx, y+h-1-dy, col) 306 } 307 } 308 // Draw rounded corners 309 + for dy := range radius { 310 + for dx := range radius { 311 // Check if point is within circle 312 cx := radius - dx - 1 313 cy := radius - dy - 1 ··· 389 centerX := radius 390 centerY := radius 391 392 + for y := range diameter { 393 + for x := range diameter { 394 dx := x - centerX 395 dy := y - centerY 396 if dx*dx+dy*dy <= radius*radius {
+62 -16
pkg/appview/readme/fetcher.go
··· 1 package readme 2 3 import ( ··· 70 // FetchAndRender fetches a README from a URL and renders it as HTML 71 // Returns the rendered HTML and any error 72 func (f *Fetcher) FetchAndRender(ctx context.Context, readmeURL string) (string, error) { 73 - // Validate URL 74 - if readmeURL == "" { 75 - return "", fmt.Errorf("empty README URL") 76 - } 77 - 78 - parsedURL, err := url.Parse(readmeURL) 79 - if err != nil { 80 - return "", fmt.Errorf("invalid README URL: %w", err) 81 - } 82 - 83 - // Only allow HTTP/HTTPS 84 - if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { 85 - return "", fmt.Errorf("invalid URL scheme: %s", parsedURL.Scheme) 86 - } 87 - 88 - // Fetch content 89 content, baseURL, err := f.fetchContent(ctx, readmeURL) 90 if err != nil { 91 return "", err ··· 100 return html, nil 101 } 102 103 // fetchContent fetches the raw content from a URL 104 func (f *Fetcher) fetchContent(ctx context.Context, urlStr string) ([]byte, string, error) { 105 req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil) 106 if err != nil { 107 return nil, "", fmt.Errorf("failed to create request: %w", err) ··· 120 return nil, "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) 121 } 122 123 // Limit content size to 1MB 124 limitedReader := io.LimitReader(resp.Body, 1*1024*1024) 125 content, err := io.ReadAll(limitedReader) ··· 127 return nil, "", fmt.Errorf("failed to read response body: %w", err) 128 } 129 130 // Get base URL for relative link resolution 131 baseURL := getBaseURL(resp.Request.URL) 132 133 return content, baseURL, nil 134 } 135 136 // renderMarkdown renders markdown content to sanitized HTML
··· 1 + // Package readme provides fetching and rendering of README files from Git hosting platforms. 2 package readme 3 4 import ( ··· 71 // FetchAndRender fetches a README from a URL and renders it as HTML 72 // Returns the rendered HTML and any error 73 func (f *Fetcher) FetchAndRender(ctx context.Context, readmeURL string) (string, error) { 74 + // Fetch content (includes URL validation, Content-Type check, and HTML detection) 75 content, baseURL, err := f.fetchContent(ctx, readmeURL) 76 if err != nil { 77 return "", err ··· 86 return html, nil 87 } 88 89 + // FetchRaw fetches raw README content from a URL without rendering 90 + // Returns raw bytes with Content-Type and HTML validation 91 + // Use this when you need to store the raw markdown (e.g., in PDS records) 92 + func (f *Fetcher) FetchRaw(ctx context.Context, readmeURL string) ([]byte, error) { 93 + // Fetch content (includes URL validation, Content-Type check, and HTML detection) 94 + content, _, err := f.fetchContent(ctx, readmeURL) 95 + if err != nil { 96 + return nil, err 97 + } 98 + 99 + return content, nil 100 + } 101 + 102 // fetchContent fetches the raw content from a URL 103 func (f *Fetcher) fetchContent(ctx context.Context, urlStr string) ([]byte, string, error) { 104 + // Validate URL 105 + if urlStr == "" { 106 + return nil, "", fmt.Errorf("empty README URL") 107 + } 108 + 109 + parsedURL, err := url.Parse(urlStr) 110 + if err != nil { 111 + return nil, "", fmt.Errorf("invalid README URL: %w", err) 112 + } 113 + 114 + // Only allow HTTP/HTTPS 115 + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { 116 + return nil, "", fmt.Errorf("invalid URL scheme: %s", parsedURL.Scheme) 117 + } 118 + 119 req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil) 120 if err != nil { 121 return nil, "", fmt.Errorf("failed to create request: %w", err) ··· 134 return nil, "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) 135 } 136 137 + // Reject HTML content types (catches proper error pages) 138 + contentType := resp.Header.Get("Content-Type") 139 + if contentType != "" { 140 + ct := strings.ToLower(contentType) 141 + if strings.Contains(ct, "text/html") || strings.Contains(ct, "application/xhtml") { 142 + return nil, "", fmt.Errorf("unsupported content type: %s (expected markdown or plain text)", contentType) 143 + } 144 + } 145 + 146 // Limit content size to 1MB 147 limitedReader := io.LimitReader(resp.Body, 1*1024*1024) 148 content, err := io.ReadAll(limitedReader) ··· 150 return nil, "", fmt.Errorf("failed to read response body: %w", err) 151 } 152 153 + // Detect HTML content by checking for common markers (catches soft 404s) 154 + if LooksLikeHTML(content) { 155 + return nil, "", fmt.Errorf("detected HTML content instead of markdown") 156 + } 157 + 158 // Get base URL for relative link resolution 159 baseURL := getBaseURL(resp.Request.URL) 160 161 return content, baseURL, nil 162 + } 163 + 164 + // LooksLikeHTML checks if content appears to be HTML rather than markdown 165 + // Exported for use by other packages that fetch README content 166 + func LooksLikeHTML(content []byte) bool { 167 + if len(content) == 0 { 168 + return false 169 + } 170 + 171 + // Check first 512 bytes for HTML markers 172 + checkLen := min(len(content), 512) 173 + 174 + trimmed := strings.TrimSpace(string(content[:checkLen])) 175 + lower := strings.ToLower(trimmed) 176 + 177 + return strings.HasPrefix(lower, "<!doctype") || 178 + strings.HasPrefix(lower, "<html") || 179 + strings.HasPrefix(lower, "<?xml") 180 } 181 182 // renderMarkdown renders markdown content to sanitized HTML
+272 -2
pkg/appview/readme/fetcher_test.go
··· 1 package readme 2 3 import ( 4 "net/url" 5 "testing" 6 ) 7 ··· 297 } 298 299 func containsSubstringHelper(s, substr string) bool { 300 - for i := 0; i <= len(s)-len(substr); i++ { 301 if s[i:i+len(substr)] == substr { 302 return true 303 } ··· 305 return false 306 } 307 308 - // TODO: Add README fetching and caching tests
··· 1 package readme 2 3 import ( 4 + "context" 5 + "net/http" 6 + "net/http/httptest" 7 "net/url" 8 + "strings" 9 "testing" 10 ) 11 ··· 301 } 302 303 func containsSubstringHelper(s, substr string) bool { 304 + for i := range len(s) - len(substr) + 1 { 305 if s[i:i+len(substr)] == substr { 306 return true 307 } ··· 309 return false 310 } 311 312 + func TestLooksLikeHTML(t *testing.T) { 313 + tests := []struct { 314 + name string 315 + content string 316 + expected bool 317 + }{ 318 + { 319 + name: "empty content", 320 + content: "", 321 + expected: false, 322 + }, 323 + { 324 + name: "markdown content", 325 + content: "# Hello World\n\nThis is a README.", 326 + expected: false, 327 + }, 328 + { 329 + name: "plain text", 330 + content: "Just some plain text without any HTML.", 331 + expected: false, 332 + }, 333 + { 334 + name: "doctype html", 335 + content: "<!DOCTYPE html>\n<html><body>Page</body></html>", 336 + expected: true, 337 + }, 338 + { 339 + name: "doctype html lowercase", 340 + content: "<!doctype html>\n<html><body>Page</body></html>", 341 + expected: true, 342 + }, 343 + { 344 + name: "html tag only", 345 + content: "<html><head></head><body>Page</body></html>", 346 + expected: true, 347 + }, 348 + { 349 + name: "html tag with whitespace", 350 + content: " \n <html>\n<body>Page</body></html>", 351 + expected: true, 352 + }, 353 + { 354 + name: "xml declaration", 355 + content: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<html>...</html>", 356 + expected: true, 357 + }, 358 + { 359 + name: "soft 404 page", 360 + content: "<!DOCTYPE html><html><head><title>Page Not Found</title></head><body><h1>404</h1></body></html>", 361 + expected: true, 362 + }, 363 + { 364 + name: "markdown with inline html", 365 + content: "# Title\n\nSome text with <strong>bold</strong> inline.", 366 + expected: false, 367 + }, 368 + { 369 + name: "markdown starting with hash", 370 + content: "## Section\n\nContent here.", 371 + expected: false, 372 + }, 373 + } 374 + 375 + for _, tt := range tests { 376 + t.Run(tt.name, func(t *testing.T) { 377 + result := LooksLikeHTML([]byte(tt.content)) 378 + if result != tt.expected { 379 + t.Errorf("looksLikeHTML(%q) = %v, want %v", tt.content, result, tt.expected) 380 + } 381 + }) 382 + } 383 + } 384 + 385 + func TestFetcher_FetchRaw(t *testing.T) { 386 + fetcher := NewFetcher() 387 + 388 + tests := []struct { 389 + name string 390 + handler http.HandlerFunc 391 + wantErr bool 392 + errContains string 393 + wantContent string 394 + }{ 395 + { 396 + name: "successful markdown fetch", 397 + handler: func(w http.ResponseWriter, r *http.Request) { 398 + w.Header().Set("Content-Type", "text/plain") 399 + w.Write([]byte("# Hello World\n\nThis is markdown.")) 400 + }, 401 + wantErr: false, 402 + wantContent: "# Hello World", 403 + }, 404 + { 405 + name: "rejects HTML content type", 406 + handler: func(w http.ResponseWriter, r *http.Request) { 407 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 408 + w.Write([]byte("<html><body>Error</body></html>")) 409 + }, 410 + wantErr: true, 411 + errContains: "unsupported content type", 412 + }, 413 + { 414 + name: "rejects soft 404 HTML content", 415 + handler: func(w http.ResponseWriter, r *http.Request) { 416 + w.Header().Set("Content-Type", "text/plain") 417 + w.Write([]byte("<!DOCTYPE html><html><body>404 Not Found</body></html>")) 418 + }, 419 + wantErr: true, 420 + errContains: "detected HTML content", 421 + }, 422 + { 423 + name: "rejects 404 status", 424 + handler: func(w http.ResponseWriter, r *http.Request) { 425 + w.WriteHeader(http.StatusNotFound) 426 + w.Write([]byte("Not Found")) 427 + }, 428 + wantErr: true, 429 + errContains: "unexpected status code: 404", 430 + }, 431 + { 432 + name: "rejects 500 status", 433 + handler: func(w http.ResponseWriter, r *http.Request) { 434 + w.WriteHeader(http.StatusInternalServerError) 435 + w.Write([]byte("Internal Server Error")) 436 + }, 437 + wantErr: true, 438 + errContains: "unexpected status code: 500", 439 + }, 440 + } 441 + 442 + for _, tt := range tests { 443 + t.Run(tt.name, func(t *testing.T) { 444 + server := httptest.NewServer(tt.handler) 445 + defer server.Close() 446 + 447 + content, err := fetcher.FetchRaw(context.Background(), server.URL) 448 + 449 + if tt.wantErr { 450 + if err == nil { 451 + t.Errorf("FetchRaw() expected error containing %q, got nil", tt.errContains) 452 + return 453 + } 454 + if !strings.Contains(err.Error(), tt.errContains) { 455 + t.Errorf("FetchRaw() error = %q, want error containing %q", err.Error(), tt.errContains) 456 + } 457 + return 458 + } 459 + 460 + if err != nil { 461 + t.Errorf("FetchRaw() unexpected error: %v", err) 462 + return 463 + } 464 + 465 + if !strings.Contains(string(content), tt.wantContent) { 466 + t.Errorf("FetchRaw() content = %q, want content containing %q", string(content), tt.wantContent) 467 + } 468 + }) 469 + } 470 + } 471 + 472 + func TestFetcher_FetchRaw_URLValidation(t *testing.T) { 473 + fetcher := NewFetcher() 474 + 475 + tests := []struct { 476 + name string 477 + url string 478 + errContains string 479 + }{ 480 + { 481 + name: "empty URL", 482 + url: "", 483 + errContains: "empty README URL", 484 + }, 485 + { 486 + name: "invalid URL scheme", 487 + url: "ftp://example.com/README.md", 488 + errContains: "invalid URL scheme", 489 + }, 490 + { 491 + name: "file URL scheme", 492 + url: "file:///etc/passwd", 493 + errContains: "invalid URL scheme", 494 + }, 495 + } 496 + 497 + for _, tt := range tests { 498 + t.Run(tt.name, func(t *testing.T) { 499 + _, err := fetcher.FetchRaw(context.Background(), tt.url) 500 + if err == nil { 501 + t.Errorf("FetchRaw(%q) expected error, got nil", tt.url) 502 + return 503 + } 504 + if !strings.Contains(err.Error(), tt.errContains) { 505 + t.Errorf("FetchRaw(%q) error = %q, want error containing %q", tt.url, err.Error(), tt.errContains) 506 + } 507 + }) 508 + } 509 + } 510 + 511 + func TestFetcher_FetchAndRender(t *testing.T) { 512 + fetcher := NewFetcher() 513 + 514 + tests := []struct { 515 + name string 516 + handler http.HandlerFunc 517 + wantErr bool 518 + errContains string 519 + wantContain string 520 + }{ 521 + { 522 + name: "renders markdown to HTML", 523 + handler: func(w http.ResponseWriter, r *http.Request) { 524 + w.Header().Set("Content-Type", "text/plain") 525 + w.Write([]byte("# Hello World\n\nThis is **bold** text.")) 526 + }, 527 + wantErr: false, 528 + wantContain: "<strong>bold</strong>", 529 + }, 530 + { 531 + name: "rejects HTML content type", 532 + handler: func(w http.ResponseWriter, r *http.Request) { 533 + w.Header().Set("Content-Type", "text/html") 534 + w.Write([]byte("<html><body>Error</body></html>")) 535 + }, 536 + wantErr: true, 537 + errContains: "unsupported content type", 538 + }, 539 + { 540 + name: "rejects soft 404", 541 + handler: func(w http.ResponseWriter, r *http.Request) { 542 + w.Header().Set("Content-Type", "text/plain") 543 + w.Write([]byte("<!doctype html><html><body>Not Found</body></html>")) 544 + }, 545 + wantErr: true, 546 + errContains: "detected HTML content", 547 + }, 548 + } 549 + 550 + for _, tt := range tests { 551 + t.Run(tt.name, func(t *testing.T) { 552 + server := httptest.NewServer(tt.handler) 553 + defer server.Close() 554 + 555 + html, err := fetcher.FetchAndRender(context.Background(), server.URL) 556 + 557 + if tt.wantErr { 558 + if err == nil { 559 + t.Errorf("FetchAndRender() expected error containing %q, got nil", tt.errContains) 560 + return 561 + } 562 + if !strings.Contains(err.Error(), tt.errContains) { 563 + t.Errorf("FetchAndRender() error = %q, want error containing %q", err.Error(), tt.errContains) 564 + } 565 + return 566 + } 567 + 568 + if err != nil { 569 + t.Errorf("FetchAndRender() unexpected error: %v", err) 570 + return 571 + } 572 + 573 + if !strings.Contains(html, tt.wantContain) { 574 + t.Errorf("FetchAndRender() = %q, want HTML containing %q", html, tt.wantContain) 575 + } 576 + }) 577 + } 578 + }
+20
pkg/appview/routes/routes.go
··· 87 }, 88 ).ServeHTTP) 89 90 // API route for repository stats (public, read-only) 91 router.Get("/api/stats/{handle}/{repository}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 92 &uihandlers.GetStatsHandler{ ··· 172 Templates: deps.Templates, 173 Refresher: deps.Refresher, 174 RegistryURL: registryURL, 175 }).ServeHTTP) 176 177 r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{
··· 87 }, 88 ).ServeHTTP) 89 90 + // Legal pages (public) 91 + router.Get("/privacy", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 92 + &uihandlers.PrivacyPolicyHandler{ 93 + Templates: deps.Templates, 94 + RegistryURL: registryURL, 95 + }, 96 + ).ServeHTTP) 97 + 98 + router.Get("/terms", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 99 + &uihandlers.TermsOfServiceHandler{ 100 + Templates: deps.Templates, 101 + RegistryURL: registryURL, 102 + }, 103 + ).ServeHTTP) 104 + 105 // API route for repository stats (public, read-only) 106 router.Get("/api/stats/{handle}/{repository}", middleware.OptionalAuth(deps.SessionStore, deps.Database)( 107 &uihandlers.GetStatsHandler{ ··· 187 Templates: deps.Templates, 188 Refresher: deps.Refresher, 189 RegistryURL: registryURL, 190 + }).ServeHTTP) 191 + 192 + r.Get("/api/storage", (&uihandlers.StorageHandler{ 193 + Templates: deps.Templates, 194 + Refresher: deps.Refresher, 195 }).ServeHTTP) 196 197 r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{
+151
pkg/appview/static/css/style.css
··· 1569 margin-left: 0.5rem; 1570 } 1571 1572 .platform-badge { 1573 display: inline-flex; 1574 align-items: center; ··· 2457 background-color: rgba(13, 108, 191, 0.15); 2458 color: #0d6cbf; 2459 }
··· 1569 margin-left: 0.5rem; 1570 } 1571 1572 + /* Helm chart badge */ 1573 + .badge-helm { 1574 + display: inline-flex; 1575 + align-items: center; 1576 + gap: 0.25rem; 1577 + padding: 0.25rem 0.6rem; 1578 + font-size: 0.75rem; 1579 + font-weight: 600; 1580 + border-radius: 12px; 1581 + background: #0f1689; 1582 + color: #fff; 1583 + white-space: nowrap; 1584 + margin-left: 0.5rem; 1585 + } 1586 + 1587 + .badge-helm svg { 1588 + width: 12px; 1589 + height: 12px; 1590 + } 1591 + 1592 .platform-badge { 1593 display: inline-flex; 1594 align-items: center; ··· 2477 background-color: rgba(13, 108, 191, 0.15); 2478 color: #0d6cbf; 2479 } 2480 + 2481 + /* Legal Pages (Privacy Policy, Terms of Service) */ 2482 + .legal-page { 2483 + max-width: 800px; 2484 + margin: 0 auto; 2485 + padding: 2rem 1rem; 2486 + } 2487 + 2488 + .legal-page h1 { 2489 + font-size: 2rem; 2490 + margin-bottom: 0.5rem; 2491 + color: var(--fg); 2492 + } 2493 + 2494 + .legal-updated { 2495 + color: var(--secondary); 2496 + margin-bottom: 2rem; 2497 + } 2498 + 2499 + .legal-section { 2500 + margin: 2rem 0; 2501 + padding-bottom: 1.5rem; 2502 + border-bottom: 1px solid var(--border); 2503 + } 2504 + 2505 + .legal-section:last-child { 2506 + border-bottom: none; 2507 + } 2508 + 2509 + .legal-section h2 { 2510 + font-size: 1.5rem; 2511 + margin-bottom: 1rem; 2512 + color: var(--fg); 2513 + } 2514 + 2515 + .legal-section h3 { 2516 + font-size: 1.15rem; 2517 + margin: 1.5rem 0 0.75rem; 2518 + color: var(--fg); 2519 + } 2520 + 2521 + .legal-section p { 2522 + margin-bottom: 1rem; 2523 + line-height: 1.7; 2524 + } 2525 + 2526 + .legal-section ul, 2527 + .legal-section ol { 2528 + margin-bottom: 1rem; 2529 + padding-left: 2rem; 2530 + } 2531 + 2532 + .legal-section li { 2533 + margin-bottom: 0.5rem; 2534 + line-height: 1.6; 2535 + } 2536 + 2537 + .legal-section ul ul { 2538 + margin-top: 0.5rem; 2539 + margin-bottom: 0.5rem; 2540 + } 2541 + 2542 + .legal-section code { 2543 + background: var(--code-bg); 2544 + padding: 0.2rem 0.4rem; 2545 + border-radius: 3px; 2546 + font-family: "Monaco", "Menlo", monospace; 2547 + font-size: 0.9em; 2548 + } 2549 + 2550 + .legal-section a { 2551 + color: var(--primary); 2552 + text-decoration: underline; 2553 + } 2554 + 2555 + .legal-section a:hover { 2556 + color: var(--primary-dark); 2557 + } 2558 + 2559 + .legal-section table { 2560 + width: 100%; 2561 + border-collapse: collapse; 2562 + margin: 1rem 0; 2563 + } 2564 + 2565 + .legal-section table th, 2566 + .legal-section table td { 2567 + padding: 0.75rem 1rem; 2568 + border: 1px solid var(--border); 2569 + text-align: left; 2570 + } 2571 + 2572 + .legal-section table th { 2573 + background: var(--code-bg); 2574 + font-weight: 600; 2575 + } 2576 + 2577 + .legal-section table tr:nth-child(even) { 2578 + background: var(--hover-bg); 2579 + } 2580 + 2581 + .legal-disclaimer { 2582 + background: var(--code-bg); 2583 + padding: 1rem; 2584 + border-radius: 4px; 2585 + font-size: 0.95rem; 2586 + margin: 1rem 0; 2587 + } 2588 + 2589 + @media (max-width: 768px) { 2590 + .legal-page { 2591 + padding: 1rem 0.5rem; 2592 + } 2593 + 2594 + .legal-page h1 { 2595 + font-size: 1.5rem; 2596 + } 2597 + 2598 + .legal-section h2 { 2599 + font-size: 1.25rem; 2600 + } 2601 + 2602 + .legal-section table { 2603 + font-size: 0.85rem; 2604 + } 2605 + 2606 + .legal-section table th, 2607 + .legal-section table td { 2608 + padding: 0.5rem; 2609 + } 2610 + }
-6
pkg/appview/storage/context_test.go
··· 17 return m.holdDID, nil 18 } 19 20 - type mockHoldAuthorizer struct{} 21 - 22 - func (m *mockHoldAuthorizer) Authorize(holdDID, userDID, permission string) (bool, error) { 23 - return true, nil 24 - } 25 - 26 func TestRegistryContext_Fields(t *testing.T) { 27 // Create a sample RegistryContext 28 ctx := &RegistryContext{
··· 17 return m.holdDID, nil 18 } 19 20 func TestRegistryContext_Fields(t *testing.T) { 21 // Create a sample RegistryContext 22 ctx := &RegistryContext{
+62 -74
pkg/appview/storage/manifest_store.go
··· 76 // Notify hold about manifest pull (for stats tracking) 77 // Only count GET requests (actual downloads), not HEAD requests (existence checks) 78 // Check HTTP method from context (distribution library stores it as "http.request.method") 79 - if method, ok := ctx.Value("http.request.method").(string); ok && method == "GET" { 80 // Do this asynchronously to avoid blocking the response 81 if s.ctx.ServiceToken != "" && s.ctx.Handle != "" { 82 go func() { ··· 90 } 91 }() 92 } 93 } 94 95 // Parse the manifest based on media type 96 // For now, we'll return the raw bytes wrapped in a manifest object 97 // In a full implementation, you'd use distribution's manifest parsing 98 return &rawManifest{ 99 - mediaType: manifestRecord.MediaType, 100 payload: ociManifest, 101 }, nil 102 } ··· 122 manifestRecord, err := atproto.NewManifestRecord(s.ctx.Repository, dgst.String(), payload) 123 if err != nil { 124 return "", fmt.Errorf("failed to create manifest record: %w", err) 125 } 126 127 // Set the blob reference, hold DID, and hold endpoint ··· 212 213 // Notify hold about manifest push (for layer tracking, Bluesky posts, and stats) 214 // Do this asynchronously to avoid blocking the push 215 - if tag != "" && s.ctx.ServiceToken != "" && s.ctx.Handle != "" { 216 go func() { 217 defer func() { 218 if r := recover(); r != nil { ··· 313 serviceToken := s.ctx.ServiceToken 314 315 // Build notification request 316 notifyReq := map[string]any{ 317 - "repository": s.ctx.Repository, 318 - "userDid": s.ctx.DID, 319 - "userHandle": s.ctx.Handle, 320 - "operation": operation, 321 } 322 323 // For push operations, include full manifest data ··· 412 return nil 413 } 414 415 - // ensureRepoPage creates or updates a repo page record in the user's PDS if needed 416 // This syncs repository metadata from manifest annotations to the io.atcr.repo.page collection 417 - // Only creates a new record if one doesn't exist (doesn't overwrite user's custom content) 418 func (s *ManifestStore) ensureRepoPage(ctx context.Context, manifestRecord *atproto.ManifestRecord) { 419 - // Check if repo page already exists (don't overwrite user's custom content) 420 rkey := s.ctx.Repository 421 - _, err := s.ctx.ATProtoClient.GetRecord(ctx, atproto.RepoPageCollection, rkey) 422 - if err == nil { 423 - // Record already exists - don't overwrite 424 - slog.Debug("Repo page already exists, skipping creation", "did", s.ctx.DID, "repository", s.ctx.Repository) 425 - return 426 - } 427 428 - // Only continue if it's a "not found" error - other errors mean we should skip 429 - if !errors.Is(err, atproto.ErrRecordNotFound) { 430 slog.Warn("Failed to check for existing repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", err) 431 - return 432 } 433 434 // Get annotations (may be nil if image has no OCI labels) ··· 446 description = annotations["org.opencontainers.image.description"] 447 } 448 449 - // Try to fetch and upload icon from io.atcr.icon annotation 450 - var avatarRef *atproto.ATProtoBlobRef 451 if iconURL := annotations["io.atcr.icon"]; iconURL != "" { 452 - avatarRef = s.fetchAndUploadIcon(ctx, iconURL) 453 } 454 455 - // Create new repo page record with description and optional avatar 456 repoPage := atproto.NewRepoPageRecord(s.ctx.Repository, description, avatarRef) 457 458 - slog.Info("Creating repo page from manifest annotations", "did", s.ctx.DID, "repository", s.ctx.Repository, "descriptionLength", len(description), "hasAvatar", avatarRef != nil) 459 460 _, err = s.ctx.ATProtoClient.PutRecord(ctx, atproto.RepoPageCollection, rkey, repoPage) 461 if err != nil { 462 - slog.Warn("Failed to create repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", err) 463 return 464 } 465 466 - slog.Info("Repo page created successfully", "did", s.ctx.DID, "repository", s.ctx.Repository) 467 } 468 469 // fetchReadmeContent attempts to fetch README content from external sources 470 // Priority: io.atcr.readme annotation > derived from org.opencontainers.image.source 471 // Returns the raw markdown content, or empty string if not available 472 func (s *ManifestStore) fetchReadmeContent(ctx context.Context, annotations map[string]string) string { 473 if s.ctx.ReadmeFetcher == nil { 474 return "" ··· 480 481 // Priority 1: Direct README URL from io.atcr.readme annotation 482 if readmeURL := annotations["io.atcr.readme"]; readmeURL != "" { 483 - content, err := s.fetchRawReadme(fetchCtx, readmeURL) 484 if err != nil { 485 slog.Debug("Failed to fetch README from io.atcr.readme annotation", "url", readmeURL, "error", err) 486 - } else if content != "" { 487 slog.Info("Fetched README from io.atcr.readme annotation", "url", readmeURL, "length", len(content)) 488 - return content 489 } 490 } 491 ··· 498 continue 499 } 500 501 - content, err := s.fetchRawReadme(fetchCtx, readmeURL) 502 if err != nil { 503 // Only log non-404 errors (404 is expected when trying main vs master) 504 if !readme.Is404(err) { ··· 507 continue 508 } 509 510 - if content != "" { 511 slog.Info("Fetched README from source URL", "sourceURL", sourceURL, "branch", branch, "length", len(content)) 512 - return content 513 } 514 } 515 } 516 517 return "" 518 - } 519 - 520 - // fetchRawReadme fetches raw markdown content from a URL 521 - // Returns the raw markdown (not rendered HTML) for storage in the repo page record 522 - func (s *ManifestStore) fetchRawReadme(ctx context.Context, readmeURL string) (string, error) { 523 - // Use a simple HTTP client to fetch raw content 524 - // We want raw markdown, not rendered HTML (the Fetcher renders to HTML) 525 - req, err := http.NewRequestWithContext(ctx, "GET", readmeURL, nil) 526 - if err != nil { 527 - return "", fmt.Errorf("failed to create request: %w", err) 528 - } 529 - 530 - req.Header.Set("User-Agent", "ATCR-README-Fetcher/1.0") 531 - 532 - client := &http.Client{ 533 - Timeout: 10 * time.Second, 534 - CheckRedirect: func(req *http.Request, via []*http.Request) error { 535 - if len(via) >= 5 { 536 - return fmt.Errorf("too many redirects") 537 - } 538 - return nil 539 - }, 540 - } 541 - 542 - resp, err := client.Do(req) 543 - if err != nil { 544 - return "", fmt.Errorf("failed to fetch URL: %w", err) 545 - } 546 - defer resp.Body.Close() 547 - 548 - if resp.StatusCode != http.StatusOK { 549 - return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) 550 - } 551 - 552 - // Limit content size to 100KB (repo page description has 100KB limit in lexicon) 553 - limitedReader := io.LimitReader(resp.Body, 100*1024) 554 - content, err := io.ReadAll(limitedReader) 555 - if err != nil { 556 - return "", fmt.Errorf("failed to read response body: %w", err) 557 - } 558 - 559 - return string(content), nil 560 } 561 562 // fetchAndUploadIcon fetches an image from a URL and uploads it as a blob to the user's PDS
··· 76 // Notify hold about manifest pull (for stats tracking) 77 // Only count GET requests (actual downloads), not HEAD requests (existence checks) 78 // Check HTTP method from context (distribution library stores it as "http.request.method") 79 + if method, ok := ctx.Value(HTTPRequestMethod).(string); ok && method == "GET" { 80 // Do this asynchronously to avoid blocking the response 81 if s.ctx.ServiceToken != "" && s.ctx.Handle != "" { 82 go func() { ··· 90 } 91 }() 92 } 93 + } 94 + 95 + // Determine media type - prefer record field, fallback to blob mimeType for old records 96 + mediaType := manifestRecord.MediaType 97 + if mediaType == "" && manifestRecord.ManifestBlob != nil { 98 + mediaType = manifestRecord.ManifestBlob.MimeType 99 } 100 101 // Parse the manifest based on media type 102 // For now, we'll return the raw bytes wrapped in a manifest object 103 // In a full implementation, you'd use distribution's manifest parsing 104 return &rawManifest{ 105 + mediaType: mediaType, 106 payload: ociManifest, 107 }, nil 108 } ··· 128 manifestRecord, err := atproto.NewManifestRecord(s.ctx.Repository, dgst.String(), payload) 129 if err != nil { 130 return "", fmt.Errorf("failed to create manifest record: %w", err) 131 + } 132 + 133 + // OCI spec allows omitting mediaType from the manifest body (inferred from Content-Type header) 134 + // Helm charts typically omit it, so use the media type from the request if body is empty 135 + if manifestRecord.MediaType == "" && mediaType != "" { 136 + manifestRecord.MediaType = mediaType 137 } 138 139 // Set the blob reference, hold DID, and hold endpoint ··· 224 225 // Notify hold about manifest push (for layer tracking, Bluesky posts, and stats) 226 // Do this asynchronously to avoid blocking the push 227 + // Note: We notify even for tagless pushes (e.g., buildx platform manifests) to create layer records 228 + // Bluesky posts are only created for tagged pushes (handled by hold service) 229 + if s.ctx.ServiceToken != "" && s.ctx.Handle != "" { 230 go func() { 231 defer func() { 232 if r := recover(); r != nil { ··· 327 serviceToken := s.ctx.ServiceToken 328 329 // Build notification request 330 + // Note: userHandle is resolved from userDid on the hold side (cached, 24-hour TTL) 331 notifyReq := map[string]any{ 332 + "repository": s.ctx.Repository, 333 + "userDid": s.ctx.DID, 334 + "manifestDigest": manifestDigest, 335 + "operation": operation, 336 } 337 338 // For push operations, include full manifest data ··· 427 return nil 428 } 429 430 + // ensureRepoPage creates or updates a repo page record in the user's PDS 431 // This syncs repository metadata from manifest annotations to the io.atcr.repo.page collection 432 + // Always updates the description on push (since users can't edit it via appview yet) 433 + // Preserves user's avatar if they've set one via the appview 434 func (s *ManifestStore) ensureRepoPage(ctx context.Context, manifestRecord *atproto.ManifestRecord) { 435 rkey := s.ctx.Repository 436 437 + // Check for existing record to preserve user's avatar 438 + var existingAvatarRef *atproto.ATProtoBlobRef 439 + var existingRecord *atproto.RepoPageRecord 440 + record, err := s.ctx.ATProtoClient.GetRecord(ctx, atproto.RepoPageCollection, rkey) 441 + if err == nil && record != nil { 442 + // Unmarshal the Value to get the RepoPageRecord 443 + var repoPage atproto.RepoPageRecord 444 + if unmarshalErr := json.Unmarshal(record.Value, &repoPage); unmarshalErr == nil { 445 + existingRecord = &repoPage 446 + existingAvatarRef = repoPage.Avatar 447 + slog.Debug("Found existing repo page, will update", "did", s.ctx.DID, "repository", s.ctx.Repository, "hasExistingAvatar", existingAvatarRef != nil) 448 + } else { 449 + slog.Warn("Failed to unmarshal existing repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", unmarshalErr) 450 + } 451 + } else if err != nil && !errors.Is(err, atproto.ErrRecordNotFound) { 452 + // Unexpected error - log and continue (will create new record) 453 slog.Warn("Failed to check for existing repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", err) 454 } 455 456 // Get annotations (may be nil if image has no OCI labels) ··· 468 description = annotations["org.opencontainers.image.description"] 469 } 470 471 + // Determine avatar: prefer new icon from annotations, otherwise keep existing 472 + avatarRef := existingAvatarRef 473 if iconURL := annotations["io.atcr.icon"]; iconURL != "" { 474 + if newAvatar := s.fetchAndUploadIcon(ctx, iconURL); newAvatar != nil { 475 + avatarRef = newAvatar 476 + } 477 } 478 479 + // Create/update repo page record with description and avatar 480 repoPage := atproto.NewRepoPageRecord(s.ctx.Repository, description, avatarRef) 481 482 + isUpdate := existingRecord != nil 483 + action := "Creating" 484 + if isUpdate { 485 + action = "Updating" 486 + } 487 + slog.Info(action+" repo page from manifest annotations", "did", s.ctx.DID, "repository", s.ctx.Repository, "descriptionLength", len(description), "hasAvatar", avatarRef != nil) 488 489 _, err = s.ctx.ATProtoClient.PutRecord(ctx, atproto.RepoPageCollection, rkey, repoPage) 490 if err != nil { 491 + slog.Warn("Failed to "+strings.ToLower(action)+" repo page", "did", s.ctx.DID, "repository", s.ctx.Repository, "error", err) 492 return 493 } 494 495 + slog.Info("Repo page "+strings.ToLower(action)+"d successfully", "did", s.ctx.DID, "repository", s.ctx.Repository) 496 } 497 498 // fetchReadmeContent attempts to fetch README content from external sources 499 // Priority: io.atcr.readme annotation > derived from org.opencontainers.image.source 500 // Returns the raw markdown content, or empty string if not available 501 + // Uses the shared readme.Fetcher which validates Content-Type and rejects HTML content 502 func (s *ManifestStore) fetchReadmeContent(ctx context.Context, annotations map[string]string) string { 503 if s.ctx.ReadmeFetcher == nil { 504 return "" ··· 510 511 // Priority 1: Direct README URL from io.atcr.readme annotation 512 if readmeURL := annotations["io.atcr.readme"]; readmeURL != "" { 513 + content, err := s.ctx.ReadmeFetcher.FetchRaw(fetchCtx, readmeURL) 514 if err != nil { 515 slog.Debug("Failed to fetch README from io.atcr.readme annotation", "url", readmeURL, "error", err) 516 + } else if len(content) > 0 { 517 slog.Info("Fetched README from io.atcr.readme annotation", "url", readmeURL, "length", len(content)) 518 + return string(content) 519 } 520 } 521 ··· 528 continue 529 } 530 531 + content, err := s.ctx.ReadmeFetcher.FetchRaw(fetchCtx, readmeURL) 532 if err != nil { 533 // Only log non-404 errors (404 is expected when trying main vs master) 534 if !readme.Is404(err) { ··· 537 continue 538 } 539 540 + if len(content) > 0 { 541 slog.Info("Fetched README from source URL", "sourceURL", sourceURL, "branch", branch, "length", len(content)) 542 + return string(content) 543 } 544 } 545 } 546 547 return "" 548 } 549 550 // fetchAndUploadIcon fetches an image from a URL and uploads it as a blob to the user's PDS
+3 -5
pkg/appview/storage/profile_test.go
··· 340 341 // Make 5 concurrent GetProfile calls 342 var wg sync.WaitGroup 343 - for i := 0; i < 5; i++ { 344 - wg.Add(1) 345 - go func() { 346 - defer wg.Done() 347 _, err := GetProfile(context.Background(), client) 348 if err != nil { 349 t.Errorf("GetProfile() error = %v", err) 350 } 351 - }() 352 } 353 354 wg.Wait()
··· 340 341 // Make 5 concurrent GetProfile calls 342 var wg sync.WaitGroup 343 + for range 5 { 344 + wg.Go(func() { 345 _, err := GetProfile(context.Background(), client) 346 if err != nil { 347 t.Errorf("GetProfile() error = %v", err) 348 } 349 + }) 350 } 351 352 wg.Wait()
+6 -5
pkg/appview/storage/proxy_blob_store.go
··· 552 } 553 554 // abortMultipartUpload aborts a multipart upload via XRPC abortUpload endpoint 555 - func (p *ProxyBlobStore) abortMultipartUpload(ctx context.Context, digest, uploadID string) error { 556 reqBody := map[string]any{ 557 "uploadId": uploadID, 558 } ··· 760 slog.Debug("Flushing final buffer", "component", "proxy_blob_store/Commit", "bytes", w.buffer.Len()) 761 if err := w.flushPart(); err != nil { 762 // Try to abort multipart on error 763 - tempDigest := fmt.Sprintf("uploads/temp-%s", w.id) 764 - w.store.abortMultipartUpload(ctx, tempDigest, w.uploadID) 765 return distribution.Descriptor{}, fmt.Errorf("failed to flush final part: %w", err) 766 } 767 } ··· 794 globalUploadsMu.Unlock() 795 796 // Abort multipart upload 797 - tempDigest := fmt.Sprintf("uploads/temp-%s", w.id) 798 - if err := w.store.abortMultipartUpload(ctx, tempDigest, w.uploadID); err != nil { 799 slog.Warn("Failed to abort multipart upload", "component", "proxy_blob_store/Cancel", "error", err) 800 // Continue anyway - we want to mark upload as cancelled 801 }
··· 552 } 553 554 // abortMultipartUpload aborts a multipart upload via XRPC abortUpload endpoint 555 + func (p *ProxyBlobStore) abortMultipartUpload(ctx context.Context, uploadID string) error { 556 reqBody := map[string]any{ 557 "uploadId": uploadID, 558 } ··· 760 slog.Debug("Flushing final buffer", "component", "proxy_blob_store/Commit", "bytes", w.buffer.Len()) 761 if err := w.flushPart(); err != nil { 762 // Try to abort multipart on error 763 + if err := w.store.abortMultipartUpload(ctx, w.uploadID); err != nil { 764 + slog.Warn("Failed to abort multipart upload", "component", "proxy_blob_store/Cancel", "error", err) 765 + // Continue anyway - we want to mark upload as cancelled 766 + } 767 return distribution.Descriptor{}, fmt.Errorf("failed to flush final part: %w", err) 768 } 769 } ··· 796 globalUploadsMu.Unlock() 797 798 // Abort multipart upload 799 + if err := w.store.abortMultipartUpload(ctx, w.uploadID); err != nil { 800 slog.Warn("Failed to abort multipart upload", "component", "proxy_blob_store/Cancel", "error", err) 801 // Continue anyway - we want to mark upload as cancelled 802 }
+1 -1
pkg/appview/storage/proxy_blob_store_test.go
··· 563 { 564 name: "abortMultipartUpload", 565 testFunc: func(store *ProxyBlobStore) error { 566 - return store.abortMultipartUpload(context.Background(), "sha256:test", "upload-123") 567 }, 568 expectedPath: atproto.HoldAbortUpload, 569 },
··· 563 { 564 name: "abortMultipartUpload", 565 testFunc: func(store *ProxyBlobStore) error { 566 + return store.abortMultipartUpload(context.Background(), "upload-123") 567 }, 568 expectedPath: atproto.HoldAbortUpload, 569 },
+46 -45
pkg/appview/storage/routing_repository.go
··· 7 import ( 8 "context" 9 "log/slog" 10 11 "github.com/distribution/distribution/v3" 12 ) 13 14 // RoutingRepository routes manifests to ATProto and blobs to external hold service 15 // The registry (AppView) is stateless and NEVER stores blobs locally 16 // NOTE: A fresh instance is created per-request (see middleware/registry.go) 17 - // so no mutex is needed - each request has its own instance 18 type RoutingRepository struct { 19 distribution.Repository 20 - Ctx *RegistryContext // All context and services (exported for token updates) 21 - manifestStore *ManifestStore // Manifest store instance (lazy-initialized) 22 - blobStore *ProxyBlobStore // Blob store instance (lazy-initialized) 23 } 24 25 // NewRoutingRepository creates a new routing repository ··· 32 33 // Manifests returns the ATProto-backed manifest service 34 func (r *RoutingRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { 35 - // Lazy-initialize manifest store (no mutex needed - one instance per request) 36 - if r.manifestStore == nil { 37 // Ensure blob store is created first (needed for label extraction during push) 38 blobStore := r.Blobs(ctx) 39 r.manifestStore = NewManifestStore(r.Ctx, blobStore) 40 - } 41 return r.manifestStore, nil 42 } 43 44 // Blobs returns a proxy blob store that routes to external hold service 45 // The registry (AppView) NEVER stores blobs locally - all blobs go through hold service 46 func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore { 47 - // Return cached blob store if available (no mutex needed - one instance per request) 48 - if r.blobStore != nil { 49 - slog.Debug("Returning cached blob store", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository) 50 - return r.blobStore 51 - } 52 53 - // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation 54 - // Pull operations use the historical hold DID from the database (blobs are where they were pushed) 55 - // Push operations use the discovery-based hold DID from user's profile/default 56 - // This allows users to change their default hold and have new pushes go there 57 - isPull := false 58 - if method, ok := ctx.Value("http.request.method").(string); ok { 59 - isPull = method == "GET" || method == "HEAD" 60 - } 61 62 - holdDID := r.Ctx.HoldDID // Default to discovery-based DID 63 - holdSource := "discovery" 64 65 - // Only query database for pull operations 66 - if isPull && r.Ctx.Database != nil { 67 - // Query database for the latest manifest's hold DID 68 - if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" { 69 - // Use hold DID from database (pull case - use historical reference) 70 - holdDID = dbHoldDID 71 - holdSource = "database" 72 - slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID) 73 - } else if err != nil { 74 - // Log error but don't fail - fall back to discovery-based DID 75 - slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err) 76 } 77 - // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID 78 - } 79 - 80 - if holdDID == "" { 81 - // This should never happen if middleware is configured correctly 82 - panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware") 83 - } 84 85 - slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource) 86 87 - // Update context with the correct hold DID (may be from database or discovered) 88 - r.Ctx.HoldDID = holdDID 89 90 - // Create and cache proxy blob store 91 - r.blobStore = NewProxyBlobStore(r.Ctx) 92 return r.blobStore 93 } 94
··· 7 import ( 8 "context" 9 "log/slog" 10 + "sync" 11 12 "github.com/distribution/distribution/v3" 13 ) 14 15 + type contextKey string 16 + 17 + const HTTPRequestMethod contextKey = "http.request.method" 18 + 19 // RoutingRepository routes manifests to ATProto and blobs to external hold service 20 // The registry (AppView) is stateless and NEVER stores blobs locally 21 // NOTE: A fresh instance is created per-request (see middleware/registry.go) 22 type RoutingRepository struct { 23 distribution.Repository 24 + Ctx *RegistryContext // All context and services (exported for token updates) 25 + manifestStore *ManifestStore // Manifest store instance (lazy-initialized) 26 + manifestStoreOnce sync.Once // Ensures thread-safe lazy initialization 27 + blobStore *ProxyBlobStore // Blob store instance (lazy-initialized) 28 + blobStoreOnce sync.Once // Ensures thread-safe lazy initialization 29 } 30 31 // NewRoutingRepository creates a new routing repository ··· 38 39 // Manifests returns the ATProto-backed manifest service 40 func (r *RoutingRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { 41 + r.manifestStoreOnce.Do(func() { 42 // Ensure blob store is created first (needed for label extraction during push) 43 blobStore := r.Blobs(ctx) 44 r.manifestStore = NewManifestStore(r.Ctx, blobStore) 45 + }) 46 return r.manifestStore, nil 47 } 48 49 // Blobs returns a proxy blob store that routes to external hold service 50 // The registry (AppView) NEVER stores blobs locally - all blobs go through hold service 51 func (r *RoutingRepository) Blobs(ctx context.Context) distribution.BlobStore { 52 + r.blobStoreOnce.Do(func() { 53 + // Determine if this is a pull (GET/HEAD) or push (PUT/POST/etc) operation 54 + // Pull operations use the historical hold DID from the database (blobs are where they were pushed) 55 + // Push operations use the discovery-based hold DID from user's profile/default 56 + // This allows users to change their default hold and have new pushes go there 57 + isPull := false 58 + if method, ok := ctx.Value(HTTPRequestMethod).(string); ok { 59 + isPull = method == "GET" || method == "HEAD" 60 + } 61 62 + holdDID := r.Ctx.HoldDID // Default to discovery-based DID 63 + holdSource := "discovery" 64 65 + // Only query database for pull operations 66 + if isPull && r.Ctx.Database != nil { 67 + // Query database for the latest manifest's hold DID 68 + if dbHoldDID, err := r.Ctx.Database.GetLatestHoldDIDForRepo(r.Ctx.DID, r.Ctx.Repository); err == nil && dbHoldDID != "" { 69 + // Use hold DID from database (pull case - use historical reference) 70 + holdDID = dbHoldDID 71 + holdSource = "database" 72 + slog.Debug("Using hold from database manifest (pull)", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", dbHoldDID) 73 + } else if err != nil { 74 + // Log error but don't fail - fall back to discovery-based DID 75 + slog.Warn("Failed to query database for hold DID", "component", "storage/blobs", "error", err) 76 + } 77 + // If dbHoldDID is empty (no manifests yet), fall through to use discovery-based DID 78 + } 79 80 + if holdDID == "" { 81 + // This should never happen if middleware is configured correctly 82 + panic("hold DID not set in RegistryContext - ensure default_hold_did is configured in middleware") 83 } 84 85 + slog.Debug("Using hold DID for blobs", "component", "storage/blobs", "did", r.Ctx.DID, "repo", r.Ctx.Repository, "hold", holdDID, "source", holdSource) 86 87 + // Update context with the correct hold DID (may be from database or discovered) 88 + r.Ctx.HoldDID = holdDID 89 90 + // Create and cache proxy blob store 91 + r.blobStore = NewProxyBlobStore(r.Ctx) 92 + }) 93 return r.blobStore 94 } 95
+11 -15
pkg/appview/storage/routing_repository_test.go
··· 126 } 127 repo := NewRoutingRepository(nil, ctx) 128 129 - pullCtx := context.WithValue(context.Background(), "http.request.method", method) 130 blobStore := repo.Blobs(pullCtx) 131 132 assert.NotNil(t, blobStore) ··· 164 repo := NewRoutingRepository(nil, ctx) 165 166 // Create context with push method 167 - pushCtx := context.WithValue(context.Background(), "http.request.method", tc.method) 168 blobStore := repo.Blobs(pushCtx) 169 170 assert.NotNil(t, blobStore) ··· 318 319 // Concurrent access to Manifests() 320 for i := 0; i < numGoroutines; i++ { 321 - wg.Add(1) 322 - go func(index int) { 323 - defer wg.Done() 324 store, err := repo.Manifests(context.Background()) 325 require.NoError(t, err) 326 - manifestStores[index] = store 327 - }(i) 328 } 329 330 wg.Wait() 331 332 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 333 - for i := 0; i < numGoroutines; i++ { 334 assert.NotNil(t, manifestStores[i], "manifest store should not be nil") 335 } 336 ··· 341 342 // Concurrent access to Blobs() 343 for i := 0; i < numGoroutines; i++ { 344 - wg.Add(1) 345 - go func(index int) { 346 - defer wg.Done() 347 - blobStores[index] = repo.Blobs(context.Background()) 348 - }(i) 349 } 350 351 wg.Wait() 352 353 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 354 - for i := 0; i < numGoroutines; i++ { 355 assert.NotNil(t, blobStores[i], "blob store should not be nil") 356 } 357 ··· 376 repo := NewRoutingRepository(nil, ctx) 377 378 // For pull (GET), database should take priority 379 - pullCtx := context.WithValue(context.Background(), "http.request.method", "GET") 380 blobStore := repo.Blobs(pullCtx) 381 382 assert.NotNil(t, blobStore)
··· 126 } 127 repo := NewRoutingRepository(nil, ctx) 128 129 + pullCtx := context.WithValue(context.Background(), HTTPRequestMethod, method) 130 blobStore := repo.Blobs(pullCtx) 131 132 assert.NotNil(t, blobStore) ··· 164 repo := NewRoutingRepository(nil, ctx) 165 166 // Create context with push method 167 + pushCtx := context.WithValue(context.Background(), HTTPRequestMethod, tc.method) 168 blobStore := repo.Blobs(pushCtx) 169 170 assert.NotNil(t, blobStore) ··· 318 319 // Concurrent access to Manifests() 320 for i := 0; i < numGoroutines; i++ { 321 + wg.Go(func() { 322 store, err := repo.Manifests(context.Background()) 323 require.NoError(t, err) 324 + manifestStores[i] = store 325 + }) 326 } 327 328 wg.Wait() 329 330 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 331 + for i := range numGoroutines { 332 assert.NotNil(t, manifestStores[i], "manifest store should not be nil") 333 } 334 ··· 339 340 // Concurrent access to Blobs() 341 for i := 0; i < numGoroutines; i++ { 342 + wg.Go(func() { 343 + blobStores[i] = repo.Blobs(context.Background()) 344 + }) 345 } 346 347 wg.Wait() 348 349 // Verify all stores are non-nil (due to race conditions, they may not all be the same instance) 350 + for i := range numGoroutines { 351 assert.NotNil(t, blobStores[i], "blob store should not be nil") 352 } 353 ··· 372 repo := NewRoutingRepository(nil, ctx) 373 374 // For pull (GET), database should take priority 375 + pullCtx := context.WithValue(context.Background(), HTTPRequestMethod, "GET") 376 blobStore := repo.Blobs(pullCtx) 377 378 assert.NotNil(t, blobStore)
+266
pkg/appview/templates/pages/privacy.html
···
··· 1 + {{ define "privacy" }} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <title>Privacy Policy - ATCR</title> 6 + {{ template "head" . }} 7 + </head> 8 + <body> 9 + {{ template "nav" . }} 10 + 11 + <main class="container"> 12 + <div class="legal-page"> 13 + <h1>Privacy Policy - AT Container Registry (atcr.io)</h1> 14 + <p class="legal-updated"><em>Last updated: January 2025</em></p> 15 + 16 + <div class="legal-section"> 17 + <h2>Data We Collect and Store</h2> 18 + 19 + <h3>Data Stored on Your PDS (Controlled by You)</h3> 20 + <p>When you use AT Container Registry, records are written to your Personal Data Server (PDS) under the <code>io.atcr.*</code> namespace. This data is stored on infrastructure you or your PDS hosting provider controls. We do not control this data, and its retention and deletion is governed by your PDS provider's policies.</p> 21 + 22 + <h3>Data Stored on Our Infrastructure</h3> 23 + 24 + <p><strong>Layer Records:</strong> We maintain records on our own PDS that reference container image layers you publish. These records are public and link your AT Protocol identity (DID) to content-addressed SHA identifiers.</p> 25 + 26 + <p><strong>OCI Blobs:</strong> Container image layers are stored in our object storage (S3). These blobs are content-addressed and deduplicatedโ€”meaning identical layers uploaded by different users are stored only once.</p> 27 + 28 + <p><strong>Authentication Data:</strong></p> 29 + <ul> 30 + <li>OAuth tokens obtained during sign-in</li> 31 + <li>Web UI session tokens</li> 32 + <li>Docker credential helper device tokens, including: 33 + <ul> 34 + <li>IP address</li> 35 + <li>Device name</li> 36 + <li>Token creation and last-used timestamps</li> 37 + </ul> 38 + </li> 39 + </ul> 40 + 41 + <p><strong>Cached PDS Data:</strong> We may cache data from your PDS in our database to improve performance and reduce load on your PDS. This cached data mirrors public information already stored on your PDS.</p> 42 + 43 + <p><strong>Server Logs:</strong> Our logs may include your handle, DID, IP address, timestamps, and actions performed. Logs are currently ephemeral but may be retained in the future for security and debugging purposes.</p> 44 + </div> 45 + 46 + <div class="legal-section"> 47 + <h2>Data Sharing and Deduplication</h2> 48 + 49 + <p>OCI container images use content-addressable storage. When you push an image layer, it is identified by its cryptographic hash (SHA256). If another user pushes an identical layer, both users reference the same underlying blob. This is standard practice for container registries and enables efficient storage and distribution.</p> 50 + 51 + <p><strong>What this means for your data:</strong></p> 52 + <ul> 53 + <li>Layer content is not uniquely "yours" if other users have pushed identical content</li> 54 + <li>Public SHA references may be associated with your AT Protocol identity</li> 55 + <li>Deleting your records does not delete blob data that other users also reference</li> 56 + </ul> 57 + </div> 58 + 59 + <div class="legal-section"> 60 + <h2>Your Rights Under GDPR</h2> 61 + 62 + <p>If you are located in the European Economic Area (EEA), you have the following rights:</p> 63 + 64 + <h3>Right to Access</h3> 65 + <p>You may request a copy of all personal data we store about you. This includes:</p> 66 + <ul> 67 + <li>Layer records associated with your DID on our PDS</li> 68 + <li>Server logs containing your handle, DID, IP address, or actions (if retained)</li> 69 + <li>OAuth tokens, web UI sessions, and device tokens</li> 70 + <li>Cached PDS data</li> 71 + <li>List of registered devices (credential helper)</li> 72 + </ul> 73 + <p class="note">Note: Data stored on your own PDS is already under your control and accessible to you directly.</p> 74 + 75 + <h3>Right to Erasure ("Right to be Forgotten")</h3> 76 + <p>You may request deletion of your data via the account settings page. Due to our technical architecture, deletion works as follows:</p> 77 + 78 + <p><strong>Immediately deleted:</strong></p> 79 + <ul> 80 + <li>Layer records on our PDS that reference your DID</li> 81 + <li>OAuth tokens, web UI sessions, and device tokens</li> 82 + <li>Cached PDS data</li> 83 + <li>Server logs containing your identifiers (deleted or anonymized, if retained)</li> 84 + </ul> 85 + 86 + <p><strong>Deleted within 30 days:</strong></p> 87 + <ul> 88 + <li>OCI blobs in our object storage that are no longer referenced by any user after your records are removed (via our orphan blob pruning process)</li> 89 + </ul> 90 + 91 + <p><strong>Cannot be deleted by us:</strong></p> 92 + <ul> 93 + <li>Records stored on your own PDS (you control these, or your PDS provider does)</li> 94 + <li>Blob data that is also referenced by other users (deduplicated content)</li> 95 + </ul> 96 + 97 + <p><strong>Optional: Delete AT Protocol Records</strong></p> 98 + <p>When deleting your account, you may optionally authorize us to delete <code>io.atcr.*</code> records from your PDS. This requires an active OAuth session and is optional because:</p> 99 + <ul> 100 + <li>Your PDS is controlled by you or your hosting provider, not us</li> 101 + <li>You may delete these records yourself at any time</li> 102 + <li>We have no ongoing obligation to manage data on infrastructure we do not control</li> 103 + </ul> 104 + 105 + <h3>Right to Rectification</h3> 106 + <p>You may update your data through normal use of the service. Data stored on your PDS is under your direct control.</p> 107 + 108 + <h3>Right to Data Portability</h3> 109 + <p>AT Protocol is designed for data portability. Your records are stored in an open, documented format on your PDS and can be exported or migrated at any time.</p> 110 + 111 + <h3>Right to Object / Restrict Processing</h3> 112 + <p>You may revoke our OAuth access at any time through your PDS provider's settings. This will prevent us from reading or writing records to your PDS.</p> 113 + </div> 114 + 115 + <div class="legal-section"> 116 + <h2>Your Rights Under CCPA</h2> 117 + 118 + <p>If you are a California resident, you have the following rights under the California Consumer Privacy Act:</p> 119 + 120 + <h3>Right to Know</h3> 121 + <p>You may request disclosure of:</p> 122 + <ul> 123 + <li>The categories of personal information we collect</li> 124 + <li>The purposes for which we use your personal information</li> 125 + <li>The categories of third parties with whom we share your personal information</li> 126 + </ul> 127 + 128 + <h3>Right to Delete</h3> 129 + <p>You may delete your personal information via the account settings page, subject to the same technical limitations described in the GDPR section above. For data not accessible through self-service, we will respond to requests within 45 days, except where retention is necessary for:</p> 130 + <ul> 131 + <li>Completing the transaction for which the data was collected</li> 132 + <li>Security and fraud prevention</li> 133 + <li>Legal compliance</li> 134 + </ul> 135 + 136 + <h3>Right to Non-Discrimination</h3> 137 + <p>We will not discriminate against you for exercising your CCPA rights.</p> 138 + 139 + <h3>Categories of Personal Information Collected</h3> 140 + <table> 141 + <thead> 142 + <tr> 143 + <th>Category</th> 144 + <th>Examples</th> 145 + <th>Collected</th> 146 + </tr> 147 + </thead> 148 + <tbody> 149 + <tr> 150 + <td>Identifiers</td> 151 + <td>DID, handle, IP address, device name</td> 152 + <td>Yes</td> 153 + </tr> 154 + <tr> 155 + <td>Internet activity</td> 156 + <td>Access logs, usage data, actions performed</td> 157 + <td>Yes</td> 158 + </tr> 159 + <tr> 160 + <td>Geolocation</td> 161 + <td>Approximate location via IP</td> 162 + <td>Yes</td> 163 + </tr> 164 + </tbody> 165 + </table> 166 + 167 + <p>We do not sell or share your personal information for cross-context behavioral advertising.</p> 168 + </div> 169 + 170 + <div class="legal-section"> 171 + <h2>Data Retention</h2> 172 + 173 + <table> 174 + <thead> 175 + <tr> 176 + <th>Data Type</th> 177 + <th>Retention Period</th> 178 + </tr> 179 + </thead> 180 + <tbody> 181 + <tr> 182 + <td>OAuth tokens</td> 183 + <td>Until revoked or logout</td> 184 + </tr> 185 + <tr> 186 + <td>Web UI session tokens</td> 187 + <td>Until logout or expiration</td> 188 + </tr> 189 + <tr> 190 + <td>Device tokens (credential helper)</td> 191 + <td>Until revoked by user</td> 192 + </tr> 193 + <tr> 194 + <td>Cached PDS data</td> 195 + <td>Refreshed periodically; deleted on account deletion</td> 196 + </tr> 197 + <tr> 198 + <td>Server logs</td> 199 + <td>Currently ephemeral; this policy will be updated if log retention is implemented</td> 200 + </tr> 201 + <tr> 202 + <td>Layer records (our PDS)</td> 203 + <td>Until you request deletion</td> 204 + </tr> 205 + <tr> 206 + <td>OCI blobs</td> 207 + <td>Until no longer referenced (pruned monthly)</td> 208 + </tr> 209 + </tbody> 210 + </table> 211 + </div> 212 + 213 + <div class="legal-section"> 214 + <h2>Important Notes on AT Protocol Architecture</h2> 215 + 216 + <p>AT Container Registry is built on the AT Protocol, which has a unique data architecture:</p> 217 + 218 + <ol> 219 + <li><strong>You control your data.</strong> Most data associated with your use of AT Container Registry is stored on your Personal Data Server (PDS), which you or your chosen provider controls.</li> 220 + <li><strong>Public by design.</strong> AT Protocol data is designed to be public and distributed. Records you create, including container image references, are publicly visible and may be replicated across the network.</li> 221 + <li><strong>Content-addressed storage.</strong> OCI blobs are identified by their cryptographic hash. This means blob data is inherently pseudonymousโ€”it cannot be attributed to you without the corresponding records that reference it.</li> 222 + <li><strong>Deletion limitations.</strong> Because AT Protocol is distributed, we cannot guarantee that copies of public records have not been made by other participants in the network. We can only delete data on infrastructure we control.</li> 223 + </ol> 224 + </div> 225 + 226 + <div class="legal-section"> 227 + <h2>How to Exercise Your Rights</h2> 228 + 229 + <h3>Self-Service (via Settings)</h3> 230 + <p>Most data management can be done directly through your account settings at atcr.io:</p> 231 + <ul> 232 + <li><strong>Delete your data:</strong> Use the "Delete Account" button in settings. This will remove your layer records, cached data, and authentication tokens. You may also choose to have us delete <code>io.atcr.*</code> records from your PDS (requires active OAuth session).</li> 233 + <li><strong>Revoke device tokens:</strong> Manage and revoke credential helper devices in settings.</li> 234 + <li><strong>Update your data:</strong> Corrections happen through normal use of the service.</li> 235 + </ul> 236 + 237 + <h3>Contact Us</h3> 238 + <p>For requests we cannot fulfill through self-service, such as:</p> 239 + <ul> 240 + <li>Copies of server logs containing your data</li> 241 + <li>Database records not exposed in the UI</li> 242 + <li>Questions about this policy</li> 243 + </ul> 244 + 245 + <p><strong>Email:</strong> <a href="mailto:privacy@atcr.io">privacy@atcr.io</a></p> 246 + 247 + <p>Please include your AT Protocol DID or handle so we can verify your identity.</p> 248 + 249 + <p>We will respond to requests within 30 days (GDPR) or 45 days (CCPA).</p> 250 + </div> 251 + 252 + <div class="legal-section"> 253 + <h2>Contact</h2> 254 + 255 + <p>For questions about this privacy policy or to exercise your data rights, contact:</p> 256 + 257 + <p><strong>Email:</strong> <a href="mailto:privacy@atcr.io">privacy@atcr.io</a></p> 258 + <p><strong>Website:</strong> <a href="https://atcr.io">https://atcr.io</a></p> 259 + </div> 260 + </div> 261 + </main> 262 + 263 + <div id="modal"></div> 264 + </body> 265 + </html> 266 + {{ end }}
+4 -2
pkg/appview/templates/pages/repository.html
··· 146 <div class="tag-item-header"> 147 <div> 148 <span class="tag-name-large">{{ .Tag.Tag }}</span> 149 - {{ if .IsMultiArch }} 150 <span class="badge-multi">Multi-arch</span> 151 {{ end }} 152 {{ if .HasAttestations }} ··· 183 {{ end }} 184 </div> 185 </div> 186 - {{ if eq $.ArtifactType "helm-chart" }} 187 {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " .Tag.Tag) }} 188 {{ else }} 189 {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }}
··· 146 <div class="tag-item-header"> 147 <div> 148 <span class="tag-name-large">{{ .Tag.Tag }}</span> 149 + {{ if eq .ArtifactType "helm-chart" }} 150 + <span class="badge-helm"><i data-lucide="anchor"></i> Helm</span> 151 + {{ else if .IsMultiArch }} 152 <span class="badge-multi">Multi-arch</span> 153 {{ end }} 154 {{ if .HasAttestations }} ··· 185 {{ end }} 186 </div> 187 </div> 188 + {{ if eq .ArtifactType "helm-chart" }} 189 {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " .Tag.Tag) }} 190 {{ else }} 191 {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" .Tag.Tag) }}
+124
pkg/appview/templates/pages/settings.html
··· 29 </div> 30 </section> 31 32 <!-- Default Hold Section --> 33 <section class="settings-section"> 34 <h2>Default Hold</h2> ··· 200 </script> 201 202 <style> 203 /* Devices Section Styles */ 204 .devices-section .setup-instructions { 205 margin: 1rem 0;
··· 29 </div> 30 </section> 31 32 + <!-- Storage Usage Section --> 33 + <section class="settings-section storage-section"> 34 + <h2>Stowage</h2> 35 + <p>Estimated storage usage on your default hold.</p> 36 + <div id="storage-stats" hx-get="/api/storage" hx-trigger="load" hx-swap="innerHTML"> 37 + <p><i data-lucide="loader-2" class="spin"></i> Loading...</p> 38 + </div> 39 + </section> 40 + 41 <!-- Default Hold Section --> 42 <section class="settings-section"> 43 <h2>Default Hold</h2> ··· 209 </script> 210 211 <style> 212 + /* Storage Section Styles */ 213 + .storage-section .storage-stats { 214 + background: var(--code-bg); 215 + padding: 1rem; 216 + border-radius: 4px; 217 + margin-top: 0.5rem; 218 + } 219 + .storage-section .stat-row { 220 + display: flex; 221 + justify-content: space-between; 222 + padding: 0.5rem 0; 223 + border-bottom: 1px solid var(--border); 224 + } 225 + .storage-section .stat-row:last-child { 226 + border-bottom: none; 227 + } 228 + .storage-section .stat-label { 229 + color: var(--fg-muted); 230 + } 231 + .storage-section .stat-value { 232 + font-weight: bold; 233 + font-family: monospace; 234 + } 235 + .storage-section .storage-error, 236 + .storage-section .storage-info { 237 + padding: 1rem; 238 + border-radius: 4px; 239 + margin-top: 0.5rem; 240 + display: flex; 241 + align-items: center; 242 + gap: 0.5rem; 243 + } 244 + .storage-section .storage-error { 245 + background: var(--error-bg, #fef2f2); 246 + color: var(--error, #dc2626); 247 + border: 1px solid var(--error, #dc2626); 248 + } 249 + .storage-section .storage-info { 250 + background: var(--info-bg, #eff6ff); 251 + color: var(--info, #2563eb); 252 + border: 1px solid var(--info, #2563eb); 253 + } 254 + .spin { 255 + animation: spin 1s linear infinite; 256 + } 257 + @keyframes spin { 258 + from { transform: rotate(0deg); } 259 + to { transform: rotate(360deg); } 260 + } 261 + 262 + /* Quota Progress Bar */ 263 + .storage-section .quota-progress { 264 + display: flex; 265 + align-items: center; 266 + gap: 0.75rem; 267 + padding: 0.75rem 0; 268 + } 269 + .storage-section .progress-bar { 270 + flex: 1; 271 + height: 8px; 272 + background: var(--border); 273 + border-radius: 4px; 274 + overflow: hidden; 275 + } 276 + .storage-section .progress-fill { 277 + height: 100%; 278 + border-radius: 4px; 279 + transition: width 0.3s ease; 280 + } 281 + .storage-section .progress-ok { 282 + background: #22c55e; 283 + } 284 + .storage-section .progress-warning { 285 + background: #eab308; 286 + } 287 + .storage-section .progress-danger { 288 + background: #ef4444; 289 + } 290 + .storage-section .progress-text { 291 + font-size: 0.85rem; 292 + color: var(--fg-muted); 293 + white-space: nowrap; 294 + } 295 + 296 + /* Tier Badge */ 297 + .storage-section .tier-badge { 298 + text-transform: capitalize; 299 + padding: 0.125rem 0.5rem; 300 + border-radius: 4px; 301 + font-size: 0.85rem; 302 + background: var(--accent-bg, #e0f2fe); 303 + color: var(--accent, #0369a1); 304 + } 305 + .storage-section .tier-owner { 306 + background: #fef3c7; 307 + color: #92400e; 308 + } 309 + .storage-section .tier-quartermaster { 310 + background: #dcfce7; 311 + color: #166534; 312 + } 313 + .storage-section .tier-bosun { 314 + background: #e0e7ff; 315 + color: #3730a3; 316 + } 317 + .storage-section .unlimited-badge { 318 + font-size: 0.75rem; 319 + padding: 0.125rem 0.375rem; 320 + background: #22c55e; 321 + color: #fff; 322 + border-radius: 3px; 323 + margin-left: 0.25rem; 324 + font-weight: 500; 325 + } 326 + 327 /* Devices Section Styles */ 328 .devices-section .setup-instructions { 329 margin: 1rem 0;
+204
pkg/appview/templates/pages/terms.html
···
··· 1 + {{ define "terms" }} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <title>Terms of Service - ATCR</title> 6 + {{ template "head" . }} 7 + </head> 8 + <body> 9 + {{ template "nav" . }} 10 + 11 + <main class="container"> 12 + <div class="legal-page"> 13 + <h1>Terms of Service - AT Container Registry (atcr.io)</h1> 14 + <p class="legal-updated"><em>Last updated: January 2025</em></p> 15 + 16 + <p>These Terms of Service ("Terms") govern your use of AT Container Registry ("atcr.io", "the Service", "we", "us", "our"). By using the Service, you agree to these Terms. If you do not agree, do not use the Service.</p> 17 + 18 + <div class="legal-section"> 19 + <h2>1. Description of Service</h2> 20 + <p>AT Container Registry is an OCI-compatible container registry built on the AT Protocol. The Service allows you to store, distribute, and manage container images using your AT Protocol identity.</p> 21 + </div> 22 + 23 + <div class="legal-section"> 24 + <h2>2. Accounts and Access</h2> 25 + 26 + <h3>2.1 AT Protocol Identity</h3> 27 + <p>Access to the Service requires an AT Protocol identity (DID). You are responsible for maintaining the security of your PDS credentials and any device tokens issued by the Service.</p> 28 + 29 + <h3>2.2 Account Responsibility</h3> 30 + <p>You are responsible for all activity under your account, whether authorized by you or not. Notify us immediately if you suspect unauthorized access.</p> 31 + 32 + <h3>2.3 Age Requirement</h3> 33 + <p>You must be at least 13 years old to use the Service. If you are under 18, you must have permission from a parent or legal guardian.</p> 34 + </div> 35 + 36 + <div class="legal-section"> 37 + <h2>3. Acceptable Use</h2> 38 + 39 + <p>You agree NOT to use the Service to:</p> 40 + <ul> 41 + <li>Store or distribute malware, viruses, or malicious code</li> 42 + <li>Store or distribute illegal content under applicable law</li> 43 + <li>Infringe on intellectual property rights of others</li> 44 + <li>Store content you do not have the right to distribute</li> 45 + <li>Circumvent or abuse usage quotas or rate limits</li> 46 + <li>Interfere with or disrupt the Service or its infrastructure</li> 47 + <li>Use the Service for cryptocurrency mining or similarly resource-intensive activities unrelated to container distribution</li> 48 + <li>Impersonate others or misrepresent your affiliation with any person or entity</li> 49 + </ul> 50 + 51 + <p>We reserve the right to determine what constitutes a violation of these terms.</p> 52 + </div> 53 + 54 + <div class="legal-section"> 55 + <h2>4. Content and Intellectual Property</h2> 56 + 57 + <h3>4.1 Your Content</h3> 58 + <p>You retain ownership of container images and other content you upload to the Service. By uploading content, you grant us a license to store, cache, and distribute that content as necessary to operate the Service.</p> 59 + 60 + <h3>4.2 Content-Addressed Storage</h3> 61 + <p>The Service uses content-addressed, deduplicated storage. Identical image layers uploaded by different users are stored once and shared. This means:</p> 62 + <ul> 63 + <li>You cannot delete blob data that is also referenced by other users</li> 64 + <li>Blob data alone cannot identify you; only the associated records link content to your identity</li> 65 + </ul> 66 + 67 + <h3>4.3 Public Data</h3> 68 + <p>AT Protocol records are public by design. Container image references, tags, and metadata associated with your identity are publicly visible. Do not store sensitive information in image tags, labels, or other public metadata.</p> 69 + 70 + <h3>4.4 Content Removal</h3> 71 + <p>We may remove content that violates these Terms or applicable law. We may also remove content in response to valid legal requests.</p> 72 + </div> 73 + 74 + <div class="legal-section"> 75 + <h2>5. Usage Quotas and Limits</h2> 76 + 77 + <h3>5.1 Free Tier</h3> 78 + <p>The Service offers a free tier subject to usage quotas. These quotas may include limits on storage, bandwidth, or number of repositories. Current quotas are published in your account settings.</p> 79 + 80 + <h3>5.2 Quota Changes</h3> 81 + <p>We may adjust free tier quotas at any time. We will make reasonable efforts to notify users of significant changes, but continued use after changes constitutes acceptance.</p> 82 + 83 + <h3>5.3 Paid Tiers</h3> 84 + <p>Paid tiers with higher quotas may be offered in the future. Paid tier terms will be provided at the time of purchase.</p> 85 + </div> 86 + 87 + <div class="legal-section"> 88 + <h2>6. Service Availability</h2> 89 + 90 + <h3>6.1 No SLA for Free Tier</h3> 91 + <p>The free tier is provided on a best-effort basis. We make no guarantees regarding uptime, availability, or performance. The Service may be unavailable due to maintenance, infrastructure issues, or resource constraints.</p> 92 + 93 + <h3>6.2 Service Changes</h3> 94 + <p>We may modify, suspend, or discontinue the Service (or any part of it) at any time, with or without notice. We are not liable to you or any third party for any modification, suspension, or discontinuation.</p> 95 + 96 + <h3>6.3 Data Durability</h3> 97 + <p>While we take reasonable measures to protect your data, we do not guarantee data durability. You are responsible for maintaining backups of your container images.</p> 98 + </div> 99 + 100 + <div class="legal-section"> 101 + <h2>7. AT Protocol Considerations</h2> 102 + 103 + <h3>7.1 Distributed Architecture</h3> 104 + <p>The Service operates on the AT Protocol, a distributed network. Data written to your PDS is controlled by you or your PDS hosting provider, not by us.</p> 105 + 106 + <h3>7.2 Records on Your PDS</h3> 107 + <p>When you use the Service, records are written to your PDS under the <code>io.atcr.*</code> namespace. We can create, update, and delete these records only while you have granted us OAuth access. Revoking access does not automatically delete existing records.</p> 108 + 109 + <h3>7.3 No Control Over Your PDS</h3> 110 + <p>We do not control your PDS. If your PDS is offline or your PDS provider terminates your account, we cannot restore your AT Protocol records.</p> 111 + </div> 112 + 113 + <div class="legal-section"> 114 + <h2>8. Termination</h2> 115 + 116 + <h3>8.1 By You</h3> 117 + <p>You may stop using the Service at any time. To delete your data, use the account deletion option in settings or contact us.</p> 118 + 119 + <h3>8.2 By Us</h3> 120 + <p>We may suspend or terminate your access to the Service at any time, for any reason, including but not limited to:</p> 121 + <ul> 122 + <li>Violation of these Terms</li> 123 + <li>Abuse of the Service or its infrastructure</li> 124 + <li>Extended inactivity</li> 125 + <li>Legal requirements</li> 126 + </ul> 127 + 128 + <h3>8.3 Effect of Termination</h3> 129 + <p>Upon termination, we will delete your data in accordance with our Privacy Policy. Deduplicated blob data referenced by other users will not be deleted.</p> 130 + </div> 131 + 132 + <div class="legal-section"> 133 + <h2>9. Disclaimer of Warranties</h2> 134 + 135 + <p class="legal-disclaimer">THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.</p> 136 + 137 + <p class="legal-disclaimer">WE DO NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED, ERROR-FREE, OR SECURE. WE DO NOT WARRANT THAT ANY DEFECTS WILL BE CORRECTED.</p> 138 + </div> 139 + 140 + <div class="legal-section"> 141 + <h2>10. Limitation of Liability</h2> 142 + 143 + <p class="legal-disclaimer">TO THE MAXIMUM EXTENT PERMITTED BY LAW, WE SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR GOODWILL, ARISING OUT OF OR RELATED TO YOUR USE OF THE SERVICE.</p> 144 + 145 + <p class="legal-disclaimer">OUR TOTAL LIABILITY FOR ANY CLAIM ARISING OUT OF OR RELATED TO THESE TERMS OR THE SERVICE SHALL NOT EXCEED THE AMOUNT YOU PAID US IN THE TWELVE (12) MONTHS PRECEDING THE CLAIM, OR $50 USD, WHICHEVER IS GREATER.</p> 146 + </div> 147 + 148 + <div class="legal-section"> 149 + <h2>11. Indemnification</h2> 150 + 151 + <p>You agree to indemnify and hold harmless AT Container Registry, its operators, and affiliates from any claims, damages, losses, or expenses (including reasonable legal fees) arising out of:</p> 152 + <ul> 153 + <li>Your use of the Service</li> 154 + <li>Your violation of these Terms</li> 155 + <li>Your violation of any third-party rights</li> 156 + <li>Content you upload to the Service</li> 157 + </ul> 158 + </div> 159 + 160 + <div class="legal-section"> 161 + <h2>12. Changes to Terms</h2> 162 + 163 + <p>We may update these Terms from time to time. If we make material changes, we will notify you by posting the updated Terms and updating the "Last updated" date. Your continued use of the Service after changes constitutes acceptance of the new Terms.</p> 164 + </div> 165 + 166 + <div class="legal-section"> 167 + <h2>13. Governing Law</h2> 168 + 169 + <p>These Terms shall be governed by and construed in accordance with the laws of the State of Texas, United States, without regard to conflict of law principles.</p> 170 + </div> 171 + 172 + <div class="legal-section"> 173 + <h2>14. Dispute Resolution</h2> 174 + 175 + <p>Any disputes arising out of or relating to these Terms or the Service shall first be attempted to be resolved through good-faith negotiation. If negotiation fails, disputes shall be resolved through binding arbitration or in the courts of Texas, at our discretion.</p> 176 + </div> 177 + 178 + <div class="legal-section"> 179 + <h2>15. Severability</h2> 180 + 181 + <p>If any provision of these Terms is found to be unenforceable, the remaining provisions shall remain in full force and effect.</p> 182 + </div> 183 + 184 + <div class="legal-section"> 185 + <h2>16. Entire Agreement</h2> 186 + 187 + <p>These Terms, together with our <a href="/privacy">Privacy Policy</a>, constitute the entire agreement between you and AT Container Registry regarding your use of the Service.</p> 188 + </div> 189 + 190 + <div class="legal-section"> 191 + <h2>Contact</h2> 192 + 193 + <p>For questions about these Terms, contact us at:</p> 194 + 195 + <p><strong>Email:</strong> <a href="mailto:legal@atcr.io">legal@atcr.io</a></p> 196 + <p><strong>Website:</strong> <a href="https://atcr.io">https://atcr.io</a></p> 197 + </div> 198 + </div> 199 + </main> 200 + 201 + <div id="modal"></div> 202 + </body> 203 + </html> 204 + {{ end }}
+32
pkg/appview/templates/partials/storage_stats.html
···
··· 1 + {{ define "storage_stats" }} 2 + <div class="storage-stats"> 3 + {{ if .Tier }} 4 + <div class="stat-row"> 5 + <span class="stat-label">Tier:</span> 6 + <span class="stat-value tier-badge tier-{{ .Tier }}">{{ .Tier }}</span> 7 + </div> 8 + {{ end }} 9 + <div class="stat-row"> 10 + <span class="stat-label">Storage:</span> 11 + <span class="stat-value"> 12 + {{ if .HasLimit }} 13 + {{ .HumanSize }} / {{ .HumanLimit }} 14 + {{ else }} 15 + {{ .HumanSize }} <span class="unlimited-badge">Unlimited</span> 16 + {{ end }} 17 + </span> 18 + </div> 19 + {{ if .HasLimit }} 20 + <div class="quota-progress"> 21 + <div class="progress-bar"> 22 + <div class="progress-fill {{ if ge .UsagePercent 95 }}progress-danger{{ else if ge .UsagePercent 80 }}progress-warning{{ else }}progress-ok{{ end }}" style="width: {{ .UsagePercent }}%"></div> 23 + </div> 24 + <span class="progress-text">{{ .UsagePercent }}% used</span> 25 + </div> 26 + {{ end }} 27 + <div class="stat-row"> 28 + <span class="stat-label">Unique Blobs:</span> 29 + <span class="stat-value">{{ .UniqueBlobs }}</span> 30 + </div> 31 + </div> 32 + {{ end }}
+77 -69
pkg/atproto/cbor_gen.go
··· 25 } 26 27 cw := cbg.NewCborWriter(w) 28 29 - if _, err := cw.Write([]byte{165}); err != nil { 30 return err 31 } 32 ··· 51 } 52 if _, err := cw.WriteString(string(t.Role)); err != nil { 53 return err 54 } 55 56 // t.Type (string) (string) ··· 208 } 209 210 t.Role = string(sval) 211 } 212 // t.Type (string) (string) 213 case "$type": ··· 654 655 cw := cbg.NewCborWriter(w) 656 657 - if _, err := cw.Write([]byte{168}); err != nil { 658 return err 659 } 660 ··· 749 return err 750 } 751 752 // t.CreatedAt (string) (string) 753 if len("createdAt") > 8192 { 754 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 794 if _, err := cw.WriteString(string(t.MediaType)); err != nil { 795 return err 796 } 797 - 798 - // t.Repository (string) (string) 799 - if len("repository") > 8192 { 800 - return xerrors.Errorf("Value in field \"repository\" was too long") 801 - } 802 - 803 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repository"))); err != nil { 804 - return err 805 - } 806 - if _, err := cw.WriteString(string("repository")); err != nil { 807 - return err 808 - } 809 - 810 - if len(t.Repository) > 8192 { 811 - return xerrors.Errorf("Value in field t.Repository was too long") 812 - } 813 - 814 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repository))); err != nil { 815 - return err 816 - } 817 - if _, err := cw.WriteString(string(t.Repository)); err != nil { 818 - return err 819 - } 820 - 821 - // t.UserHandle (string) (string) 822 - if len("userHandle") > 8192 { 823 - return xerrors.Errorf("Value in field \"userHandle\" was too long") 824 - } 825 - 826 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("userHandle"))); err != nil { 827 - return err 828 - } 829 - if _, err := cw.WriteString(string("userHandle")); err != nil { 830 - return err 831 - } 832 - 833 - if len(t.UserHandle) > 8192 { 834 - return xerrors.Errorf("Value in field t.UserHandle was too long") 835 - } 836 - 837 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.UserHandle))); err != nil { 838 - return err 839 - } 840 - if _, err := cw.WriteString(string(t.UserHandle)); err != nil { 841 - return err 842 - } 843 return nil 844 } 845 ··· 868 869 n := extra 870 871 - nameBuf := make([]byte, 10) 872 for i := uint64(0); i < n; i++ { 873 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 874 if err != nil { ··· 943 944 t.UserDID = string(sval) 945 } 946 - // t.CreatedAt (string) (string) 947 - case "createdAt": 948 - 949 - { 950 - sval, err := cbg.ReadStringWithMax(cr, 8192) 951 - if err != nil { 952 - return err 953 - } 954 - 955 - t.CreatedAt = string(sval) 956 - } 957 - // t.MediaType (string) (string) 958 - case "mediaType": 959 960 { 961 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 963 return err 964 } 965 966 - t.MediaType = string(sval) 967 } 968 - // t.Repository (string) (string) 969 - case "repository": 970 971 { 972 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 974 return err 975 } 976 977 - t.Repository = string(sval) 978 } 979 - // t.UserHandle (string) (string) 980 - case "userHandle": 981 982 { 983 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 985 return err 986 } 987 988 - t.UserHandle = string(sval) 989 } 990 991 default:
··· 25 } 26 27 cw := cbg.NewCborWriter(w) 28 + fieldCount := 6 29 30 + if t.Tier == "" { 31 + fieldCount-- 32 + } 33 + 34 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 35 return err 36 } 37 ··· 56 } 57 if _, err := cw.WriteString(string(t.Role)); err != nil { 58 return err 59 + } 60 + 61 + // t.Tier (string) (string) 62 + if t.Tier != "" { 63 + 64 + if len("tier") > 8192 { 65 + return xerrors.Errorf("Value in field \"tier\" was too long") 66 + } 67 + 68 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tier"))); err != nil { 69 + return err 70 + } 71 + if _, err := cw.WriteString(string("tier")); err != nil { 72 + return err 73 + } 74 + 75 + if len(t.Tier) > 8192 { 76 + return xerrors.Errorf("Value in field t.Tier was too long") 77 + } 78 + 79 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Tier))); err != nil { 80 + return err 81 + } 82 + if _, err := cw.WriteString(string(t.Tier)); err != nil { 83 + return err 84 + } 85 } 86 87 // t.Type (string) (string) ··· 239 } 240 241 t.Role = string(sval) 242 + } 243 + // t.Tier (string) (string) 244 + case "tier": 245 + 246 + { 247 + sval, err := cbg.ReadStringWithMax(cr, 8192) 248 + if err != nil { 249 + return err 250 + } 251 + 252 + t.Tier = string(sval) 253 } 254 // t.Type (string) (string) 255 case "$type": ··· 696 697 cw := cbg.NewCborWriter(w) 698 699 + if _, err := cw.Write([]byte{167}); err != nil { 700 return err 701 } 702 ··· 791 return err 792 } 793 794 + // t.Manifest (string) (string) 795 + if len("manifest") > 8192 { 796 + return xerrors.Errorf("Value in field \"manifest\" was too long") 797 + } 798 + 799 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("manifest"))); err != nil { 800 + return err 801 + } 802 + if _, err := cw.WriteString(string("manifest")); err != nil { 803 + return err 804 + } 805 + 806 + if len(t.Manifest) > 8192 { 807 + return xerrors.Errorf("Value in field t.Manifest was too long") 808 + } 809 + 810 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Manifest))); err != nil { 811 + return err 812 + } 813 + if _, err := cw.WriteString(string(t.Manifest)); err != nil { 814 + return err 815 + } 816 + 817 // t.CreatedAt (string) (string) 818 if len("createdAt") > 8192 { 819 return xerrors.Errorf("Value in field \"createdAt\" was too long") ··· 859 if _, err := cw.WriteString(string(t.MediaType)); err != nil { 860 return err 861 } 862 return nil 863 } 864 ··· 887 888 n := extra 889 890 + nameBuf := make([]byte, 9) 891 for i := uint64(0); i < n; i++ { 892 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 893 if err != nil { ··· 962 963 t.UserDID = string(sval) 964 } 965 + // t.Manifest (string) (string) 966 + case "manifest": 967 968 { 969 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 971 return err 972 } 973 974 + t.Manifest = string(sval) 975 } 976 + // t.CreatedAt (string) (string) 977 + case "createdAt": 978 979 { 980 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 982 return err 983 } 984 985 + t.CreatedAt = string(sval) 986 } 987 + // t.MediaType (string) (string) 988 + case "mediaType": 989 990 { 991 sval, err := cbg.ReadStringWithMax(cr, 8192) ··· 993 return err 994 } 995 996 + t.MediaType = string(sval) 997 } 998 999 default:
+11 -15
pkg/atproto/directory_test.go
··· 29 t.Run("concurrent access is thread-safe", func(t *testing.T) { 30 const numGoroutines = 100 31 var wg sync.WaitGroup 32 - wg.Add(numGoroutines) 33 34 // Channel to collect all directory instances 35 - instances := make(chan interface{}, numGoroutines) 36 37 // Launch many goroutines concurrently accessing GetDirectory 38 - for i := 0; i < numGoroutines; i++ { 39 - go func() { 40 - defer wg.Done() 41 dir := GetDirectory() 42 instances <- dir 43 - }() 44 } 45 46 // Wait for all goroutines to complete ··· 48 close(instances) 49 50 // Collect all instances 51 - var dirs []interface{} 52 for dir := range instances { 53 dirs = append(dirs, dir) 54 } ··· 72 func TestGetDirectorySequential(t *testing.T) { 73 t.Run("multiple calls in sequence", func(t *testing.T) { 74 // Get directory multiple times in sequence 75 - dirs := make([]interface{}, 10) 76 - for i := 0; i < 10; i++ { 77 dirs[i] = GetDirectory() 78 } 79 ··· 120 121 const numGoroutines = 50 122 var wg sync.WaitGroup 123 - wg.Add(numGoroutines) 124 125 - instances := make([]interface{}, numGoroutines) 126 var mu sync.Mutex 127 128 // Simulate many goroutines trying to get the directory simultaneously 129 for i := 0; i < numGoroutines; i++ { 130 - go func(idx int) { 131 - defer wg.Done() 132 dir := GetDirectory() 133 mu.Lock() 134 - instances[idx] = dir 135 mu.Unlock() 136 - }(i) 137 } 138 139 wg.Wait()
··· 29 t.Run("concurrent access is thread-safe", func(t *testing.T) { 30 const numGoroutines = 100 31 var wg sync.WaitGroup 32 33 // Channel to collect all directory instances 34 + instances := make(chan any, numGoroutines) 35 36 // Launch many goroutines concurrently accessing GetDirectory 37 + for range numGoroutines { 38 + wg.Go(func() { 39 dir := GetDirectory() 40 instances <- dir 41 + }) 42 } 43 44 // Wait for all goroutines to complete ··· 46 close(instances) 47 48 // Collect all instances 49 + var dirs []any 50 for dir := range instances { 51 dirs = append(dirs, dir) 52 } ··· 70 func TestGetDirectorySequential(t *testing.T) { 71 t.Run("multiple calls in sequence", func(t *testing.T) { 72 // Get directory multiple times in sequence 73 + dirs := make([]any, 10) 74 + for i := range 10 { 75 dirs[i] = GetDirectory() 76 } 77 ··· 118 119 const numGoroutines = 50 120 var wg sync.WaitGroup 121 122 + instances := make([]any, numGoroutines) 123 var mu sync.Mutex 124 125 // Simulate many goroutines trying to get the directory simultaneously 126 for i := 0; i < numGoroutines; i++ { 127 + wg.Go(func() { 128 dir := GetDirectory() 129 mu.Lock() 130 + instances[i] = dir 131 mu.Unlock() 132 + }) 133 } 134 135 wg.Wait()
+6
pkg/atproto/endpoints.go
··· 51 // Request: {"ownerDid": "...", "repository": "...", "pullCount": 10, "pushCount": 5, "lastPull": "...", "lastPush": "..."} 52 // Response: {"success": true} 53 HoldSetStats = "/xrpc/io.atcr.hold.setStats" 54 ) 55 56 // Hold service crew management endpoints (io.atcr.hold.*)
··· 51 // Request: {"ownerDid": "...", "repository": "...", "pullCount": 10, "pushCount": 5, "lastPull": "...", "lastPush": "..."} 52 // Response: {"success": true} 53 HoldSetStats = "/xrpc/io.atcr.hold.setStats" 54 + 55 + // HoldGetQuota returns storage quota information for a user. 56 + // Method: GET 57 + // Query: userDid={did} 58 + // Response: {"userDid": "...", "uniqueBlobs": 10, "totalSize": 1073741824} 59 + HoldGetQuota = "/xrpc/io.atcr.hold.getQuota" 60 ) 61 62 // Hold service crew management endpoints (io.atcr.hold.*)
+18 -18
pkg/atproto/lexicon.go
··· 594 Member string `json:"member" cborgen:"member"` 595 Role string `json:"role" cborgen:"role"` 596 Permissions []string `json:"permissions" cborgen:"permissions"` 597 - AddedAt string `json:"addedAt" cborgen:"addedAt"` // RFC3339 timestamp 598 } 599 600 // LayerRecord represents metadata about a container layer stored in the hold ··· 602 // Stored in the hold's embedded PDS for tracking and analytics 603 // Uses CBOR encoding for efficient storage in hold's carstore 604 type LayerRecord struct { 605 - Type string `json:"$type" cborgen:"$type"` 606 - Digest string `json:"digest" cborgen:"digest"` // Layer digest (e.g., "sha256:abc123...") 607 - Size int64 `json:"size" cborgen:"size"` // Size in bytes 608 - MediaType string `json:"mediaType" cborgen:"mediaType"` // Media type (e.g., "application/vnd.oci.image.layer.v1.tar+gzip") 609 - Repository string `json:"repository" cborgen:"repository"` // Repository this layer belongs to 610 - UserDID string `json:"userDid" cborgen:"userDid"` // DID of user who uploaded this layer 611 - UserHandle string `json:"userHandle" cborgen:"userHandle"` // Handle of user (for display purposes) 612 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` // RFC3339 timestamp 613 } 614 615 // NewLayerRecord creates a new layer record 616 - func NewLayerRecord(digest string, size int64, mediaType, repository, userDID, userHandle string) *LayerRecord { 617 return &LayerRecord{ 618 - Type: LayerCollection, 619 - Digest: digest, 620 - Size: size, 621 - MediaType: mediaType, 622 - Repository: repository, 623 - UserDID: userDID, 624 - UserHandle: userHandle, 625 - CreatedAt: time.Now().Format(time.RFC3339), 626 } 627 } 628
··· 594 Member string `json:"member" cborgen:"member"` 595 Role string `json:"role" cborgen:"role"` 596 Permissions []string `json:"permissions" cborgen:"permissions"` 597 + Tier string `json:"tier,omitempty" cborgen:"tier,omitempty"` // Optional tier for quota limits (e.g., 'deckhand', 'bosun', 'quartermaster') 598 + AddedAt string `json:"addedAt" cborgen:"addedAt"` // RFC3339 timestamp 599 } 600 601 // LayerRecord represents metadata about a container layer stored in the hold ··· 603 // Stored in the hold's embedded PDS for tracking and analytics 604 // Uses CBOR encoding for efficient storage in hold's carstore 605 type LayerRecord struct { 606 + Type string `json:"$type" cborgen:"$type"` 607 + Digest string `json:"digest" cborgen:"digest"` // Layer digest (e.g., "sha256:abc123...") 608 + Size int64 `json:"size" cborgen:"size"` // Size in bytes 609 + MediaType string `json:"mediaType" cborgen:"mediaType"` // Media type (e.g., "application/vnd.oci.image.layer.v1.tar+gzip") 610 + Manifest string `json:"manifest" cborgen:"manifest"` // AT-URI of manifest that included this layer 611 + UserDID string `json:"userDid" cborgen:"userDid"` // DID of user who uploaded this layer 612 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` // RFC3339 timestamp 613 } 614 615 // NewLayerRecord creates a new layer record 616 + // manifestURI: AT-URI of the manifest (e.g., "at://did:plc:xyz/io.atcr.manifest/abc123") 617 + func NewLayerRecord(digest string, size int64, mediaType, userDID, manifestURI string) *LayerRecord { 618 return &LayerRecord{ 619 + Type: LayerCollection, 620 + Digest: digest, 621 + Size: size, 622 + MediaType: mediaType, 623 + Manifest: manifestURI, 624 + UserDID: userDID, 625 + CreatedAt: time.Now().Format(time.RFC3339), 626 } 627 } 628
+36 -49
pkg/atproto/lexicon_test.go
··· 1089 1090 func TestNewLayerRecord(t *testing.T) { 1091 tests := []struct { 1092 - name string 1093 - digest string 1094 - size int64 1095 - mediaType string 1096 - repository string 1097 - userDID string 1098 - userHandle string 1099 }{ 1100 { 1101 - name: "standard layer", 1102 - digest: "sha256:abc123", 1103 - size: 1024, 1104 - mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1105 - repository: "myapp", 1106 - userDID: "did:plc:user123", 1107 - userHandle: "alice.bsky.social", 1108 }, 1109 { 1110 - name: "large layer", 1111 - digest: "sha256:def456", 1112 - size: 1073741824, // 1GB 1113 - mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1114 - repository: "largeapp", 1115 - userDID: "did:plc:user456", 1116 - userHandle: "bob.example.com", 1117 }, 1118 { 1119 - name: "empty values", 1120 - digest: "", 1121 - size: 0, 1122 - mediaType: "", 1123 - repository: "", 1124 - userDID: "", 1125 - userHandle: "", 1126 }, 1127 { 1128 - name: "config layer", 1129 - digest: "sha256:config123", 1130 - size: 512, 1131 - mediaType: "application/vnd.oci.image.config.v1+json", 1132 - repository: "app/subapp", 1133 - userDID: "did:web:example.com", 1134 - userHandle: "charlie.tangled.io", 1135 }, 1136 } 1137 1138 for _, tt := range tests { 1139 t.Run(tt.name, func(t *testing.T) { 1140 - record := NewLayerRecord(tt.digest, tt.size, tt.mediaType, tt.repository, tt.userDID, tt.userHandle) 1141 1142 // Verify all fields 1143 if record == nil { ··· 1160 t.Errorf("MediaType = %q, want %q", record.MediaType, tt.mediaType) 1161 } 1162 1163 - if record.Repository != tt.repository { 1164 - t.Errorf("Repository = %q, want %q", record.Repository, tt.repository) 1165 } 1166 1167 if record.UserDID != tt.userDID { 1168 t.Errorf("UserDID = %q, want %q", record.UserDID, tt.userDID) 1169 - } 1170 - 1171 - if record.UserHandle != tt.userHandle { 1172 - t.Errorf("UserHandle = %q, want %q", record.UserHandle, tt.userHandle) 1173 } 1174 1175 // Verify CreatedAt is set and is a valid RFC3339 timestamp ··· 1192 "sha256:abc123", 1193 1024, 1194 "application/vnd.oci.image.layer.v1.tar+gzip", 1195 - "myapp", 1196 "did:plc:user123", 1197 - "alice.bsky.social", 1198 ) 1199 1200 // Marshal to JSON ··· 1222 if decoded.MediaType != record.MediaType { 1223 t.Errorf("MediaType = %q, want %q", decoded.MediaType, record.MediaType) 1224 } 1225 - if decoded.Repository != record.Repository { 1226 - t.Errorf("Repository = %q, want %q", decoded.Repository, record.Repository) 1227 } 1228 if decoded.UserDID != record.UserDID { 1229 t.Errorf("UserDID = %q, want %q", decoded.UserDID, record.UserDID) 1230 - } 1231 - if decoded.UserHandle != record.UserHandle { 1232 - t.Errorf("UserHandle = %q, want %q", decoded.UserHandle, record.UserHandle) 1233 } 1234 if decoded.CreatedAt != record.CreatedAt { 1235 t.Errorf("CreatedAt = %q, want %q", decoded.CreatedAt, record.CreatedAt)
··· 1089 1090 func TestNewLayerRecord(t *testing.T) { 1091 tests := []struct { 1092 + name string 1093 + digest string 1094 + size int64 1095 + mediaType string 1096 + userDID string 1097 + manifestURI string 1098 }{ 1099 { 1100 + name: "standard layer", 1101 + digest: "sha256:abc123", 1102 + size: 1024, 1103 + mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1104 + userDID: "did:plc:user123", 1105 + manifestURI: "at://did:plc:user123/io.atcr.manifest/abc123", 1106 }, 1107 { 1108 + name: "large layer", 1109 + digest: "sha256:def456", 1110 + size: 1073741824, // 1GB 1111 + mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 1112 + userDID: "did:plc:user456", 1113 + manifestURI: "at://did:plc:user456/io.atcr.manifest/def456", 1114 }, 1115 { 1116 + name: "empty values", 1117 + digest: "", 1118 + size: 0, 1119 + mediaType: "", 1120 + userDID: "", 1121 + manifestURI: "", 1122 }, 1123 { 1124 + name: "config layer", 1125 + digest: "sha256:config123", 1126 + size: 512, 1127 + mediaType: "application/vnd.oci.image.config.v1+json", 1128 + userDID: "did:web:example.com", 1129 + manifestURI: "at://did:web:example.com/io.atcr.manifest/config123", 1130 }, 1131 } 1132 1133 for _, tt := range tests { 1134 t.Run(tt.name, func(t *testing.T) { 1135 + record := NewLayerRecord(tt.digest, tt.size, tt.mediaType, tt.userDID, tt.manifestURI) 1136 1137 // Verify all fields 1138 if record == nil { ··· 1155 t.Errorf("MediaType = %q, want %q", record.MediaType, tt.mediaType) 1156 } 1157 1158 + if record.Manifest != tt.manifestURI { 1159 + t.Errorf("Manifest = %q, want %q", record.Manifest, tt.manifestURI) 1160 } 1161 1162 if record.UserDID != tt.userDID { 1163 t.Errorf("UserDID = %q, want %q", record.UserDID, tt.userDID) 1164 } 1165 1166 // Verify CreatedAt is set and is a valid RFC3339 timestamp ··· 1183 "sha256:abc123", 1184 1024, 1185 "application/vnd.oci.image.layer.v1.tar+gzip", 1186 "did:plc:user123", 1187 + "at://did:plc:user123/io.atcr.manifest/abc123", 1188 ) 1189 1190 // Marshal to JSON ··· 1212 if decoded.MediaType != record.MediaType { 1213 t.Errorf("MediaType = %q, want %q", decoded.MediaType, record.MediaType) 1214 } 1215 + if decoded.Manifest != record.Manifest { 1216 + t.Errorf("Manifest = %q, want %q", decoded.Manifest, record.Manifest) 1217 } 1218 if decoded.UserDID != record.UserDID { 1219 t.Errorf("UserDID = %q, want %q", decoded.UserDID, record.UserDID) 1220 } 1221 if decoded.CreatedAt != record.CreatedAt { 1222 t.Errorf("CreatedAt = %q, want %q", decoded.CreatedAt, record.CreatedAt)
+1 -1
pkg/auth/cache.go
··· 1 - // Package token provides service token caching and management for AppView. 2 // Service tokens are JWTs issued by a user's PDS to authorize AppView to 3 // act on their behalf when communicating with hold services. Tokens are 4 // cached with automatic expiry parsing and 10-second safety margins.
··· 1 + // Package auth provides service token caching and management for AppView. 2 // Service tokens are JWTs issued by a user's PDS to authorize AppView to 3 // act on their behalf when communicating with hold services. Tokens are 4 // cached with automatic expiry parsing and 10-second safety margins.
+56 -39
pkg/auth/hold_remote.go
··· 324 } 325 326 // isCrewMemberNoCache queries XRPC without caching (internal helper) 327 func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID, userDID string) (bool, error) { 328 // Resolve DID to URL 329 holdURL := atproto.ResolveHoldURL(holdDID) 330 331 - // Build XRPC request URL 332 - // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew 333 - xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s", 334 - holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) 335 336 - req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) 337 - if err != nil { 338 - return false, err 339 - } 340 341 - resp, err := a.httpClient.Do(req) 342 - if err != nil { 343 - return false, fmt.Errorf("XRPC request failed: %w", err) 344 - } 345 - defer resp.Body.Close() 346 347 - if resp.StatusCode != http.StatusOK { 348 - body, _ := io.ReadAll(resp.Body) 349 - return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) 350 - } 351 352 - // Parse response 353 - var xrpcResp struct { 354 - Records []struct { 355 - URI string `json:"uri"` 356 - CID string `json:"cid"` 357 - Value struct { 358 - Type string `json:"$type"` 359 - Member string `json:"member"` 360 - Role string `json:"role"` 361 - Permissions []string `json:"permissions"` 362 - AddedAt string `json:"addedAt"` 363 - } `json:"value"` 364 - } `json:"records"` 365 - } 366 367 - if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { 368 - return false, fmt.Errorf("failed to decode XRPC response: %w", err) 369 - } 370 371 - // Check if userDID is in the crew list 372 - for _, record := range xrpcResp.Records { 373 - if record.Value.Member == userDID { 374 - // TODO: Check expiration if set 375 - return true, nil 376 } 377 } 378 379 return false, nil
··· 324 } 325 326 // isCrewMemberNoCache queries XRPC without caching (internal helper) 327 + // Handles pagination to check all crew records, not just the first page 328 func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID, userDID string) (bool, error) { 329 // Resolve DID to URL 330 holdURL := atproto.ResolveHoldURL(holdDID) 331 332 + // Paginate through all crew records 333 + cursor := "" 334 + for { 335 + // Build XRPC request URL with pagination 336 + // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew&limit=100 337 + xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s&limit=100", 338 + holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) 339 + if cursor != "" { 340 + xrpcURL += "&cursor=" + url.QueryEscape(cursor) 341 + } 342 343 + req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) 344 + if err != nil { 345 + return false, err 346 + } 347 348 + resp, err := a.httpClient.Do(req) 349 + if err != nil { 350 + return false, fmt.Errorf("XRPC request failed: %w", err) 351 + } 352 353 + if resp.StatusCode != http.StatusOK { 354 + body, _ := io.ReadAll(resp.Body) 355 + resp.Body.Close() 356 + return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) 357 + } 358 + 359 + // Parse response 360 + var xrpcResp struct { 361 + Cursor string `json:"cursor"` 362 + Records []struct { 363 + URI string `json:"uri"` 364 + CID string `json:"cid"` 365 + Value struct { 366 + Type string `json:"$type"` 367 + Member string `json:"member"` 368 + Role string `json:"role"` 369 + Permissions []string `json:"permissions"` 370 + AddedAt string `json:"addedAt"` 371 + } `json:"value"` 372 + } `json:"records"` 373 + } 374 375 + if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { 376 + resp.Body.Close() 377 + return false, fmt.Errorf("failed to decode XRPC response: %w", err) 378 + } 379 + resp.Body.Close() 380 381 + // Check if userDID is in this page of crew records 382 + for _, record := range xrpcResp.Records { 383 + if record.Value.Member == userDID { 384 + // TODO: Check expiration if set 385 + return true, nil 386 + } 387 + } 388 389 + // Check if there are more pages 390 + if xrpcResp.Cursor == "" || len(xrpcResp.Records) == 0 { 391 + break 392 } 393 + cursor = xrpcResp.Cursor 394 } 395 396 return false, nil
+4 -15
pkg/auth/hold_remote_test.go
··· 14 "atcr.io/pkg/atproto" 15 ) 16 17 - func TestNewRemoteHoldAuthorizer(t *testing.T) { 18 - // Test with nil database (should still work) 19 - authorizer := NewRemoteHoldAuthorizer(nil, false) 20 - if authorizer == nil { 21 - t.Fatal("Expected non-nil authorizer") 22 - } 23 - 24 - // Verify it implements the HoldAuthorizer interface 25 - var _ HoldAuthorizer = authorizer 26 - } 27 - 28 func TestNewRemoteHoldAuthorizer_TestMode(t *testing.T) { 29 // Test with testMode enabled 30 authorizer := NewRemoteHoldAuthorizer(nil, true) ··· 78 } 79 80 // Return mock response 81 - response := map[string]interface{}{ 82 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 83 "cid": "bafytest123", 84 - "value": map[string]interface{}{ 85 "$type": atproto.CaptainCollection, 86 "owner": "did:plc:owner123", 87 "public": true, ··· 281 func TestCheckReadAccess_PublicHold(t *testing.T) { 282 // Create mock server that returns public captain record 283 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 284 - response := map[string]interface{}{ 285 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 286 "cid": "bafytest123", 287 - "value": map[string]interface{}{ 288 "$type": atproto.CaptainCollection, 289 "owner": "did:plc:owner123", 290 "public": true, // Public hold
··· 14 "atcr.io/pkg/atproto" 15 ) 16 17 func TestNewRemoteHoldAuthorizer_TestMode(t *testing.T) { 18 // Test with testMode enabled 19 authorizer := NewRemoteHoldAuthorizer(nil, true) ··· 67 } 68 69 // Return mock response 70 + response := map[string]any{ 71 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 72 "cid": "bafytest123", 73 + "value": map[string]any{ 74 "$type": atproto.CaptainCollection, 75 "owner": "did:plc:owner123", 76 "public": true, ··· 270 func TestCheckReadAccess_PublicHold(t *testing.T) { 271 // Create mock server that returns public captain record 272 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 273 + response := map[string]any{ 274 "uri": "at://did:web:test-hold/io.atcr.hold.captain/self", 275 "cid": "bafytest123", 276 + "value": map[string]any{ 277 "$type": atproto.CaptainCollection, 278 "owner": "did:plc:owner123", 279 "public": true, // Public hold
+44 -12
pkg/auth/oauth/client.go
··· 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 ) 19 20 // NewClientApp creates an indigo OAuth ClientApp with ATCR-specific configuration 21 // Automatically configures confidential client for production deployments 22 // keyPath specifies where to store/load the OAuth client P-256 key (ignored for localhost) ··· 47 return nil, fmt.Errorf("failed to configure confidential client: %w", err) 48 } 49 50 - // Log clock information for debugging timestamp issues 51 - now := time.Now() 52 slog.Info("Configured confidential OAuth client", 53 "key_id", keyID, 54 "key_path", keyPath, 55 - "system_time_unix", now.Unix(), 56 - "system_time_rfc3339", now.Format(time.RFC3339), 57 - "timezone", now.Location().String()) 58 } else { 59 config = oauth.NewLocalhostConfig(redirectURI, scopes) 60 ··· 78 func GetDefaultScopes(did string) []string { 79 return []string{ 80 "atproto", 81 - // Permission-set (for future PDS support) 82 // See lexicons/io/atcr/authFullApp.json for definition 83 - // Uses "include:" prefix per ATProto permission spec 84 "include:io.atcr.authFullApp", 85 // com.atproto scopes must be separate (permission-sets are namespace-limited) 86 "rpc:com.atproto.repo.getRecord?aud=*", ··· 102 } 103 104 // ScopesMatch checks if two scope lists are equivalent (order-independent) 105 - // Returns true if both lists contain the same scopes, regardless of order 106 func ScopesMatch(stored, desired []string) bool { 107 // Handle nil/empty cases 108 - if len(stored) == 0 && len(desired) == 0 { 109 return true 110 } 111 - if len(stored) != len(desired) { 112 return false 113 } 114 115 // Build map of desired scopes for O(1) lookup 116 - desiredMap := make(map[string]bool, len(desired)) 117 - for _, scope := range desired { 118 desiredMap[scope] = true 119 } 120
··· 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 ) 19 20 + // permissionSetExpansions maps lexicon IDs to their expanded scope format. 21 + // These must match the collections defined in lexicons/io/atcr/authFullApp.json 22 + // Collections are sorted alphabetically for consistent comparison with PDS-expanded scopes. 23 + var permissionSetExpansions = map[string]string{ 24 + "io.atcr.authFullApp": "repo?" + 25 + "collection=io.atcr.manifest&" + 26 + "collection=io.atcr.repo.page&" + 27 + "collection=io.atcr.sailor.profile&" + 28 + "collection=io.atcr.sailor.star&" + 29 + "collection=io.atcr.tag", 30 + } 31 + 32 + // ExpandIncludeScopes expands any "include:" prefixed scopes to their full form 33 + // by looking up the corresponding permission-set in the embedded lexicon files. 34 + // For example, "include:io.atcr.authFullApp" expands to "repo?collection=io.atcr.manifest&..." 35 + func ExpandIncludeScopes(scopes []string) []string { 36 + var expanded []string 37 + for _, scope := range scopes { 38 + if strings.HasPrefix(scope, "include:") { 39 + lexiconID := strings.TrimPrefix(scope, "include:") 40 + if exp, ok := permissionSetExpansions[lexiconID]; ok { 41 + expanded = append(expanded, exp) 42 + } else { 43 + expanded = append(expanded, scope) // Keep original if unknown 44 + } 45 + } else { 46 + expanded = append(expanded, scope) 47 + } 48 + } 49 + return expanded 50 + } 51 + 52 // NewClientApp creates an indigo OAuth ClientApp with ATCR-specific configuration 53 // Automatically configures confidential client for production deployments 54 // keyPath specifies where to store/load the OAuth client P-256 key (ignored for localhost) ··· 79 return nil, fmt.Errorf("failed to configure confidential client: %w", err) 80 } 81 82 slog.Info("Configured confidential OAuth client", 83 "key_id", keyID, 84 "key_path", keyPath, 85 + ) 86 } else { 87 config = oauth.NewLocalhostConfig(redirectURI, scopes) 88 ··· 106 func GetDefaultScopes(did string) []string { 107 return []string{ 108 "atproto", 109 + // Permission-set 110 // See lexicons/io/atcr/authFullApp.json for definition 111 "include:io.atcr.authFullApp", 112 // com.atproto scopes must be separate (permission-sets are namespace-limited) 113 "rpc:com.atproto.repo.getRecord?aud=*", ··· 129 } 130 131 // ScopesMatch checks if two scope lists are equivalent (order-independent) 132 + // Returns true if both lists contain the same scopes, regardless of order. 133 + // Expands any "include:" prefixed scopes in the desired list before comparing, 134 + // since the PDS returns expanded scopes in the stored session. 135 func ScopesMatch(stored, desired []string) bool { 136 + // Expand any include: scopes in desired before comparing 137 + expandedDesired := ExpandIncludeScopes(desired) 138 + 139 // Handle nil/empty cases 140 + if len(stored) == 0 && len(expandedDesired) == 0 { 141 return true 142 } 143 + if len(stored) != len(expandedDesired) { 144 return false 145 } 146 147 // Build map of desired scopes for O(1) lookup 148 + desiredMap := make(map[string]bool, len(expandedDesired)) 149 + for _, scope := range expandedDesired { 150 desiredMap[scope] = true 151 } 152
+2 -1
pkg/auth/oauth/client_test.go
··· 1 package oauth 2 3 import ( 4 "github.com/bluesky-social/indigo/atproto/auth/oauth" 5 - "testing" 6 ) 7 8 func TestNewClientApp(t *testing.T) {
··· 1 package oauth 2 3 import ( 4 + "testing" 5 + 6 "github.com/bluesky-social/indigo/atproto/auth/oauth" 7 ) 8 9 func TestNewClientApp(t *testing.T) {
+2 -11
pkg/auth/oauth/server_test.go
··· 2 3 import ( 4 "context" 5 - "github.com/bluesky-social/indigo/atproto/auth/oauth" 6 "net/http" 7 "net/http/httptest" 8 "strings" 9 "testing" 10 "time" 11 ) 12 13 func TestNewServer(t *testing.T) { ··· 112 func (m *mockUISessionStore) DeleteByDID(did string) { 113 if m.deleteByDIDFunc != nil { 114 m.deleteByDIDFunc(did) 115 - } 116 - } 117 - 118 - type mockRefresher struct { 119 - invalidateSessionFunc func(did string) 120 - } 121 - 122 - func (m *mockRefresher) InvalidateSession(did string) { 123 - if m.invalidateSessionFunc != nil { 124 - m.invalidateSessionFunc(did) 125 } 126 } 127
··· 2 3 import ( 4 "context" 5 "net/http" 6 "net/http/httptest" 7 "strings" 8 "testing" 9 "time" 10 + 11 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 12 ) 13 14 func TestNewServer(t *testing.T) { ··· 113 func (m *mockUISessionStore) DeleteByDID(did string) { 114 if m.deleteByDIDFunc != nil { 115 m.deleteByDIDFunc(did) 116 } 117 } 118
+23 -5
pkg/auth/session.go
··· 9 "crypto/sha256" 10 "encoding/hex" 11 "encoding/json" 12 "fmt" 13 "io" 14 "log/slog" ··· 17 "time" 18 19 "atcr.io/pkg/atproto" 20 ) 21 22 // CachedSession represents a cached session ··· 99 // Resolve identifier to PDS endpoint 100 _, _, pds, err := atproto.ResolveIdentity(ctx, identifier) 101 if err != nil { 102 - return "", "", "", err 103 } 104 105 // Create session 106 sessionResp, err := v.createSession(ctx, pds, identifier, password) 107 if err != nil { 108 - return "", "", "", fmt.Errorf("authentication failed: %w", err) 109 } 110 111 // Cache the session (ATProto sessions typically last 2 hours) ··· 146 resp, err := v.httpClient.Do(req) 147 if err != nil { 148 slog.Debug("Session creation HTTP request failed", "error", err) 149 - return nil, fmt.Errorf("failed to create session: %w", err) 150 } 151 defer resp.Body.Close() 152 ··· 155 if resp.StatusCode == http.StatusUnauthorized { 156 bodyBytes, _ := io.ReadAll(resp.Body) 157 slog.Debug("Session creation unauthorized", "response", string(bodyBytes)) 158 - return nil, fmt.Errorf("invalid credentials") 159 } 160 161 if resp.StatusCode != http.StatusOK { 162 bodyBytes, _ := io.ReadAll(resp.Body) 163 slog.Debug("Session creation failed", "status", resp.StatusCode, "response", string(bodyBytes)) 164 - return nil, fmt.Errorf("create session failed with status %d: %s", resp.StatusCode, string(bodyBytes)) 165 } 166 167 var sessionResp SessionResponse
··· 9 "crypto/sha256" 10 "encoding/hex" 11 "encoding/json" 12 + "errors" 13 "fmt" 14 "io" 15 "log/slog" ··· 18 "time" 19 20 "atcr.io/pkg/atproto" 21 + ) 22 + 23 + // Sentinel errors for authentication failures 24 + var ( 25 + // ErrIdentityResolution indicates handle/DID resolution failed 26 + ErrIdentityResolution = errors.New("identity resolution failed") 27 + // ErrInvalidCredentials indicates PDS returned 401 (bad password/app-password) 28 + ErrInvalidCredentials = errors.New("invalid credentials") 29 + // ErrPDSUnavailable indicates PDS is unreachable or returned a server error 30 + ErrPDSUnavailable = errors.New("PDS unavailable") 31 ) 32 33 // CachedSession represents a cached session ··· 110 // Resolve identifier to PDS endpoint 111 _, _, pds, err := atproto.ResolveIdentity(ctx, identifier) 112 if err != nil { 113 + return "", "", "", fmt.Errorf("%w: %v", ErrIdentityResolution, err) 114 } 115 116 // Create session 117 sessionResp, err := v.createSession(ctx, pds, identifier, password) 118 if err != nil { 119 + // Pass through typed errors from createSession 120 + return "", "", "", err 121 } 122 123 // Cache the session (ATProto sessions typically last 2 hours) ··· 158 resp, err := v.httpClient.Do(req) 159 if err != nil { 160 slog.Debug("Session creation HTTP request failed", "error", err) 161 + return nil, fmt.Errorf("%w: %v", ErrPDSUnavailable, err) 162 } 163 defer resp.Body.Close() 164 ··· 167 if resp.StatusCode == http.StatusUnauthorized { 168 bodyBytes, _ := io.ReadAll(resp.Body) 169 slog.Debug("Session creation unauthorized", "response", string(bodyBytes)) 170 + return nil, ErrInvalidCredentials 171 + } 172 + 173 + if resp.StatusCode >= 500 { 174 + bodyBytes, _ := io.ReadAll(resp.Body) 175 + slog.Debug("PDS server error", "status", resp.StatusCode, "response", string(bodyBytes)) 176 + return nil, fmt.Errorf("%w: server returned %d", ErrPDSUnavailable, resp.StatusCode) 177 } 178 179 if resp.StatusCode != http.StatusOK { 180 bodyBytes, _ := io.ReadAll(resp.Body) 181 slog.Debug("Session creation failed", "status", resp.StatusCode, "response", string(bodyBytes)) 182 + return nil, fmt.Errorf("%w: unexpected status %d: %s", ErrPDSUnavailable, resp.StatusCode, string(bodyBytes)) 183 } 184 185 var sessionResp SessionResponse
+1
pkg/auth/token/claims.go
··· 1 package token 2 3 import (
··· 1 + // Package token provides JWT claims and token handling for registry authentication. 2 package token 3 4 import (
+15 -2
pkg/auth/token/handler.go
··· 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 "net/http" ··· 194 slog.Debug("Trying app password authentication", "username", username) 195 did, handle, accessToken, err = h.validator.CreateSessionAndGetToken(r.Context(), username, password) 196 if err != nil { 197 - slog.Debug("App password validation failed", "error", err, "username", username) 198 - sendAuthError(w, r, "authentication failed") 199 return 200 } 201
··· 3 import ( 4 "context" 5 "encoding/json" 6 + "errors" 7 "fmt" 8 "log/slog" 9 "net/http" ··· 195 slog.Debug("Trying app password authentication", "username", username) 196 did, handle, accessToken, err = h.validator.CreateSessionAndGetToken(r.Context(), username, password) 197 if err != nil { 198 + // Log at WARN level with specific error type 199 + if errors.Is(err, auth.ErrIdentityResolution) { 200 + slog.Warn("Identity resolution failed", "error", err, "username", username) 201 + sendAuthError(w, r, "authentication failed: could not resolve handle") 202 + } else if errors.Is(err, auth.ErrInvalidCredentials) { 203 + slog.Warn("Invalid credentials", "username", username) 204 + sendAuthError(w, r, "authentication failed: invalid credentials") 205 + } else if errors.Is(err, auth.ErrPDSUnavailable) { 206 + slog.Warn("PDS unavailable", "error", err, "username", username) 207 + sendAuthError(w, r, "authentication failed: PDS unavailable") 208 + } else { 209 + slog.Warn("Authentication failed", "error", err, "username", username) 210 + sendAuthError(w, r, "authentication failed") 211 + } 212 return 213 } 214
+1 -3
pkg/auth/token/handler_test.go
··· 2 3 import ( 4 "context" 5 - "crypto/rsa" 6 "crypto/tls" 7 "database/sql" 8 "encoding/base64" ··· 22 // Shared test key to avoid generating a new RSA key for each test 23 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves ~4.5s for 32 tests 24 var ( 25 - sharedTestKey *rsa.PrivateKey 26 sharedTestKeyPath string 27 sharedTestKeyOnce sync.Once 28 sharedTestKeyDir string ··· 513 } 514 515 // Verify JSON structure 516 - var decoded map[string]interface{} 517 if err := json.Unmarshal(data, &decoded); err != nil { 518 t.Fatalf("Failed to unmarshal JSON: %v", err) 519 }
··· 2 3 import ( 4 "context" 5 "crypto/tls" 6 "database/sql" 7 "encoding/base64" ··· 21 // Shared test key to avoid generating a new RSA key for each test 22 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves ~4.5s for 32 tests 23 var ( 24 sharedTestKeyPath string 25 sharedTestKeyOnce sync.Once 26 sharedTestKeyDir string ··· 511 } 512 513 // Verify JSON structure 514 + var decoded map[string]any 515 if err := json.Unmarshal(data, &decoded); err != nil { 516 t.Fatalf("Failed to unmarshal JSON: %v", err) 517 }
+8 -11
pkg/auth/token/issuer_test.go
··· 19 // Shared test key to avoid generating a new RSA key for each test 20 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves significant time 21 var ( 22 - issuerSharedTestKey *rsa.PrivateKey 23 issuerSharedTestKeyPath string 24 issuerSharedTestKeyOnce sync.Once 25 issuerSharedTestKeyDir string ··· 207 } 208 209 // Parse and validate the token 210 - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 211 return issuer.publicKey, nil 212 }) 213 if err != nil { ··· 289 } 290 291 // x5c should be a slice of base64-encoded certificates 292 - x5cSlice, ok := x5c.([]interface{}) 293 if !ok { 294 t.Fatal("Expected x5c to be a slice") 295 } ··· 379 // Issue tokens concurrently 380 const numGoroutines = 10 381 var wg sync.WaitGroup 382 - wg.Add(numGoroutines) 383 384 tokens := make([]string, numGoroutines) 385 errors := make([]error, numGoroutines) 386 387 for i := 0; i < numGoroutines; i++ { 388 - go func(idx int) { 389 - defer wg.Done() 390 - subject := "did:plc:user" + string(rune('0'+idx)) 391 token, err := issuer.Issue(subject, nil, AuthMethodOAuth) 392 - tokens[idx] = token 393 - errors[idx] = err 394 - }(i) 395 } 396 397 wg.Wait() ··· 575 } 576 577 // Parse token and verify expiration 578 - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { 579 return issuer.publicKey, nil 580 }) 581 if err != nil {
··· 19 // Shared test key to avoid generating a new RSA key for each test 20 // Generating a 2048-bit RSA key takes ~0.15s, so reusing one key saves significant time 21 var ( 22 issuerSharedTestKeyPath string 23 issuerSharedTestKeyOnce sync.Once 24 issuerSharedTestKeyDir string ··· 206 } 207 208 // Parse and validate the token 209 + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { 210 return issuer.publicKey, nil 211 }) 212 if err != nil { ··· 288 } 289 290 // x5c should be a slice of base64-encoded certificates 291 + x5cSlice, ok := x5c.([]any) 292 if !ok { 293 t.Fatal("Expected x5c to be a slice") 294 } ··· 378 // Issue tokens concurrently 379 const numGoroutines = 10 380 var wg sync.WaitGroup 381 382 tokens := make([]string, numGoroutines) 383 errors := make([]error, numGoroutines) 384 385 for i := 0; i < numGoroutines; i++ { 386 + wg.Go(func() { 387 + subject := "did:plc:user" + string(rune('0'+i)) 388 token, err := issuer.Issue(subject, nil, AuthMethodOAuth) 389 + tokens[i] = token 390 + errors[i] = err 391 + }) 392 } 393 394 wg.Wait() ··· 572 } 573 574 // Parse token and verify expiration 575 + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { 576 return issuer.publicKey, nil 577 }) 578 if err != nil {
+349
pkg/hold/admin/admin.go
···
··· 1 + // Package admin provides an owner-only web UI for managing the hold service. 2 + // It includes OAuth-based authentication, crew management, settings configuration, 3 + // and usage metrics. The admin panel is embedded directly in the hold service binary. 4 + package admin 5 + 6 + //go:generate curl -fsSL -o static/js/htmx.min.js https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js 7 + //go:generate curl -fsSL -o static/js/lucide.min.js https://unpkg.com/lucide@latest/dist/umd/lucide.min.js 8 + 9 + import ( 10 + "context" 11 + "crypto/rand" 12 + "embed" 13 + "encoding/base64" 14 + "encoding/json" 15 + "fmt" 16 + "html/template" 17 + "io/fs" 18 + "log/slog" 19 + "net" 20 + "net/http" 21 + "net/url" 22 + "sync" 23 + "time" 24 + 25 + "atcr.io/pkg/atproto" 26 + "atcr.io/pkg/hold/pds" 27 + "atcr.io/pkg/hold/quota" 28 + 29 + indigooauth "github.com/bluesky-social/indigo/atproto/auth/oauth" 30 + "github.com/go-chi/chi/v5" 31 + ) 32 + 33 + //go:embed templates/* 34 + var templatesFS embed.FS 35 + 36 + //go:embed static/* 37 + var staticFS embed.FS 38 + 39 + // AdminConfig holds admin panel configuration 40 + type AdminConfig struct { 41 + // Enabled controls whether the admin panel is accessible 42 + Enabled bool 43 + // PublicURL is the hold's public URL (for DID resolution, AppView communication) 44 + PublicURL string 45 + } 46 + 47 + // DefaultAdminConfig returns sensible defaults 48 + func DefaultAdminConfig() AdminConfig { 49 + return AdminConfig{ 50 + Enabled: false, 51 + } 52 + } 53 + 54 + // AdminSession represents an authenticated admin session 55 + type AdminSession struct { 56 + DID string 57 + Handle string 58 + } 59 + 60 + // AdminUI manages the admin web interface 61 + type AdminUI struct { 62 + pds *pds.HoldPDS 63 + quotaMgr *quota.Manager 64 + clientApp *indigooauth.ClientApp 65 + templates *template.Template 66 + config AdminConfig 67 + 68 + // In-memory session storage (single user, no persistence needed) 69 + sessions map[string]*AdminSession 70 + sessionsMu sync.RWMutex 71 + } 72 + 73 + // adminContextKey is used to store session data in request context 74 + type adminContextKey struct{} 75 + 76 + // NewAdminUI creates a new admin UI instance 77 + func NewAdminUI(ctx context.Context, holdPDS *pds.HoldPDS, quotaMgr *quota.Manager, cfg AdminConfig) (*AdminUI, error) { 78 + if !cfg.Enabled { 79 + return nil, nil 80 + } 81 + 82 + // Validate required config 83 + if cfg.PublicURL == "" { 84 + return nil, fmt.Errorf("PublicURL is required for admin panel") 85 + } 86 + 87 + // Determine OAuth configuration based on URL type 88 + u, err := url.Parse(cfg.PublicURL) 89 + if err != nil { 90 + return nil, fmt.Errorf("invalid PublicURL: %w", err) 91 + } 92 + 93 + // Use in-memory store for OAuth sessions 94 + oauthStore := indigooauth.NewMemStore() 95 + 96 + // Use minimal scopes for admin (only need basic auth, no blob access) 97 + adminScopes := []string{"atproto"} 98 + 99 + var oauthConfig indigooauth.ClientConfig 100 + var redirectURI string 101 + 102 + host := u.Hostname() 103 + if isIPAddress(host) || host == "localhost" || host == "127.0.0.1" { 104 + // Development mode: IP address or localhost - use localhost OAuth config 105 + // Substitute 127.0.0.1 for Docker network IPs 106 + port := u.Port() 107 + if port == "" { 108 + port = "8080" 109 + } 110 + oauthBaseURL := "http://127.0.0.1:" + port 111 + redirectURI = oauthBaseURL + "/admin/auth/oauth/callback" 112 + oauthConfig = indigooauth.NewLocalhostConfig(redirectURI, adminScopes) 113 + 114 + slog.Info("Admin OAuth configured (localhost mode)", 115 + "redirect_uri", redirectURI, 116 + "public_url", cfg.PublicURL) 117 + } else { 118 + // Production mode: real domain - use public client with metadata endpoint 119 + clientID := cfg.PublicURL + "/admin/oauth-client-metadata.json" 120 + redirectURI = cfg.PublicURL + "/admin/auth/oauth/callback" 121 + oauthConfig = indigooauth.NewPublicConfig(clientID, redirectURI, adminScopes) 122 + 123 + slog.Info("Admin OAuth configured (production mode)", 124 + "client_id", clientID, 125 + "redirect_uri", redirectURI) 126 + } 127 + 128 + clientApp := indigooauth.NewClientApp(&oauthConfig, oauthStore) 129 + clientApp.Dir = atproto.GetDirectory() 130 + 131 + // Parse templates 132 + templates, err := parseTemplates() 133 + if err != nil { 134 + return nil, fmt.Errorf("failed to parse templates: %w", err) 135 + } 136 + 137 + ui := &AdminUI{ 138 + pds: holdPDS, 139 + quotaMgr: quotaMgr, 140 + clientApp: clientApp, 141 + templates: templates, 142 + config: cfg, 143 + sessions: make(map[string]*AdminSession), 144 + } 145 + 146 + slog.Info("Admin panel initialized", "publicURL", cfg.PublicURL) 147 + 148 + return ui, nil 149 + } 150 + 151 + // Session management 152 + 153 + func (ui *AdminUI) createSession(did, handle string) string { 154 + b := make([]byte, 32) 155 + rand.Read(b) 156 + token := base64.URLEncoding.EncodeToString(b) 157 + 158 + ui.sessionsMu.Lock() 159 + ui.sessions[token] = &AdminSession{DID: did, Handle: handle} 160 + ui.sessionsMu.Unlock() 161 + 162 + return token 163 + } 164 + 165 + func (ui *AdminUI) getSession(token string) *AdminSession { 166 + ui.sessionsMu.RLock() 167 + defer ui.sessionsMu.RUnlock() 168 + return ui.sessions[token] 169 + } 170 + 171 + func (ui *AdminUI) deleteSession(token string) { 172 + ui.sessionsMu.Lock() 173 + delete(ui.sessions, token) 174 + ui.sessionsMu.Unlock() 175 + } 176 + 177 + // Cookie helpers 178 + 179 + const sessionCookieName = "hold_admin_session" 180 + 181 + func (ui *AdminUI) setSessionCookie(w http.ResponseWriter, r *http.Request, token string) { 182 + secure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" 183 + http.SetCookie(w, &http.Cookie{ 184 + Name: sessionCookieName, 185 + Value: token, 186 + Path: "/admin", 187 + MaxAge: 86400, // 24 hours 188 + HttpOnly: true, 189 + Secure: secure, 190 + SameSite: http.SameSiteLaxMode, 191 + }) 192 + } 193 + 194 + func clearSessionCookie(w http.ResponseWriter) { 195 + http.SetCookie(w, &http.Cookie{ 196 + Name: sessionCookieName, 197 + Value: "", 198 + Path: "/admin", 199 + MaxAge: -1, 200 + HttpOnly: true, 201 + SameSite: http.SameSiteLaxMode, 202 + }) 203 + } 204 + 205 + func getSessionCookie(r *http.Request) (string, bool) { 206 + cookie, err := r.Cookie(sessionCookieName) 207 + if err != nil { 208 + return "", false 209 + } 210 + return cookie.Value, true 211 + } 212 + 213 + // parseTemplates loads and parses all HTML templates 214 + func parseTemplates() (*template.Template, error) { 215 + funcMap := template.FuncMap{ 216 + "truncate": func(s string, n int) string { 217 + if len(s) <= n { 218 + return s 219 + } 220 + return s[:n] + "..." 221 + }, 222 + "formatBytes": formatHumanBytes, 223 + "formatTime": func(t time.Time) string { 224 + return t.Format("2006-01-02 15:04") 225 + }, 226 + "contains": func(slice []string, item string) bool { 227 + for _, s := range slice { 228 + if s == item { 229 + return true 230 + } 231 + } 232 + return false 233 + }, 234 + } 235 + 236 + tmpl := template.New("").Funcs(funcMap) 237 + 238 + err := fs.WalkDir(templatesFS, "templates", func(path string, d fs.DirEntry, err error) error { 239 + if err != nil { 240 + return err 241 + } 242 + if d.IsDir() { 243 + return nil 244 + } 245 + if len(path) < 5 || path[len(path)-5:] != ".html" { 246 + return nil 247 + } 248 + 249 + content, err := templatesFS.ReadFile(path) 250 + if err != nil { 251 + return fmt.Errorf("failed to read template %s: %w", path, err) 252 + } 253 + 254 + name := path[len("templates/"):] 255 + _, err = tmpl.New(name).Parse(string(content)) 256 + if err != nil { 257 + return fmt.Errorf("failed to parse template %s: %w", path, err) 258 + } 259 + 260 + return nil 261 + }) 262 + 263 + if err != nil { 264 + return nil, err 265 + } 266 + 267 + return tmpl, nil 268 + } 269 + 270 + // formatHumanBytes formats bytes as human-readable string 271 + func formatHumanBytes(bytes int64) string { 272 + const unit = 1024 273 + if bytes < unit { 274 + return fmt.Sprintf("%d B", bytes) 275 + } 276 + div, exp := int64(unit), 0 277 + for n := bytes / unit; n >= unit; n /= unit { 278 + div *= unit 279 + exp++ 280 + } 281 + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) 282 + } 283 + 284 + // isIPAddress returns true if the host is an IP address (not a domain name) 285 + func isIPAddress(host string) bool { 286 + return net.ParseIP(host) != nil 287 + } 288 + 289 + // RegisterRoutes registers all admin routes with the router 290 + func (ui *AdminUI) RegisterRoutes(r chi.Router) { 291 + // Static files (public) 292 + staticSub, _ := fs.Sub(staticFS, "static") 293 + r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", http.FileServer(http.FS(staticSub)))) 294 + 295 + // OAuth client metadata endpoint (required for production OAuth) 296 + r.Get("/admin/oauth-client-metadata.json", ui.handleClientMetadata) 297 + 298 + // Public auth routes 299 + r.Get("/admin/auth/login", ui.handleLogin) 300 + r.Get("/admin/auth/oauth/authorize", ui.handleAuthorize) 301 + r.Get("/admin/auth/oauth/callback", ui.handleCallback) 302 + 303 + // Protected routes (require owner) 304 + r.Group(func(r chi.Router) { 305 + r.Use(ui.requireOwner) 306 + 307 + // Dashboard 308 + r.Get("/admin", ui.handleDashboard) 309 + r.Get("/admin/", ui.handleDashboard) 310 + 311 + // Crew management 312 + r.Get("/admin/crew", ui.handleCrewList) 313 + r.Get("/admin/crew/add", ui.handleCrewAddForm) 314 + r.Post("/admin/crew/add", ui.handleCrewAdd) 315 + r.Get("/admin/crew/{rkey}", ui.handleCrewEditForm) 316 + r.Post("/admin/crew/{rkey}/update", ui.handleCrewUpdate) 317 + r.Post("/admin/crew/{rkey}/delete", ui.handleCrewDelete) 318 + 319 + // Settings 320 + r.Get("/admin/settings", ui.handleSettings) 321 + r.Post("/admin/settings/update", ui.handleSettingsUpdate) 322 + 323 + // API endpoints (for HTMX) 324 + r.Get("/admin/api/stats", ui.handleStatsAPI) 325 + r.Get("/admin/api/top-users", ui.handleTopUsersAPI) 326 + 327 + // Logout 328 + r.Get("/admin/auth/logout", ui.handleLogout) 329 + }) 330 + } 331 + 332 + // handleClientMetadata serves the OAuth client metadata for production deployments 333 + func (ui *AdminUI) handleClientMetadata(w http.ResponseWriter, r *http.Request) { 334 + metadata := ui.clientApp.Config.ClientMetadata() 335 + 336 + // Set client name for display in OAuth consent screen 337 + clientName := "Hold Admin Panel" 338 + metadata.ClientName = &clientName 339 + metadata.ClientURI = &ui.config.PublicURL 340 + 341 + w.Header().Set("Content-Type", "application/json") 342 + w.Header().Set("Cache-Control", "public, max-age=3600") 343 + json.NewEncoder(w).Encode(metadata) 344 + } 345 + 346 + // Close cleans up resources (no-op now, but keeps interface consistent) 347 + func (ui *AdminUI) Close() error { 348 + return nil 349 + }
+115
pkg/hold/admin/auth.go
···
··· 1 + package admin 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "net/http" 7 + ) 8 + 9 + // requireOwner middleware ensures the request is from the hold owner 10 + func (ui *AdminUI) requireOwner(next http.Handler) http.Handler { 11 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 + // Get session cookie 13 + token, ok := getSessionCookie(r) 14 + if !ok { 15 + http.Redirect(w, r, "/admin/auth/login?return_to="+r.URL.Path, http.StatusFound) 16 + return 17 + } 18 + 19 + // Validate session 20 + session := ui.getSession(token) 21 + if session == nil { 22 + clearSessionCookie(w) 23 + http.Redirect(w, r, "/admin/auth/login", http.StatusFound) 24 + return 25 + } 26 + 27 + // Double-check DID still matches captain.Owner 28 + _, captain, err := ui.pds.GetCaptainRecord(r.Context()) 29 + if err != nil { 30 + slog.Error("Failed to get captain record for admin auth", "error", err) 31 + http.Error(w, "Failed to verify ownership", http.StatusInternalServerError) 32 + return 33 + } 34 + 35 + if session.DID != captain.Owner { 36 + slog.Warn("Admin session DID doesn't match captain owner", 37 + "sessionDID", session.DID, 38 + "captainOwner", captain.Owner) 39 + ui.deleteSession(token) 40 + clearSessionCookie(w) 41 + http.Error(w, "Access denied: ownership verification failed", http.StatusForbidden) 42 + return 43 + } 44 + 45 + // Add session to context for handlers 46 + ctx := context.WithValue(r.Context(), adminContextKey{}, session) 47 + next.ServeHTTP(w, r.WithContext(ctx)) 48 + }) 49 + } 50 + 51 + // getSessionFromContext retrieves the admin session from context 52 + func getSessionFromContext(ctx context.Context) *AdminSession { 53 + session, ok := ctx.Value(adminContextKey{}).(*AdminSession) 54 + if !ok { 55 + return nil 56 + } 57 + return session 58 + } 59 + 60 + // PageData contains common data for all admin pages 61 + type PageData struct { 62 + Title string 63 + ActivePage string 64 + User *AdminSession 65 + HoldDID string 66 + Flash *Flash 67 + } 68 + 69 + // Flash represents a flash message 70 + type Flash struct { 71 + Category string // "success", "error", "warning", "info" 72 + Message string 73 + } 74 + 75 + // newPageData creates PageData with common values 76 + func (ui *AdminUI) newPageData(r *http.Request, title, activePage string) PageData { 77 + session := getSessionFromContext(r.Context()) 78 + flash := getFlash(r, ui) 79 + 80 + return PageData{ 81 + Title: title, 82 + ActivePage: activePage, 83 + User: session, 84 + HoldDID: ui.pds.DID(), 85 + Flash: flash, 86 + } 87 + } 88 + 89 + // renderTemplate renders a template with the given data 90 + func (ui *AdminUI) renderTemplate(w http.ResponseWriter, name string, data interface{}) { 91 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 92 + 93 + if err := ui.templates.ExecuteTemplate(w, name, data); err != nil { 94 + slog.Error("Failed to render template", "template", name, "error", err) 95 + http.Error(w, "Internal server error", http.StatusInternalServerError) 96 + } 97 + } 98 + 99 + // renderError renders an error page 100 + func (ui *AdminUI) renderError(w http.ResponseWriter, r *http.Request, message string, statusCode int) { 101 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 102 + w.WriteHeader(statusCode) 103 + 104 + data := struct { 105 + PageData 106 + Error string 107 + }{ 108 + PageData: ui.newPageData(r, "Error", ""), 109 + Error: message, 110 + } 111 + 112 + if err := ui.templates.ExecuteTemplate(w, "pages/error.html", data); err != nil { 113 + http.Error(w, message, statusCode) 114 + } 115 + }
+67
pkg/hold/admin/flash.go
···
··· 1 + package admin 2 + 3 + import ( 4 + "encoding/base64" 5 + "encoding/json" 6 + "net/http" 7 + ) 8 + 9 + const flashCookieName = "hold_admin_flash" 10 + 11 + // setFlash sets a flash message cookie 12 + func setFlash(w http.ResponseWriter, r *http.Request, category, message string) { 13 + flash := Flash{ 14 + Category: category, 15 + Message: message, 16 + } 17 + 18 + data, err := json.Marshal(flash) 19 + if err != nil { 20 + return 21 + } 22 + 23 + encoded := base64.URLEncoding.EncodeToString(data) 24 + secure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" 25 + 26 + http.SetCookie(w, &http.Cookie{ 27 + Name: flashCookieName, 28 + Value: encoded, 29 + Path: "/admin", 30 + MaxAge: 60, // 1 minute - should be consumed on next page load 31 + HttpOnly: true, 32 + Secure: secure, 33 + SameSite: http.SameSiteLaxMode, 34 + }) 35 + } 36 + 37 + // getFlash retrieves and clears the flash message 38 + func getFlash(r *http.Request, ui *AdminUI) *Flash { 39 + cookie, err := r.Cookie(flashCookieName) 40 + if err != nil { 41 + return nil 42 + } 43 + 44 + data, err := base64.URLEncoding.DecodeString(cookie.Value) 45 + if err != nil { 46 + return nil 47 + } 48 + 49 + var flash Flash 50 + if err := json.Unmarshal(data, &flash); err != nil { 51 + return nil 52 + } 53 + 54 + return &flash 55 + } 56 + 57 + // clearFlash clears the flash cookie (called after displaying) 58 + func clearFlash(w http.ResponseWriter) { 59 + http.SetCookie(w, &http.Cookie{ 60 + Name: flashCookieName, 61 + Value: "", 62 + Path: "/admin", 63 + MaxAge: -1, 64 + HttpOnly: true, 65 + SameSite: http.SameSiteLaxMode, 66 + }) 67 + }
+196
pkg/hold/admin/handlers.go
···
··· 1 + package admin 2 + 3 + import ( 4 + "encoding/json" 5 + "log/slog" 6 + "net/http" 7 + "sort" 8 + "strconv" 9 + 10 + "atcr.io/pkg/atproto" 11 + ) 12 + 13 + // DashboardStats contains dashboard statistics 14 + type DashboardStats struct { 15 + TotalCrewMembers int 16 + TierDistribution map[string]int 17 + } 18 + 19 + // handleDashboard renders the main dashboard 20 + func (ui *AdminUI) handleDashboard(w http.ResponseWriter, r *http.Request) { 21 + ctx := r.Context() 22 + 23 + // Clear flash after reading 24 + defer clearFlash(w) 25 + 26 + // Get crew members 27 + crew, err := ui.pds.ListCrewMembers(ctx) 28 + if err != nil { 29 + slog.Warn("Failed to list crew members for dashboard", "error", err) 30 + } 31 + 32 + stats := DashboardStats{ 33 + TotalCrewMembers: len(crew), 34 + TierDistribution: make(map[string]int), 35 + } 36 + 37 + // Count tier distribution 38 + defaultTier := "default" 39 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 40 + defaultTier = ui.quotaMgr.GetDefaultTier() 41 + } 42 + 43 + for _, member := range crew { 44 + tier := member.Record.Tier 45 + if tier == "" { 46 + tier = defaultTier 47 + } 48 + stats.TierDistribution[tier]++ 49 + } 50 + 51 + data := struct { 52 + PageData 53 + Stats DashboardStats 54 + }{ 55 + PageData: ui.newPageData(r, "Dashboard", "dashboard"), 56 + Stats: stats, 57 + } 58 + 59 + ui.renderTemplate(w, "pages/dashboard.html", data) 60 + } 61 + 62 + // StorageStats contains storage statistics 63 + type StorageStats struct { 64 + TotalBlobs int `json:"totalBlobs"` 65 + TotalSize int64 `json:"totalSize"` 66 + TotalHuman string `json:"totalHuman"` 67 + UniqueDigests int `json:"uniqueDigests"` 68 + } 69 + 70 + // handleStatsAPI returns storage statistics (for HTMX lazy loading) 71 + func (ui *AdminUI) handleStatsAPI(w http.ResponseWriter, r *http.Request) { 72 + ctx := r.Context() 73 + 74 + // Get layer record count from the index 75 + recordsIndex := ui.pds.RecordsIndex() 76 + if recordsIndex == nil { 77 + http.Error(w, "Records index not available", http.StatusInternalServerError) 78 + return 79 + } 80 + 81 + // Count total layer records 82 + totalBlobs, err := recordsIndex.Count(atproto.LayerCollection) 83 + if err != nil { 84 + slog.Error("Failed to count layer records", "error", err) 85 + http.Error(w, "Failed to load stats", http.StatusInternalServerError) 86 + return 87 + } 88 + 89 + // Calculate total storage by summing quota for all crew members 90 + var totalSize int64 91 + uniqueDigests := 0 92 + 93 + crew, err := ui.pds.ListCrewMembers(ctx) 94 + if err != nil { 95 + slog.Warn("Failed to list crew for stats", "error", err) 96 + } else { 97 + // Get usage for each crew member 98 + for _, member := range crew { 99 + quotaStats, err := ui.pds.GetQuotaForUser(ctx, member.Record.Member) 100 + if err != nil { 101 + continue 102 + } 103 + totalSize += quotaStats.TotalSize 104 + uniqueDigests += quotaStats.UniqueBlobs 105 + } 106 + } 107 + 108 + stats := StorageStats{ 109 + TotalBlobs: totalBlobs, 110 + TotalSize: totalSize, 111 + TotalHuman: formatHumanBytes(totalSize), 112 + UniqueDigests: uniqueDigests, 113 + } 114 + 115 + // If HTMX request, return HTML partial 116 + if r.Header.Get("HX-Request") == "true" { 117 + data := struct { 118 + Stats StorageStats 119 + }{Stats: stats} 120 + ui.renderTemplate(w, "partials/usage_stats.html", data) 121 + return 122 + } 123 + 124 + // Otherwise return JSON 125 + w.Header().Set("Content-Type", "application/json") 126 + json.NewEncoder(w).Encode(stats) 127 + } 128 + 129 + // UserUsage represents storage usage for a user 130 + type UserUsage struct { 131 + DID string `json:"did"` 132 + Handle string `json:"handle"` 133 + Usage int64 `json:"usage"` 134 + UsageHuman string `json:"usageHuman"` 135 + BlobCount int `json:"blobCount"` 136 + } 137 + 138 + // handleTopUsersAPI returns top users by storage (for HTMX lazy loading) 139 + func (ui *AdminUI) handleTopUsersAPI(w http.ResponseWriter, r *http.Request) { 140 + ctx := r.Context() 141 + 142 + limit := 10 143 + if l := r.URL.Query().Get("limit"); l != "" { 144 + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { 145 + limit = parsed 146 + } 147 + } 148 + 149 + // Get all crew members and their usage 150 + crew, err := ui.pds.ListCrewMembers(ctx) 151 + if err != nil { 152 + slog.Error("Failed to list crew members for top users", "error", err) 153 + http.Error(w, "Failed to load top users", http.StatusInternalServerError) 154 + return 155 + } 156 + 157 + var users []UserUsage 158 + for _, member := range crew { 159 + quotaStats, err := ui.pds.GetQuotaForUser(ctx, member.Record.Member) 160 + if err != nil { 161 + slog.Warn("Failed to get quota for user", "did", member.Record.Member, "error", err) 162 + continue 163 + } 164 + 165 + users = append(users, UserUsage{ 166 + DID: member.Record.Member, 167 + Handle: resolveHandle(ctx, member.Record.Member), 168 + Usage: quotaStats.TotalSize, 169 + UsageHuman: formatHumanBytes(quotaStats.TotalSize), 170 + BlobCount: quotaStats.UniqueBlobs, 171 + }) 172 + } 173 + 174 + // Sort by usage (highest first) 175 + sort.Slice(users, func(i, j int) bool { 176 + return users[i].Usage > users[j].Usage 177 + }) 178 + 179 + // Limit results 180 + if len(users) > limit { 181 + users = users[:limit] 182 + } 183 + 184 + // If HTMX request, return HTML partial 185 + if r.Header.Get("HX-Request") == "true" { 186 + data := struct { 187 + Users []UserUsage 188 + }{Users: users} 189 + ui.renderTemplate(w, "partials/top_users.html", data) 190 + return 191 + } 192 + 193 + // Otherwise return JSON 194 + w.Header().Set("Content-Type", "application/json") 195 + json.NewEncoder(w).Encode(users) 196 + }
+135
pkg/hold/admin/handlers_auth.go
···
··· 1 + package admin 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "strings" 7 + 8 + "atcr.io/pkg/atproto" 9 + ) 10 + 11 + // handleLogin renders the login page 12 + func (ui *AdminUI) handleLogin(w http.ResponseWriter, r *http.Request) { 13 + // If already logged in, redirect to dashboard 14 + if token, ok := getSessionCookie(r); ok { 15 + if session := ui.getSession(token); session != nil { 16 + // Verify still owner 17 + if _, captain, err := ui.pds.GetCaptainRecord(r.Context()); err == nil && session.DID == captain.Owner { 18 + http.Redirect(w, r, "/admin", http.StatusFound) 19 + return 20 + } 21 + } 22 + } 23 + 24 + returnTo := r.URL.Query().Get("return_to") 25 + if returnTo == "" { 26 + returnTo = "/admin" 27 + } 28 + 29 + data := struct { 30 + PageData 31 + ReturnTo string 32 + Error string 33 + }{ 34 + PageData: PageData{ 35 + Title: "Login", 36 + ActivePage: "login", 37 + HoldDID: ui.pds.DID(), 38 + }, 39 + ReturnTo: returnTo, 40 + Error: r.URL.Query().Get("error"), 41 + } 42 + 43 + ui.renderTemplate(w, "pages/login.html", data) 44 + } 45 + 46 + // handleAuthorize starts the OAuth flow 47 + func (ui *AdminUI) handleAuthorize(w http.ResponseWriter, r *http.Request) { 48 + handle := strings.TrimSpace(r.URL.Query().Get("handle")) 49 + if handle == "" { 50 + http.Redirect(w, r, "/admin/auth/login?error=Handle+is+required", http.StatusFound) 51 + return 52 + } 53 + 54 + // Normalize handle 55 + handle = strings.TrimPrefix(handle, "@") 56 + 57 + // Resolve handle to DID 58 + did, _, _, err := atproto.ResolveIdentity(r.Context(), handle) 59 + if err != nil { 60 + slog.Warn("Failed to resolve handle for admin login", "handle", handle, "error", err) 61 + http.Redirect(w, r, "/admin/auth/login?error=Could+not+resolve+handle", http.StatusFound) 62 + return 63 + } 64 + 65 + slog.Info("Starting admin OAuth flow", "handle", handle, "did", did) 66 + 67 + // Start OAuth flow 68 + authURL, err := ui.clientApp.StartAuthFlow(r.Context(), did) 69 + if err != nil { 70 + slog.Error("Failed to start OAuth flow", "error", err) 71 + http.Redirect(w, r, "/admin/auth/login?error=OAuth+initialization+failed", http.StatusFound) 72 + return 73 + } 74 + 75 + http.Redirect(w, r, authURL, http.StatusFound) 76 + } 77 + 78 + // handleCallback processes the OAuth callback 79 + func (ui *AdminUI) handleCallback(w http.ResponseWriter, r *http.Request) { 80 + ctx := r.Context() 81 + 82 + // Process OAuth callback 83 + sessionData, err := ui.clientApp.ProcessCallback(ctx, r.URL.Query()) 84 + if err != nil { 85 + slog.Error("OAuth callback failed", "error", err) 86 + http.Redirect(w, r, "/admin/auth/login?error=OAuth+authentication+failed", http.StatusFound) 87 + return 88 + } 89 + 90 + did := sessionData.AccountDID.String() 91 + 92 + // Resolve handle from DID 93 + _, handle, _, err := atproto.ResolveIdentity(ctx, did) 94 + if err != nil { 95 + slog.Warn("Failed to resolve handle from DID", "did", did, "error", err) 96 + handle = did // Fallback to DID 97 + } 98 + 99 + slog.Info("OAuth callback successful", "did", did, "handle", handle) 100 + 101 + // Get captain record to check owner 102 + _, captain, err := ui.pds.GetCaptainRecord(ctx) 103 + if err != nil { 104 + slog.Error("Failed to get captain record during OAuth callback", "error", err) 105 + http.Redirect(w, r, "/admin/auth/login?error=Failed+to+verify+ownership", http.StatusFound) 106 + return 107 + } 108 + 109 + // CRITICAL: Only allow the hold owner 110 + if did != captain.Owner { 111 + slog.Warn("Non-owner attempted admin access", 112 + "did", did, 113 + "handle", handle, 114 + "owner", captain.Owner) 115 + http.Redirect(w, r, "/admin/auth/login?error=Access+denied:+Only+the+hold+owner+can+access+the+admin+panel", http.StatusFound) 116 + return 117 + } 118 + 119 + // Create session and set cookie 120 + token := ui.createSession(did, handle) 121 + ui.setSessionCookie(w, r, token) 122 + 123 + slog.Info("Admin login successful", "did", did, "handle", handle) 124 + 125 + http.Redirect(w, r, "/admin", http.StatusFound) 126 + } 127 + 128 + // handleLogout clears the session and redirects to login 129 + func (ui *AdminUI) handleLogout(w http.ResponseWriter, r *http.Request) { 130 + if token, ok := getSessionCookie(r); ok { 131 + ui.deleteSession(token) 132 + } 133 + clearSessionCookie(w) 134 + http.Redirect(w, r, "/admin/auth/login", http.StatusFound) 135 + }
+422
pkg/hold/admin/handlers_crew.go
···
··· 1 + package admin 2 + 3 + import ( 4 + "context" 5 + "log/slog" 6 + "net/http" 7 + "sort" 8 + "strings" 9 + "time" 10 + 11 + "atcr.io/pkg/atproto" 12 + "github.com/go-chi/chi/v5" 13 + ) 14 + 15 + // CrewMemberView represents a crew member for display 16 + type CrewMemberView struct { 17 + RKey string 18 + DID string 19 + Handle string 20 + Role string 21 + Permissions []string 22 + Tier string 23 + TierLimit string 24 + CurrentUsage int64 25 + UsageHuman string 26 + UsagePercent int 27 + AddedAt time.Time 28 + } 29 + 30 + // resolveHandle attempts to resolve a DID to a handle 31 + // Returns empty string if resolution fails 32 + func resolveHandle(ctx context.Context, did string) string { 33 + _, handle, _, err := atproto.ResolveIdentity(ctx, did) 34 + if err != nil { 35 + slog.Debug("Failed to resolve handle for DID", "did", did, "error", err) 36 + return "" // Empty string means no handle resolved 37 + } 38 + // If handle is the same as the DID (fallback), treat as no handle 39 + if handle == did { 40 + return "" 41 + } 42 + return handle 43 + } 44 + 45 + // TierOption represents a tier choice in forms 46 + type TierOption struct { 47 + Key string 48 + Name string 49 + Limit string 50 + } 51 + 52 + // handleCrewList displays all crew members 53 + func (ui *AdminUI) handleCrewList(w http.ResponseWriter, r *http.Request) { 54 + ctx := r.Context() 55 + defer clearFlash(w) 56 + 57 + crew, err := ui.pds.ListCrewMembers(ctx) 58 + if err != nil { 59 + ui.renderError(w, r, "Failed to list crew: "+err.Error(), http.StatusInternalServerError) 60 + return 61 + } 62 + 63 + // Get usage data for each crew member using GetQuotaForUser 64 + var crewViews []CrewMemberView 65 + userUsage := make(map[string]int64) 66 + 67 + // Pre-fetch usage for all crew members 68 + for _, member := range crew { 69 + quotaStats, err := ui.pds.GetQuotaForUser(ctx, member.Record.Member) 70 + if err != nil { 71 + slog.Warn("Failed to get quota for crew member", "did", member.Record.Member, "error", err) 72 + continue 73 + } 74 + userUsage[member.Record.Member] = quotaStats.TotalSize 75 + } 76 + 77 + defaultTier := "default" 78 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 79 + defaultTier = ui.quotaMgr.GetDefaultTier() 80 + } 81 + 82 + for _, member := range crew { 83 + tier := member.Record.Tier 84 + if tier == "" { 85 + tier = defaultTier 86 + } 87 + 88 + view := CrewMemberView{ 89 + RKey: member.Rkey, 90 + DID: member.Record.Member, 91 + Handle: resolveHandle(ctx, member.Record.Member), 92 + Role: member.Record.Role, 93 + Permissions: member.Record.Permissions, 94 + Tier: tier, 95 + AddedAt: parseTime(member.Record.AddedAt), 96 + } 97 + 98 + // Get tier limit 99 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 100 + if limit := ui.quotaMgr.GetTierLimit(tier); limit != nil { 101 + view.TierLimit = formatHumanBytes(*limit) 102 + if *limit > 0 { 103 + view.UsagePercent = int(float64(userUsage[view.DID]) / float64(*limit) * 100) 104 + } 105 + } else { 106 + view.TierLimit = "Unlimited" 107 + } 108 + } else { 109 + view.TierLimit = "Unlimited" 110 + } 111 + 112 + view.CurrentUsage = userUsage[view.DID] 113 + view.UsageHuman = formatHumanBytes(view.CurrentUsage) 114 + 115 + crewViews = append(crewViews, view) 116 + } 117 + 118 + // Sort by usage (highest first) 119 + sort.Slice(crewViews, func(i, j int) bool { 120 + return crewViews[i].CurrentUsage > crewViews[j].CurrentUsage 121 + }) 122 + 123 + data := struct { 124 + PageData 125 + Crew []CrewMemberView 126 + Tiers []TierOption 127 + }{ 128 + PageData: ui.newPageData(r, "Crew Management", "crew"), 129 + Crew: crewViews, 130 + Tiers: ui.getTierOptions(), 131 + } 132 + 133 + ui.renderTemplate(w, "pages/crew.html", data) 134 + } 135 + 136 + // handleCrewAddForm displays the add crew form 137 + func (ui *AdminUI) handleCrewAddForm(w http.ResponseWriter, r *http.Request) { 138 + defer clearFlash(w) 139 + 140 + data := struct { 141 + PageData 142 + Tiers []TierOption 143 + }{ 144 + PageData: ui.newPageData(r, "Add Crew Member", "crew"), 145 + Tiers: ui.getTierOptions(), 146 + } 147 + 148 + ui.renderTemplate(w, "pages/crew_add.html", data) 149 + } 150 + 151 + // handleCrewAdd processes adding a new crew member 152 + func (ui *AdminUI) handleCrewAdd(w http.ResponseWriter, r *http.Request) { 153 + ctx := r.Context() 154 + 155 + if err := r.ParseForm(); err != nil { 156 + setFlash(w, r, "error", "Invalid form data") 157 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 158 + return 159 + } 160 + 161 + did := strings.TrimSpace(r.FormValue("did")) 162 + role := r.FormValue("role") 163 + tier := r.FormValue("tier") 164 + 165 + // Parse permissions checkboxes 166 + var permissions []string 167 + if r.FormValue("perm_read") == "on" { 168 + permissions = append(permissions, "blob:read") 169 + } 170 + if r.FormValue("perm_write") == "on" { 171 + permissions = append(permissions, "blob:write") 172 + } 173 + if r.FormValue("perm_admin") == "on" { 174 + permissions = append(permissions, "crew:admin") 175 + } 176 + 177 + // Validate DID format 178 + if !strings.HasPrefix(did, "did:") { 179 + setFlash(w, r, "error", "Invalid DID format (must start with did:)") 180 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 181 + return 182 + } 183 + 184 + // Default role 185 + if role == "" { 186 + role = "member" 187 + } 188 + 189 + // Add crew member 190 + _, err := ui.pds.AddCrewMember(ctx, did, role, permissions) 191 + if err != nil { 192 + slog.Error("Failed to add crew member", "did", did, "error", err) 193 + setFlash(w, r, "error", "Failed to add crew member: "+err.Error()) 194 + http.Redirect(w, r, "/admin/crew/add", http.StatusFound) 195 + return 196 + } 197 + 198 + // Update tier if specified and different from default 199 + defaultTier := "default" 200 + if ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() { 201 + defaultTier = ui.quotaMgr.GetDefaultTier() 202 + } 203 + 204 + if tier != "" && tier != defaultTier { 205 + if err := ui.pds.UpdateCrewMemberTier(ctx, did, tier); err != nil { 206 + slog.Warn("Failed to set tier for new crew member", "did", did, "tier", tier, "error", err) 207 + } 208 + } 209 + 210 + session := getSessionFromContext(ctx) 211 + slog.Info("Crew member added via admin panel", 212 + "did", did, 213 + "role", role, 214 + "permissions", permissions, 215 + "by", session.DID) 216 + 217 + setFlash(w, r, "success", "Crew member added successfully") 218 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 219 + } 220 + 221 + // handleCrewEditForm displays the edit crew form 222 + func (ui *AdminUI) handleCrewEditForm(w http.ResponseWriter, r *http.Request) { 223 + ctx := r.Context() 224 + rkey := chi.URLParam(r, "rkey") 225 + defer clearFlash(w) 226 + 227 + _, member, err := ui.pds.GetCrewMember(ctx, rkey) 228 + if err != nil { 229 + setFlash(w, r, "error", "Crew member not found") 230 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 231 + return 232 + } 233 + 234 + // Check if this is the owner 235 + _, captain, _ := ui.pds.GetCaptainRecord(ctx) 236 + isOwner := captain != nil && member.Member == captain.Owner 237 + 238 + // Resolve handle for display 239 + memberHandle := resolveHandle(ctx, member.Member) 240 + 241 + data := struct { 242 + PageData 243 + Member *atproto.CrewRecord 244 + MemberHandle string 245 + RKey string 246 + IsOwner bool 247 + Tiers []TierOption 248 + }{ 249 + PageData: ui.newPageData(r, "Edit Crew Member", "crew"), 250 + Member: member, 251 + MemberHandle: memberHandle, 252 + RKey: rkey, 253 + IsOwner: isOwner, 254 + Tiers: ui.getTierOptions(), 255 + } 256 + 257 + ui.renderTemplate(w, "pages/crew_edit.html", data) 258 + } 259 + 260 + // handleCrewUpdate processes updating a crew member 261 + func (ui *AdminUI) handleCrewUpdate(w http.ResponseWriter, r *http.Request) { 262 + ctx := r.Context() 263 + rkey := chi.URLParam(r, "rkey") 264 + 265 + if err := r.ParseForm(); err != nil { 266 + setFlash(w, r, "error", "Invalid form data") 267 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 268 + return 269 + } 270 + 271 + // Get current crew member 272 + _, current, err := ui.pds.GetCrewMember(ctx, rkey) 273 + if err != nil { 274 + setFlash(w, r, "error", "Crew member not found") 275 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 276 + return 277 + } 278 + 279 + // Parse new values 280 + role := r.FormValue("role") 281 + tier := r.FormValue("tier") 282 + 283 + var permissions []string 284 + if r.FormValue("perm_read") == "on" { 285 + permissions = append(permissions, "blob:read") 286 + } 287 + if r.FormValue("perm_write") == "on" { 288 + permissions = append(permissions, "blob:write") 289 + } 290 + if r.FormValue("perm_admin") == "on" { 291 + permissions = append(permissions, "crew:admin") 292 + } 293 + 294 + // Update tier if changed 295 + if tier != current.Tier { 296 + if err := ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier); err != nil { 297 + setFlash(w, r, "error", "Failed to update tier: "+err.Error()) 298 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 299 + return 300 + } 301 + } 302 + 303 + // For role/permissions changes, need to delete and recreate 304 + // (ATProto records are immutable, updates require delete+create) 305 + if role != current.Role || !slicesEqual(permissions, current.Permissions) { 306 + // Delete old record 307 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 308 + setFlash(w, r, "error", "Failed to update: "+err.Error()) 309 + http.Redirect(w, r, "/admin/crew/"+rkey, http.StatusFound) 310 + return 311 + } 312 + 313 + // Create new record with updated values 314 + if _, err := ui.pds.AddCrewMember(ctx, current.Member, role, permissions); err != nil { 315 + setFlash(w, r, "error", "Failed to recreate crew record: "+err.Error()) 316 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 317 + return 318 + } 319 + 320 + // Re-apply tier to new record 321 + if tier != "" { 322 + ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier) 323 + } 324 + } 325 + 326 + session := getSessionFromContext(ctx) 327 + slog.Info("Crew member updated via admin panel", 328 + "did", current.Member, 329 + "role", role, 330 + "permissions", permissions, 331 + "by", session.DID) 332 + 333 + setFlash(w, r, "success", "Crew member updated successfully") 334 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 335 + } 336 + 337 + // handleCrewDelete removes a crew member 338 + func (ui *AdminUI) handleCrewDelete(w http.ResponseWriter, r *http.Request) { 339 + ctx := r.Context() 340 + rkey := chi.URLParam(r, "rkey") 341 + 342 + // Get crew member to log who was deleted 343 + _, member, err := ui.pds.GetCrewMember(ctx, rkey) 344 + if err != nil { 345 + setFlash(w, r, "error", "Crew member not found") 346 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 347 + return 348 + } 349 + 350 + // Prevent deleting self (captain) 351 + session := getSessionFromContext(ctx) 352 + if member.Member == session.DID { 353 + setFlash(w, r, "error", "Cannot remove yourself from crew") 354 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 355 + return 356 + } 357 + 358 + // Delete 359 + if err := ui.pds.RemoveCrewMember(ctx, rkey); err != nil { 360 + setFlash(w, r, "error", "Failed to remove crew member: "+err.Error()) 361 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 362 + return 363 + } 364 + 365 + slog.Info("Crew member removed via admin panel", "did", member.Member, "by", session.DID) 366 + 367 + // For HTMX requests, return empty response (row will be removed) 368 + if r.Header.Get("HX-Request") == "true" { 369 + w.WriteHeader(http.StatusOK) 370 + return 371 + } 372 + 373 + setFlash(w, r, "success", "Crew member removed") 374 + http.Redirect(w, r, "/admin/crew", http.StatusFound) 375 + } 376 + 377 + // getTierOptions returns available tier options for forms 378 + func (ui *AdminUI) getTierOptions() []TierOption { 379 + if ui.quotaMgr == nil || !ui.quotaMgr.IsEnabled() { 380 + return []TierOption{{Key: "default", Name: "Default", Limit: "Unlimited"}} 381 + } 382 + 383 + tiers := ui.quotaMgr.ListTiers() 384 + options := make([]TierOption, 0, len(tiers)) 385 + 386 + for _, t := range tiers { 387 + limit := "Unlimited" 388 + if t.Limit != nil { 389 + limit = formatHumanBytes(*t.Limit) 390 + } 391 + options = append(options, TierOption{ 392 + Key: t.Key, 393 + Name: t.Key, 394 + Limit: limit, 395 + }) 396 + } 397 + 398 + return options 399 + } 400 + 401 + // slicesEqual checks if two string slices contain the same elements 402 + func slicesEqual(a, b []string) bool { 403 + if len(a) != len(b) { 404 + return false 405 + } 406 + aMap := make(map[string]bool) 407 + for _, v := range a { 408 + aMap[v] = true 409 + } 410 + for _, v := range b { 411 + if !aMap[v] { 412 + return false 413 + } 414 + } 415 + return true 416 + } 417 + 418 + // parseTime parses an RFC3339 timestamp 419 + func parseTime(s string) time.Time { 420 + t, _ := time.Parse(time.RFC3339, s) 421 + return t 422 + }
+91
pkg/hold/admin/handlers_settings.go
···
··· 1 + package admin 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + ) 7 + 8 + // handleSettings displays the settings page 9 + func (ui *AdminUI) handleSettings(w http.ResponseWriter, r *http.Request) { 10 + ctx := r.Context() 11 + defer clearFlash(w) 12 + 13 + _, captain, err := ui.pds.GetCaptainRecord(ctx) 14 + if err != nil { 15 + ui.renderError(w, r, "Failed to load settings: "+err.Error(), http.StatusInternalServerError) 16 + return 17 + } 18 + 19 + // Resolve owner handle 20 + ownerHandle := resolveHandle(ctx, captain.Owner) 21 + 22 + // Get quota info 23 + quotasEnabled := ui.quotaMgr != nil && ui.quotaMgr.IsEnabled() 24 + tierCount := 0 25 + defaultTier := "" 26 + if quotasEnabled { 27 + tierCount = ui.quotaMgr.TierCount() 28 + defaultTier = ui.quotaMgr.GetDefaultTier() 29 + } 30 + 31 + data := struct { 32 + PageData 33 + Settings struct { 34 + Public bool 35 + AllowAllCrew bool 36 + EnableBlueskyPosts bool 37 + OwnerDID string 38 + OwnerHandle string 39 + HoldDID string 40 + QuotasEnabled bool 41 + TierCount int 42 + DefaultTier string 43 + } 44 + }{ 45 + PageData: ui.newPageData(r, "Settings", "settings"), 46 + } 47 + data.Settings.Public = captain.Public 48 + data.Settings.AllowAllCrew = captain.AllowAllCrew 49 + data.Settings.EnableBlueskyPosts = captain.EnableBlueskyPosts 50 + data.Settings.OwnerDID = captain.Owner 51 + data.Settings.OwnerHandle = ownerHandle 52 + data.Settings.HoldDID = ui.pds.DID() 53 + data.Settings.QuotasEnabled = quotasEnabled 54 + data.Settings.TierCount = tierCount 55 + data.Settings.DefaultTier = defaultTier 56 + 57 + ui.renderTemplate(w, "pages/settings.html", data) 58 + } 59 + 60 + // handleSettingsUpdate processes settings updates 61 + func (ui *AdminUI) handleSettingsUpdate(w http.ResponseWriter, r *http.Request) { 62 + ctx := r.Context() 63 + 64 + if err := r.ParseForm(); err != nil { 65 + setFlash(w, r, "error", "Invalid form data") 66 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 67 + return 68 + } 69 + 70 + public := r.FormValue("public") == "on" 71 + allowAllCrew := r.FormValue("allow_all_crew") == "on" 72 + enablePosts := r.FormValue("enable_bluesky_posts") == "on" 73 + 74 + _, err := ui.pds.UpdateCaptainRecord(ctx, public, allowAllCrew, enablePosts) 75 + if err != nil { 76 + slog.Error("Failed to update captain record", "error", err) 77 + setFlash(w, r, "error", "Failed to update settings: "+err.Error()) 78 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 79 + return 80 + } 81 + 82 + session := getSessionFromContext(ctx) 83 + slog.Info("Settings updated via admin panel", 84 + "public", public, 85 + "allowAllCrew", allowAllCrew, 86 + "enableBlueskyPosts", enablePosts, 87 + "by", session.DID) 88 + 89 + setFlash(w, r, "success", "Settings updated successfully") 90 + http.Redirect(w, r, "/admin/settings", http.StatusFound) 91 + }
+692
pkg/hold/admin/static/css/admin.css
···
··· 1 + /* Hold Admin Panel Styles */ 2 + 3 + :root { 4 + --primary: #2563eb; 5 + --primary-hover: #1d4ed8; 6 + --danger: #dc2626; 7 + --danger-hover: #b91c1c; 8 + --warning: #f59e0b; 9 + --success: #10b981; 10 + --gray-50: #f9fafb; 11 + --gray-100: #f3f4f6; 12 + --gray-200: #e5e7eb; 13 + --gray-300: #d1d5db; 14 + --gray-500: #6b7280; 15 + --gray-700: #374151; 16 + --gray-900: #111827; 17 + } 18 + 19 + * { 20 + box-sizing: border-box; 21 + margin: 0; 22 + padding: 0; 23 + } 24 + 25 + body { 26 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 27 + background: var(--gray-50); 28 + color: var(--gray-900); 29 + line-height: 1.5; 30 + } 31 + 32 + /* Navigation */ 33 + .nav { 34 + background: var(--gray-900); 35 + color: white; 36 + padding: 1rem 2rem; 37 + display: flex; 38 + align-items: center; 39 + gap: 2rem; 40 + } 41 + 42 + .nav-brand a { 43 + color: white; 44 + text-decoration: none; 45 + font-weight: 600; 46 + font-size: 1.25rem; 47 + } 48 + 49 + .nav-links { 50 + list-style: none; 51 + display: flex; 52 + gap: 1rem; 53 + } 54 + 55 + .nav-links a { 56 + color: var(--gray-300); 57 + text-decoration: none; 58 + padding: 0.5rem 1rem; 59 + border-radius: 0.375rem; 60 + transition: background 0.2s; 61 + } 62 + 63 + .nav-links a:hover, 64 + .nav-links a.active { 65 + background: rgba(255, 255, 255, 0.1); 66 + color: white; 67 + } 68 + 69 + .nav-user { 70 + margin-left: auto; 71 + display: flex; 72 + align-items: center; 73 + gap: 1rem; 74 + color: var(--gray-300); 75 + } 76 + 77 + /* Container */ 78 + .container { 79 + max-width: 1200px; 80 + margin: 0 auto; 81 + padding: 2rem; 82 + } 83 + 84 + /* Page Header */ 85 + .page-header { 86 + display: flex; 87 + justify-content: space-between; 88 + align-items: center; 89 + margin-bottom: 2rem; 90 + } 91 + 92 + .page-header h1 { 93 + margin: 0; 94 + } 95 + 96 + /* Buttons */ 97 + .btn { 98 + display: inline-flex; 99 + align-items: center; 100 + justify-content: center; 101 + padding: 0.5rem 1rem; 102 + border: none; 103 + border-radius: 0.375rem; 104 + font-size: 0.875rem; 105 + font-weight: 500; 106 + text-decoration: none; 107 + cursor: pointer; 108 + transition: background 0.2s; 109 + background: var(--gray-200); 110 + color: var(--gray-700); 111 + } 112 + 113 + .btn:hover { 114 + background: var(--gray-300); 115 + } 116 + 117 + .btn-primary { 118 + background: var(--primary); 119 + color: white; 120 + } 121 + 122 + .btn-primary:hover { 123 + background: var(--primary-hover); 124 + } 125 + 126 + .btn-danger { 127 + background: var(--danger); 128 + color: white; 129 + } 130 + 131 + .btn-danger:hover { 132 + background: var(--danger-hover); 133 + } 134 + 135 + .btn-sm { 136 + padding: 0.25rem 0.5rem; 137 + font-size: 0.75rem; 138 + } 139 + 140 + .btn-icon { 141 + padding: 0.375rem; 142 + line-height: 1; 143 + } 144 + 145 + .btn-icon i { 146 + width: 16px; 147 + height: 16px; 148 + } 149 + 150 + .btn i { 151 + width: 16px; 152 + height: 16px; 153 + margin-right: 0.25rem; 154 + } 155 + 156 + .btn-icon i { 157 + margin-right: 0; 158 + } 159 + 160 + .btn-block { 161 + width: 100%; 162 + } 163 + 164 + /* Cards */ 165 + .card { 166 + background: white; 167 + border-radius: 0.5rem; 168 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 169 + overflow: hidden; 170 + } 171 + 172 + .card-header { 173 + padding: 1rem; 174 + background: var(--gray-50); 175 + border-bottom: 1px solid var(--gray-200); 176 + } 177 + 178 + .member-header { 179 + display: flex; 180 + justify-content: space-between; 181 + align-items: center; 182 + } 183 + 184 + .member-info { 185 + display: flex; 186 + flex-direction: column; 187 + gap: 0.25rem; 188 + } 189 + 190 + .member-info strong { 191 + font-size: 1.1rem; 192 + } 193 + 194 + /* Stats Grid */ 195 + .stats-grid { 196 + display: grid; 197 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 198 + gap: 1rem; 199 + margin-bottom: 2rem; 200 + } 201 + 202 + .stat-card { 203 + background: white; 204 + padding: 1.5rem; 205 + border-radius: 0.5rem; 206 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 207 + } 208 + 209 + .stat-card h3 { 210 + font-size: 0.875rem; 211 + color: var(--gray-500); 212 + margin-bottom: 0.5rem; 213 + } 214 + 215 + .stat-value { 216 + font-size: 2rem; 217 + font-weight: 600; 218 + } 219 + 220 + .stat-detail { 221 + font-size: 0.875rem; 222 + color: var(--gray-500); 223 + } 224 + 225 + /* Sections */ 226 + .section { 227 + background: white; 228 + padding: 1.5rem; 229 + border-radius: 0.5rem; 230 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 231 + margin-bottom: 1.5rem; 232 + } 233 + 234 + .section h2 { 235 + font-size: 1.125rem; 236 + margin-bottom: 1rem; 237 + } 238 + 239 + /* Tables */ 240 + .table { 241 + width: 100%; 242 + border-collapse: collapse; 243 + background: white; 244 + border-radius: 0.5rem; 245 + overflow: hidden; 246 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 247 + } 248 + 249 + .table th, 250 + .table td { 251 + padding: 0.75rem 1rem; 252 + text-align: left; 253 + border-bottom: 1px solid var(--gray-200); 254 + } 255 + 256 + .table th { 257 + background: var(--gray-50); 258 + font-weight: 600; 259 + font-size: 0.75rem; 260 + text-transform: uppercase; 261 + color: var(--gray-500); 262 + } 263 + 264 + .table td { 265 + font-size: 0.875rem; 266 + } 267 + 268 + .table tbody tr:hover { 269 + background: var(--gray-50); 270 + } 271 + 272 + .actions { 273 + display: flex; 274 + gap: 0.25rem; 275 + justify-content: flex-end; 276 + } 277 + 278 + .actions-header { 279 + text-align: right; 280 + } 281 + 282 + .member-cell { 283 + line-height: 1.4; 284 + } 285 + 286 + .member-cell strong { 287 + color: var(--gray-900); 288 + } 289 + 290 + .did-code { 291 + font-size: 0.75rem; 292 + color: var(--gray-500); 293 + word-break: break-all; 294 + } 295 + 296 + .permissions-cell .badge { 297 + margin-right: 0.25rem; 298 + margin-bottom: 0.25rem; 299 + } 300 + 301 + .tier-limit { 302 + color: var(--gray-500); 303 + } 304 + 305 + /* Badges */ 306 + .badge { 307 + display: inline-block; 308 + padding: 0.125rem 0.5rem; 309 + font-size: 0.75rem; 310 + border-radius: 9999px; 311 + background: var(--gray-200); 312 + color: var(--gray-700); 313 + } 314 + 315 + .badge-tier { 316 + background: var(--primary); 317 + color: white; 318 + } 319 + 320 + .badge-gold { 321 + background: #fbbf24; 322 + color: #78350f; 323 + } 324 + 325 + /* Progress Bar */ 326 + .usage-cell { 327 + display: flex; 328 + flex-direction: column; 329 + gap: 0.25rem; 330 + } 331 + 332 + .progress-bar { 333 + width: 100%; 334 + height: 4px; 335 + background: var(--gray-200); 336 + border-radius: 2px; 337 + overflow: hidden; 338 + } 339 + 340 + .progress-fill { 341 + height: 100%; 342 + background: var(--primary); 343 + transition: width 0.3s; 344 + } 345 + 346 + .progress-fill.warning { 347 + background: var(--warning); 348 + } 349 + 350 + .progress-fill.danger { 351 + background: var(--danger); 352 + } 353 + 354 + /* Forms */ 355 + .form { 356 + max-width: 600px; 357 + } 358 + 359 + .form-group { 360 + margin-bottom: 1.5rem; 361 + } 362 + 363 + .form-group label { 364 + display: block; 365 + font-weight: 500; 366 + margin-bottom: 0.5rem; 367 + } 368 + 369 + .form-group input[type="text"], 370 + .form-group input[type="email"], 371 + .form-group select { 372 + width: 100%; 373 + padding: 0.5rem 0.75rem; 374 + border: 1px solid var(--gray-300); 375 + border-radius: 0.375rem; 376 + font-size: 1rem; 377 + } 378 + 379 + .form-group input:focus, 380 + .form-group select:focus { 381 + outline: none; 382 + border-color: var(--primary); 383 + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); 384 + } 385 + 386 + .form-group small { 387 + display: block; 388 + margin-top: 0.25rem; 389 + font-size: 0.75rem; 390 + color: var(--gray-500); 391 + } 392 + 393 + /* Input with lookup button */ 394 + .input-with-lookup { 395 + display: flex; 396 + gap: 0.5rem; 397 + } 398 + 399 + .input-with-lookup input { 400 + flex: 1; 401 + } 402 + 403 + .handle-lookup-result { 404 + margin-top: 0.5rem; 405 + font-size: 0.875rem; 406 + } 407 + 408 + .handle-lookup-result .success { 409 + color: var(--success); 410 + display: flex; 411 + align-items: center; 412 + gap: 0.25rem; 413 + } 414 + 415 + .handle-lookup-result .success i { 416 + width: 16px; 417 + height: 16px; 418 + } 419 + 420 + .handle-lookup-result .error { 421 + color: var(--danger); 422 + } 423 + 424 + .handle-lookup-result .warning { 425 + color: var(--warning); 426 + } 427 + 428 + .handle-lookup-result .loading { 429 + color: var(--gray-500); 430 + font-style: italic; 431 + } 432 + 433 + .checkbox-group { 434 + display: flex; 435 + flex-direction: column; 436 + gap: 0.75rem; 437 + } 438 + 439 + .checkbox { 440 + display: flex; 441 + align-items: flex-start; 442 + gap: 0.5rem; 443 + cursor: pointer; 444 + } 445 + 446 + .checkbox input { 447 + margin-top: 0.25rem; 448 + } 449 + 450 + .checkbox span { 451 + font-weight: 500; 452 + } 453 + 454 + .checkbox small { 455 + display: block; 456 + font-weight: normal; 457 + color: var(--gray-500); 458 + } 459 + 460 + .form-actions { 461 + display: flex; 462 + gap: 1rem; 463 + margin-top: 2rem; 464 + } 465 + 466 + /* Toggle Settings */ 467 + .toggle-setting { 468 + display: flex; 469 + align-items: flex-start; 470 + gap: 1rem; 471 + padding: 1rem; 472 + background: var(--gray-50); 473 + border-radius: 0.375rem; 474 + margin-bottom: 1rem; 475 + cursor: pointer; 476 + } 477 + 478 + .toggle-setting input { 479 + margin-top: 0.25rem; 480 + } 481 + 482 + .toggle-label strong { 483 + display: block; 484 + } 485 + 486 + .toggle-label small { 487 + color: var(--gray-500); 488 + } 489 + 490 + /* Info List */ 491 + .info-list { 492 + display: grid; 493 + grid-template-columns: auto 1fr; 494 + gap: 0.5rem 1rem; 495 + } 496 + 497 + .info-list dt { 498 + font-weight: 500; 499 + color: var(--gray-500); 500 + } 501 + 502 + .info-list dd { 503 + font-family: monospace; 504 + } 505 + 506 + /* Flash Messages */ 507 + .flash { 508 + padding: 1rem; 509 + border-radius: 0.375rem; 510 + margin-bottom: 1rem; 511 + } 512 + 513 + .flash-success { 514 + background: #d1fae5; 515 + color: #065f46; 516 + } 517 + 518 + .flash-error { 519 + background: #fee2e2; 520 + color: #991b1b; 521 + } 522 + 523 + .flash-warning { 524 + background: #fef3c7; 525 + color: #92400e; 526 + } 527 + 528 + .flash-info { 529 + background: #dbeafe; 530 + color: #1e40af; 531 + } 532 + 533 + /* Empty State */ 534 + .empty { 535 + text-align: center; 536 + padding: 2rem; 537 + color: var(--gray-500); 538 + } 539 + 540 + /* Loading */ 541 + .loading { 542 + color: var(--gray-500); 543 + font-style: italic; 544 + } 545 + 546 + /* Note */ 547 + .note { 548 + padding: 1rem; 549 + background: var(--gray-100); 550 + border-radius: 0.375rem; 551 + color: var(--gray-500); 552 + font-style: italic; 553 + } 554 + 555 + /* Footer */ 556 + .footer { 557 + text-align: center; 558 + padding: 2rem; 559 + color: var(--gray-500); 560 + font-size: 0.875rem; 561 + } 562 + 563 + .footer code { 564 + font-size: 0.75rem; 565 + background: var(--gray-200); 566 + padding: 0.125rem 0.375rem; 567 + border-radius: 0.25rem; 568 + } 569 + 570 + /* Login Page */ 571 + .login-page { 572 + background: var(--gray-100); 573 + min-height: 100vh; 574 + display: flex; 575 + align-items: center; 576 + justify-content: center; 577 + } 578 + 579 + .login-container { 580 + width: 100%; 581 + max-width: 400px; 582 + padding: 1rem; 583 + } 584 + 585 + .login-card { 586 + background: white; 587 + padding: 2rem; 588 + border-radius: 0.5rem; 589 + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 590 + } 591 + 592 + .login-card h1 { 593 + text-align: center; 594 + margin-bottom: 0.5rem; 595 + } 596 + 597 + .login-subtitle { 598 + text-align: center; 599 + color: var(--gray-500); 600 + margin-bottom: 1.5rem; 601 + } 602 + 603 + .login-form .form-group { 604 + margin-bottom: 1rem; 605 + } 606 + 607 + .login-note { 608 + text-align: center; 609 + font-size: 0.875rem; 610 + color: var(--gray-500); 611 + margin-top: 1rem; 612 + } 613 + 614 + .login-footer { 615 + text-align: center; 616 + margin-top: 2rem; 617 + color: var(--gray-500); 618 + font-size: 0.75rem; 619 + } 620 + 621 + /* Error Page */ 622 + .error-page { 623 + text-align: center; 624 + padding: 4rem 2rem; 625 + } 626 + 627 + .error-message { 628 + color: var(--danger); 629 + margin: 1rem 0 2rem; 630 + } 631 + 632 + /* Tier Chart */ 633 + .tier-chart { 634 + display: flex; 635 + flex-direction: column; 636 + gap: 0.5rem; 637 + } 638 + 639 + .tier-bar { 640 + display: flex; 641 + justify-content: space-between; 642 + padding: 0.5rem 1rem; 643 + background: var(--gray-100); 644 + border-radius: 0.25rem; 645 + } 646 + 647 + .tier-name { 648 + font-weight: 500; 649 + } 650 + 651 + .tier-count { 652 + color: var(--gray-500); 653 + } 654 + 655 + /* Code */ 656 + code { 657 + font-family: "SF Mono", Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", monospace; 658 + font-size: 0.875em; 659 + background: var(--gray-100); 660 + padding: 0.125rem 0.25rem; 661 + border-radius: 0.25rem; 662 + } 663 + 664 + /* Responsive */ 665 + @media (max-width: 768px) { 666 + .nav { 667 + flex-wrap: wrap; 668 + padding: 1rem; 669 + } 670 + 671 + .nav-links { 672 + order: 3; 673 + width: 100%; 674 + justify-content: center; 675 + margin-top: 0.5rem; 676 + } 677 + 678 + .container { 679 + padding: 1rem; 680 + } 681 + 682 + .page-header { 683 + flex-direction: column; 684 + align-items: flex-start; 685 + gap: 1rem; 686 + } 687 + 688 + .table { 689 + display: block; 690 + overflow-x: auto; 691 + } 692 + }
+1
pkg/hold/admin/static/js/htmx.min.js
···
··· 1 + var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true,reportValidityOfForms:false},parseInterval:null,location:location,_:null,version:"2.0.8"};Q.onLoad=V;Q.process=Ft;Q.on=xe;Q.off=be;Q.trigger=ae;Q.ajax=Ln;Q.find=f;Q.findAll=x;Q.closest=g;Q.remove=_;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=ze;Q.defineExtension=_n;Q.removeExtension=zn;Q.logAll=j;Q.logNone=$;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:X,findThisElement:Se,filterValues:yn,swap:ze,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:q,getExpressionVars:Rn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Ee,makeFragment:D,mergeObjects:le,makeSettleInfo:Sn,oobSwap:Te,querySelectorExt:ue,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:Vt};const de=["get","post","put","delete","patch"];const R=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function y(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function q(e,t){while(e&&!t(e)){e=u(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;q(t,function(e){return!!(r=o(t,ce(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function A(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function L(e){if("parseHTMLUnsafe"in Document){return Document.parseHTMLUnsafe(e)}const t=new DOMParser;return t.parseFromString(e,"text/html")}function N(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){H(e)}finally{e.remove()}}})}function D(e){const t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,"");const n=A(t);let r;if(n==="html"){r=new DocumentFragment;const i=L(e);N(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=L(t);N(r,i.body);r.title=i.title}else{const i=L('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function P(e){return typeof e==="function"}function k(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e<t.length;e++){n.push(t[e])}}return n}function ie(t,n){if(t){for(let e=0;e<t.length;e++){n(t[e])}}}function F(e){const t=e.getBoundingClientRect();const n=t.top;const r=t.bottom;return n<window.innerHeight&&r>=0}function se(e){return e.getRootNode({composed:true})===document}function B(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){H(e);return null}}function X(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function U(e){const t=new URL(e,"http://x");if(t){e=t.pathname+t.search}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function V(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function j(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function $(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(te(),e)}}function b(){return window}function _(e,t){e=w(e);if(t){b().setTimeout(function(){_(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function z(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ce(w(e));if(!e){return}if(n){b().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ce(w(e));if(!r){return}if(n){b().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=w(e);e.classList.toggle(t)}function Z(e,t){e=w(e);ie(e.parentElement.children,function(e){G(e,t)});K(ce(e),t)}function g(e,t){e=ce(w(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function pe(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=w(t);const o=[];{let t=0;let n=0;for(let e=0;e<r.length;e++){const l=r[e];if(l===","&&t===0){o.push(r.substring(n,e));n=e+1;continue}if(l==="<"){t++}else if(l==="/"&&e<r.length-1&&r[e+1]===">"){t--}}if(n<r.length){o.push(r.substring(n))}}const i=[];const s=[];while(o.length>0){const r=pe(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ce(t),pe(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),pe(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ce(t).nextElementSibling}else if(r.indexOf("next ")===0){e=ge(t,pe(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ce(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,pe(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=y(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=p(y(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var ge=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=0;e<r.length;e++){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_PRECEDING){return o}}};var me=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=r.length-1;e>=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ue(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function w(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function ye(e,t,n,r){if(P(t)){return{target:te().body,event:J(e),listener:t,options:n}}else{return{target:w(e),event:J(t),listener:n,options:r}}}function xe(t,n,r,o){Gn(function(){const e=ye(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=P(n);return e?n:r}function be(t,n,r){Gn(function(){const e=ye(t,n,r);e.target.removeEventListener(e.event,e.listener)});return P(n)?n:r}const ve=te().createElement("output");function we(t,n){const e=ne(t,n);if(e){if(e==="this"){return[Se(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ce(q(t,function(e){return e!==t&&s(ce(e),n)}));if(i){r.push(...we(i,n))}}if(r.length===0){H('The selector "'+e+'" on '+n+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ce(q(e,function(e){return a(ce(e),t)!=null}))}function Ee(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ue(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ce(e){return Q.config.attributesToSettle.includes(e)}function Oe(t,n){ie(Array.from(t.attributes),function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function He(t,e){const n=Jn(e);for(let e=0;e<n.length;e++){const r=n[e];try{if(r.isInlineSwap(t)){return true}}catch(e){H(e)}}return t==="outerHTML"}function Te(e,o,i,t){t=t||te();let n="#"+CSS.escape(ee(o,"id"));let s="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!He(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);$e(s,e,e,t,i);Re()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Re(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){ie(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Ae(l,e,c){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=p(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Le(e){return function(){G(e,Q.config.addedClass);Ft(ce(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=z(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Ae(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Le(o))}}}function Ie(e,t){let n=0;while(n<e.length){t=(t<<5)-t+e.charCodeAt(n++)|0}return t}function De(t){let n=0;for(let e=0;e<t.attributes.length;e++){const r=t.attributes[e];if(r.value){n=Ie(r.name,n);n=Ie(r.value,n)}}return n}function Pe(t){const n=oe(t);if(n.onHandlers){for(let e=0;e<n.onHandlers.length;e++){const r=n.onHandlers[e];be(t,r.event,r.listener)}delete n.onHandlers}}function ke(e){const t=oe(e);if(t.timeout){clearTimeout(t.timeout)}if(t.listenerInfos){ie(t.listenerInfos,function(e){if(e.on){be(e.on,e.trigger,e.listener)}})}Pe(e);ie(Object.keys(t),function(e){if(e!=="firstInitCompleted")delete t[e]})}function S(e){ae(e,"htmx:beforeCleanupElement");ke(e);ie(e.children,function(e){S(e)})}function Me(t,e,n){if(t.tagName==="BODY"){return je(t,e,n)}let r;const o=t.previousSibling;const i=u(t);if(!i){return}c(i,t,e,n);if(o==null){r=i.firstChild}else{r=o.nextSibling}n.elts=n.elts.filter(function(e){return e!==t});while(r&&r!==t){if(r instanceof Element){n.elts.push(r)}r=r.nextSibling}S(t);t.remove()}function Fe(e,t,n){return c(e,e.firstChild,t,n)}function Be(e,t,n){return c(u(e),e,t,n)}function Xe(e,t,n){return c(e,null,t,n)}function Ue(e,t,n){return c(u(e),e.nextSibling,t,n)}function Ve(e){S(e);const t=u(e);if(t){return t.removeChild(e)}}function je(e,t,n){const r=e.firstChild;c(e,r,t,n);if(r){while(r.nextSibling){S(r.nextSibling);e.removeChild(r.nextSibling)}S(r);e.removeChild(r)}}function $e(t,e,n,r,o){switch(t){case"none":return;case"outerHTML":Me(n,r,o);return;case"afterbegin":Fe(n,r,o);return;case"beforebegin":Be(n,r,o);return;case"beforeend":Xe(n,r,o);return;case"afterend":Ue(n,r,o);return;case"delete":Ve(n);return;default:var i=Jn(e);for(let e=0;e<i.length;e++){const s=i[e];try{const l=s.handleSwap(t,n,r,o);if(l){if(Array.isArray(l)){for(let e=0;e<l.length;e++){const c=l[e];if(c.nodeType!==Node.TEXT_NODE&&c.nodeType!==Node.COMMENT_NODE){o.tasks.push(Le(c))}}}return}}catch(e){H(e)}}if(t==="innerHTML"){je(n,r,o)}else{$e(Q.config.defaultSwapStyle,e,n,r,o)}}}function _e(e,n,r){var t=x(e,"[hx-swap-oob], [data-hx-swap-oob]");ie(t,function(e){if(Q.config.allowNestedOobSwaps||e.parentElement===null){const t=a(e,"hx-swap-oob");if(t!=null){Te(t,e,n,r)}}else{e.removeAttribute("hx-swap-oob");e.removeAttribute("data-hx-swap-oob")}});return t.length>0}function ze(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=w(h);const r=g.contextElement?y(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=D(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t<i.length;t++){const s=i[t].split(":",2);let e=s[0].trim();if(e.indexOf("#")===0){e=e.substring(1)}const l=s[1]||"true";const c=n.querySelector("#"+e);if(c){Te(l,c,o,r)}}}_e(n,o,r);ie(x(n,"template"),function(e){if(e.content&&_e(e.content,o,r)){e.remove()}});if(g.select){const u=te().createDocumentFragment();ie(n.querySelectorAll(g.select),function(e){u.appendChild(e)});n=u}qe(n);$e(p.swapStyle,g.contextElement,h,n,o);Re()}if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){const f=document.getElementById(ee(t.elt,"id"));const a={preventScroll:p.focusScroll!==undefined?!p.focusScroll:!Q.config.defaultFocusScroll};if(f){if(t.start&&f.setSelectionRange){try{f.setSelectionRange(t.start,t.end)}catch(e){}}f.focus(a)}}h.classList.remove(Q.config.swappingClass);ie(o.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ae(e,"htmx:afterSwap",g.eventInfo)});re(g.afterSwapCallback);if(!p.ignoreTitle){Xn(o.title)}const n=function(){ie(o.tasks,function(e){e.call()});ie(o.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ae(e,"htmx:afterSettle",g.eventInfo)});if(g.anchor){const e=ce(w("#"+g.anchor));if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}En(o.elts,p);re(g.afterSettleCallback);re(m)};if(p.settleDelay>0){b().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){b().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(k(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e<s.length;e++){ae(n,s[e].trim(),[])}}}const Ke=/\s/;const E=/[\s,]/;const Ge=/[_$a-zA-Z]/;const We=/[_$a-zA-Z0-9]/;const Ze=['"',"'","/"];const C=/[^\s]/;const Ye=/[{(]/;const Qe=/[})]/;function et(e){const t=[];let n=0;while(n<e.length){if(Ge.exec(e.charAt(n))){var r=n;while(We.exec(e.charAt(n+1))){n++}t.push(e.substring(r,n+1))}else if(Ze.indexOf(e.charAt(n))!==-1){const o=e.charAt(n);var r=n;n++;while(n<e.length&&e.charAt(n)!==o){if(e.charAt(n)==="\\"){n++}n++}t.push(e.substring(r,n+1))}else{const i=e.charAt(n);t.push(i)}n++}return t}function tt(e,t,n){return Ge.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function nt(r,o,i){if(o[0]==="["){o.shift();let e=1;let t=" return (function("+i+"){ return (";let n=null;while(o.length>0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,E)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,C);const l=o.length;const c=O(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};O(o,C);u.pollInterval=d(O(o,/[,\[\s]/));O(o,C);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const f={trigger:c};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,C);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,E))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,E);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,E))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,E)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,E)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ct(e,t,n){const r=oe(e);r.timeout=b().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Xt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"&&t.tagName==="FORM"){return true}else if(e.type==="click"){const n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit"){return true}const r=t.closest("a");const o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href"))){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,c,e,u,f){const a=oe(l);let t;if(u.from){t=m(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(u)){a.lastValue.set(u,new WeakMap)}a.lastValue.get(u).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,i)){e.preventDefault()}if(pt(u,l,e)){return}const t=oe(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");c(l,e);a.throttle=b().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=b().setTimeout(function(){ae(l,"htmx:trigger");c(l,e)},u.delay)}else{ae(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&F(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){b().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ue(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e<t.length;e++){const n=t[e];if(n.isIntersecting){ae(r,"intersect");break}}},o);i.observe(ce(r));gt(ce(r),n,t,e)}else if(!t.firstInitCompleted&&e.trigger==="load"){if(!pt(e,r,Xt("load",{elt:r}))){vt(ce(r),n,t,e.delay)}}else if(e.pollInterval>0){t.polling=true;ct(ce(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e<n.length;e++){const r=n[e].name;if(l(r,"hx-on:")||l(r,"data-hx-on:")||l(r,"hx-on-")||l(r,"data-hx-on-")){return true}}return false}const Ct=(new XPathEvaluator).createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or'+' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function Ot(e,t){if(Et(e)){t.push(ce(e))}const n=Ct.evaluate(e);let r=null;while(r=n.iterateNext())t.push(ce(r))}function Ht(e){const t=[];if(e instanceof DocumentFragment){for(const n of e.childNodes){Ot(n,t)}}else{Ot(e,t)}return t}function Tt(e){if(e.querySelectorAll){const n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";const r=[];for(const i in jn){const s=jn[i];if(s.getSelectors){var t=s.getSelectors();if(t){r.push(t)}}}const o=e.querySelectorAll(R+n+", form, [type='submit'],"+" [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(e=>", "+e).join(""));return o}else{return[]}}function Rt(e){const t=At(e.target);const n=Nt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ce(e),"button, input[type='submit']")}function Lt(e){return e.form||g(e,"form")}function Nt(e){const t=At(e.target);if(!t){return}const n=Lt(t);if(!n){return}return oe(n)}function It(e){e.addEventListener("click",Rt);e.addEventListener("focusin",Rt);e.addEventListener("focusout",qt)}function Dt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Pt(t){Pe(t);for(let e=0;e<t.attributes.length;e++){const n=t.attributes[e].name;const r=t.attributes[e].value;if(l(n,"hx-on")||l(n,"data-hx-on")){const o=n.indexOf("-on")+3;const i=n.slice(o,o+1);if(i==="-"||i===":"){let e=n.slice(o+1);if(l(e,":")){e="htmx"+e}else if(l(e,"-")){e="htmx:"+e.slice(1)}else if(l(e,"htmx-")){e="htmx:"+e.slice(5)}Dt(t,e,r)}}}}function kt(t){ae(t,"htmx:beforeProcessNode");const n=oe(t);const e=st(t);const r=wt(t,n,e);if(!r){if(ne(t,"hx-boost")==="true"){at(t,n,e)}else if(s(t,"hx-trigger")){e.forEach(function(e){St(t,e,n,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&s(t,"form")){It(t)}n.firstInitCompleted=true;ae(t,"htmx:afterProcessNode")}function Mt(e){if(!(e instanceof Element)){return false}const t=oe(e);const n=De(e);if(t.initHash!==n){ke(e);t.initHash=n;return true}return false}function Ft(e){e=w(e);if(ft(e)){S(e);return}const t=[];if(Mt(e)){t.push(e)}ie(Tt(e),function(e){if(ft(e)){S(e);return}if(Mt(e)){t.push(e)}});ie(Ht(e),Pt);ie(t,kt)}function Bt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Xt(e,t){return new CustomEvent(e,{bubbles:true,cancelable:true,composed:true,detail:t})}function fe(e,t,n){ae(e,t,le({error:t},n))}function Ut(e){return e==="htmx:afterProcessNode"}function Vt(e,t,n){ie(Jn(e,[],n),function(e){try{t(e)}catch(e){H(e)}})}function H(e){console.error(e)}function ae(e,t,n){e=w(e);if(n==null){n={}}n.elt=e;const r=Xt(t,n);if(Q.logger&&!Ut(t)){Q.logger(e,t,n)}if(n.error){H(n.error);ae(e,"htmx:error",{errorInfo:n})}let o=e.dispatchEvent(r);const i=Bt(t);if(o&&i!==t){const s=Xt(i,r.detail);o=o&&e.dispatchEvent(s)}Vt(ce(e),function(e){o=o&&(e.onEvent(t,r)!==false&&!r.defaultPrevented)});return o}let jt;function $t(e){jt=e;if(X()){sessionStorage.setItem("htmx-current-path-for-history",e)}}$t(location.pathname+location.search);function _t(){const e=te().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||te().body}function zt(t,e){if(!X()){return}const n=Kt(e);const r=te().title;const o=window.scrollY;if(Q.config.historyCacheSize<=0){sessionStorage.removeItem("htmx-history-cache");return}t=U(t);const i=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<i.length;e++){if(i[e].url===t){i.splice(e,1);break}}const s={url:t,content:n,title:r,scroll:o};ae(te().body,"htmx:historyItemCreated",{item:s,cache:i});i.push(s);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!X()){return null}t=U(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e<n.length;e++){if(n[e].url===t){return n[e]}}return null}function Kt(e){const t=Q.config.requestClass;const n=e.cloneNode(true);ie(x(n,"."+t),function(e){G(e,t)});ie(x(n,"[data-disabled-by-htmx]"),function(e){e.removeAttribute("disabled")});return n.innerHTML}function Gt(){const e=_t();let t=jt;if(X()){t=sessionStorage.getItem("htmx-current-path-for-history")}t=t||location.pathname+location.search;const n=te().querySelector('[hx-history="false" i],[data-hx-history="false" i]');if(!n){ae(te().body,"htmx:beforeHistorySave",{path:t,historyElt:e});zt(t,e)}if(Q.config.historyEnabled)history.replaceState({htmx:true},te().title,location.href)}function Wt(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(Y(e,"&")||Y(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}$t(e)}function Zt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);$t(e)}function Yt(e){ie(e,function(e){e.call(undefined)})}function Qt(e){const t=new XMLHttpRequest;const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0};const r={path:e,xhr:t,historyElt:_t(),swapSpec:n};t.open("GET",e,true);if(Q.config.historyRestoreAsHxRequest){t.setRequestHeader("HX-Request","true")}t.setRequestHeader("HX-History-Restore-Request","true");t.setRequestHeader("HX-Current-URL",location.href);t.onload=function(){if(this.status>=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);ze(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});$t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:_t(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){ze(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});$t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function nn(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;e<t.length;e++){const r=t[e];if(r.isSameNode(n)){return true}}return false}function sn(e){const t=e;if(t.name===""||t.name==null||t.disabled||g(t,"fieldset[disabled]")){return false}if(t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"){return false}if(t.type==="checkbox"||t.type==="radio"){return t.checked}return true}function ln(t,e,n){if(t!=null&&e!=null){if(Array.isArray(e)){e.forEach(function(e){n.append(t,e)})}else{n.append(t,e)}}}function cn(t,n,r){if(t!=null&&n!=null){let e=r.getAll(t);if(Array.isArray(n)){e=e.filter(e=>n.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function un(e){if(e instanceof HTMLSelectElement&&e.multiple){return M(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return M(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,un(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){cn(e.name,un(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){if(ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&Q.config.reportValidityOfForms){n.reportValidity()}t.push({elt:n,message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Lt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const f=ee(u,"name");ln(f,u.value,o)}const c=we(e,"hx-include");ie(c,function(e){fn(n,r,i,ce(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Dn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=B(n);if(s.length>0){for(let e=0;e<s.length;e++){const l=s[e];if(l.indexOf("swap:")===0){r.swapDelay=d(l.slice(5))}else if(l.indexOf("settle:")===0){r.settleDelay=d(l.slice(7))}else if(l.indexOf("transition:")===0){r.transition=l.slice(11)==="true"}else if(l.indexOf("ignoreTitle:")===0){r.ignoreTitle=l.slice(12)==="true"}else if(l.indexOf("scroll:")===0){const c=l.slice(7);var o=c.split(":");const u=o.pop();var i=o.length>0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{H("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;Vt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Dn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ue(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){b().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ue(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const c in n){if(n.hasOwnProperty(c)){if(i[c]==null){i[c]=n[c]}}}}return Cn(ce(u(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Hn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Tn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Rn(e,t){return le(Hn(e,t),Tn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function T(e,t){return t.test(e.getAllResponseHeaders())}function Ln(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:w(r)||ve,returnPromise:true})}else{let e=w(r.target);if(r.target&&!e||r.source&&!e&&!w(r.source)){e=ve}return he(t,n,w(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true,push:r.push,replace:r.replace,selectOOB:r.selectOOB})}}else{return he(t,n,null,null,{returnPromise:true})}}function Nn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function In(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Dn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Pn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Pn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const M=i.handler||Vn;const F=i.select||null;if(!se(r)){re(s);return e}const c=i.targetOverride||ce(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let u=oe(r);const f=u.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const L=ee(f,"formmethod");if(L!=null){if(de.includes(L.toLowerCase())){t=L}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let B=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ce(ue(r,I))}d=(N[1]||"drop").trim();u=oe(h);if(d==="drop"&&u.xhr&&u.abortable!==true){re(s);return e}else if(d==="abort"){if(u.xhr){re(s);return e}else{B=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const D=oe(o);if(D&&D.triggerSpec&&D.triggerSpec.queue){p=D.triggerSpec.queue}}if(p==null){p="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(p==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;u.xhr=g;u.abortable=B;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const X=ne(r,"hx-prompt");if(X){var y=prompt(X);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:c})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,c,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const V=U.formData;if(i.values){hn(V,Dn(i.values))}const j=Dn(Rn(r,o));const v=hn(V,j);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const $=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:$,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Dn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const _=n.split("#");const z=_[0];const O=_[1];let H=n;if(E){H=z;const Z=!w.keys().next().done;if(Z){if(H.indexOf("?")<0){H+="?"}else{H+="&"}H+=gn(w);if(O){H+="#"+O}}}if(!In(r,H,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),H,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const P in x){if(x.hasOwnProperty(P)){const Y=x[P];qn(g,P,Y)}}}const T={xhr:g,target:c,requestConfig:C,etc:i,boosted:$,select:F,pathInfo:{requestPath:n,finalRequestPath:H,responsePath:null,anchor:O}};g.onload=function(){try{const t=Nn(r);T.pathInfo.responsePath=An(g);M(r,T);if(T.keepIndicators!==true){rn(R,q)}ae(r,"htmx:afterRequest",T);ae(r,"htmx:afterOnLoad",T);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",T);ae(e,"htmx:afterOnLoad",T)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},T));throw e}finally{m()}};g.onerror=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendError",T);re(l);m()};g.onabort=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:sendAbort",T);re(l);m()};g.ontimeout=function(){rn(R,q);fe(r,"htmx:afterRequest",T);fe(r,"htmx:timeout",T);re(l);m()};if(!ae(r,"htmx:beforeRequest",T)){re(s);m();return e}var R=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",T);const J=E?null:wn(g,r,w);g.send(J);return e}function Mn(e,t){const n=t.xhr;let r=null;let o=null;if(T(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(T(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(T(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=t.etc.push||ne(e,"hx-push-url");const c=t.etc.replace||ne(e,"hx-replace-url");const u=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(c){f="replace";a=c}else if(u){f="push";a=s||i}if(a){if(a==="false"){return{}}if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Fn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Bn(e){for(var t=0;t<Q.config.responseHandling.length;t++){var n=Q.config.responseHandling[t];if(Fn(n,e.status)){return n}}return{swap:false}}function Xn(e){if(e){const t=f("title");if(t){t.textContent=e}else{window.document.title=e}}}function Un(e,t){if(t==="this"){return e}const n=ce(ue(e,t));if(n==null){fe(e,"htmx:targetError",{target:t});throw new Error(`Invalid re-target ${t}`)}return n}function Vn(t,e){const n=e.xhr;let r=e.target;const o=e.etc;const i=e.select;if(!ae(t,"htmx:beforeOnLoad",e))return;if(T(n,/HX-Trigger:/i)){Je(n,"HX-Trigger",t)}if(T(n,/HX-Location:/i)){let e=n.getResponseHeader("HX-Location");var s={};if(e.indexOf("{")===0){s=v(e);e=s.path;delete s.path}s.push=s.push||"true";Ln("get",e,s);return}const l=T(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(T(n,/HX-Redirect:/i)){e.keepIndicators=true;Q.location.href=n.getResponseHeader("HX-Redirect");l&&Q.location.reload();return}if(l){e.keepIndicators=true;Q.location.reload();return}const c=Mn(t,e);const u=Bn(n);const f=u.swap;let a=!!u.error;let h=Q.config.ignoreTitle||u.ignoreTitle;let d=u.select;if(u.target){e.target=Un(t,u.target)}var p=o.swapOverride;if(p==null&&u.swapOverride){p=u.swapOverride}if(T(n,/HX-Retarget:/i)){e.target=Un(t,n.getResponseHeader("HX-Retarget"))}if(T(n,/HX-Reswap:/i)){p=n.getResponseHeader("HX-Reswap")}var g=n.response;var m=le({shouldSwap:f,serverResponse:g,isError:a,ignoreTitle:h,selectOverride:d,swapOverride:p},e);if(u.event&&!ae(r,u.event,m))return;if(!ae(r,"htmx:beforeSwap",m))return;r=m.target;g=m.serverResponse;a=m.isError;h=m.ignoreTitle;d=m.selectOverride;p=m.swapOverride;e.target=r;e.failed=a;e.successful=!a;if(m.shouldSwap){if(n.status===286){lt(t)}Vt(t,function(e){g=e.transformResponse(g,n,t)});if(c.type){Gt()}var y=bn(t,p);if(!y.hasOwnProperty("ignoreTitle")){y.ignoreTitle=h}r.classList.add(Q.config.swappingClass);if(i){d=i}if(T(n,/HX-Reselect:/i)){d=n.getResponseHeader("HX-Reselect")}const x=o.selectOOB||ne(t,"hx-select-oob");const b=ne(t,"hx-select");ze(r,g,y,{select:d==="unset"?null:d||b,selectOOB:x,eventInfo:e,anchor:e.pathInfo.anchor,contextElement:t,afterSwapCallback:function(){if(T(n,/HX-Trigger-After-Swap:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Swap",e)}},afterSettleCallback:function(){if(T(n,/HX-Trigger-After-Settle:/i)){let e=t;if(!se(t)){e=te().body}Je(n,"HX-Trigger-After-Settle",e)}},beforeSwapCallback:function(){if(c.type){ae(te().body,"htmx:beforeHistoryUpdate",le({history:c},e));if(c.type==="push"){Wt(c.path);ae(te().body,"htmx:pushedIntoHistory",{path:c.path})}else{Zt(c.path);ae(te().body,"htmx:replacedInHistory",{path:c.path})}}}})}if(a){fe(t,"htmx:responseError",le({error:"Response Status Error Code "+n.status+" from "+e.pathInfo.requestPath},e))}}const jn={};function $n(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function _n(e,t){if(t.init){t.init(n)}jn[e]=le($n(),t)}function zn(e){delete jn[e]}function Jn(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=a(e,"hx-ext");if(t){ie(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=jn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Jn(ce(u(e)),n,r)}var Kn=false;te().addEventListener("DOMContentLoaded",function(){Kn=true});function Gn(e){if(Kn||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Wn(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";const t=Q.config.indicatorClass;const n=Q.config.requestClass;te().head.insertAdjacentHTML("beforeend",`<style${e}>`+`.${t}{opacity:0;visibility: hidden} `+`.${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`+"</style>")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Ft(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.detail.elt||e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};b().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}();
+12
pkg/hold/admin/static/js/lucide.min.js
···
··· 1 + /** 2 + * @license lucide v0.562.0 - ISC 3 + * 4 + * This source code is licensed under the ISC license. 5 + * See the LICENSE file in the root directory of this source tree. 6 + */ 7 + 8 + (function(a,n){typeof exports=="object"&&typeof module<"u"?n(exports):typeof define=="function"&&define.amd?define(["exports"],n):(a=typeof globalThis<"u"?globalThis:a||self,n(a.lucide={}))})(this,(function(a){"use strict";const n={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"},Sa=([t,h,d])=>{const c=document.createElementNS("http://www.w3.org/2000/svg",t);return Object.keys(h).forEach(M=>{c.setAttribute(M,String(h[M]))}),d?.length&&d.forEach(M=>{const p=Sa(M);c.appendChild(p)}),c},La=(t,h={})=>{const d={...n,...h};return Sa(["svg",d,t])},ru=t=>Array.from(t.attributes).reduce((h,d)=>(h[d.name]=d.value,h),{}),ou=t=>typeof t=="string"?t:!t||!t.class?"":t.class&&typeof t.class=="string"?t.class.split(" "):t.class&&Array.isArray(t.class)?t.class:"",vu=t=>t.flatMap(ou).map(h=>h.trim()).filter(Boolean).filter((h,d,c)=>c.indexOf(h)===d).join(" "),$u=t=>t.replace(/(\w)(\w*)(_|-|\s*)/g,(h,d,c)=>d.toUpperCase()+c.toLowerCase()),fa=(t,{nameAttr:h,icons:d,attrs:c})=>{const M=t.getAttribute(h);if(M==null)return;const p=$u(M),Va=d[p];if(!Va)return console.warn(`${t.outerHTML} icon name was not found in the provided icons object.`);const nu=ru(t),lu={...n,"data-lucide":M,...c,...nu},eu=vu(["lucide",`lucide-${M}`,nu,c]);eu&&Object.assign(lu,{class:eu});const mu=La(Va,lu);return t.parentNode?.replaceChild(mu,t)},ka=[["path",{d:"m14 12 4 4 4-4"}],["path",{d:"M18 16V7"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],Pa=[["path",{d:"m14 11 4-4 4 4"}],["path",{d:"M18 16V7"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],Ba=[["path",{d:"m15 16 2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16"}],["path",{d:"M15.697 14h5.606"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],za=[["circle",{cx:"16",cy:"4",r:"1"}],["path",{d:"m18 19 1-7-6 1"}],["path",{d:"m5 8 3-3 5.5 3-2.36 3.5"}],["path",{d:"M4.24 14.5a5 5 0 0 0 6.88 6"}],["path",{d:"M13.76 17.5a5 5 0 0 0-6.88-6"}]],Fa=[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"}]],Da=[["path",{d:"M18 17.5a2.5 2.5 0 1 1-4 2.03V12"}],["path",{d:"M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 8h12"}],["path",{d:"M6.6 15.572A2 2 0 1 0 10 17v-5"}]],s=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"m9 13 2 2 4-4"}]],ba=[["path",{d:"M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"}],["path",{d:"m12 15 5 6H7Z"}]],g=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M9 13h6"}]],Ra=[["path",{d:"M6.87 6.87a8 8 0 1 0 11.26 11.26"}],["path",{d:"M19.9 14.25a8 8 0 0 0-9.15-9.15"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.26 18.67 4 21"}],["path",{d:"m2 2 20 20"}],["path",{d:"M4 4 2 6"}]],C=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}],["path",{d:"M12 10v6"}],["path",{d:"M9 13h6"}]],Ta=[["circle",{cx:"12",cy:"13",r:"8"}],["path",{d:"M12 9v4l2 2"}],["path",{d:"M5 3 2 6"}],["path",{d:"m22 6-3-3"}],["path",{d:"M6.38 18.7 4 21"}],["path",{d:"M17.64 18.67 20 21"}]],qa=[["path",{d:"M11 21c0-2.5 2-2.5 2-5"}],["path",{d:"M16 21c0-2.5 2-2.5 2-5"}],["path",{d:"m19 8-.8 3a1.25 1.25 0 0 1-1.2 1H7a1.25 1.25 0 0 1-1.2-1L5 8"}],["path",{d:"M21 3a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a1 1 0 0 1 1-1z"}],["path",{d:"M6 21c0-2.5 2-2.5 2-5"}]],Ua=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["polyline",{points:"11 3 11 11 14 8 17 11 17 3"}]],Oa=[["path",{d:"M2 12h20"}],["path",{d:"M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4"}],["path",{d:"M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4"}],["path",{d:"M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1"}],["path",{d:"M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1"}]],Za=[["path",{d:"M12 2v20"}],["path",{d:"M8 10H4a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h4"}],["path",{d:"M16 10h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4"}],["path",{d:"M8 20H7a2 2 0 0 1-2-2v-2c0-1.1.9-2 2-2h1"}],["path",{d:"M16 14h1a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-1"}]],Ga=[["rect",{width:"6",height:"16",x:"4",y:"2",rx:"2"}],["rect",{width:"6",height:"9",x:"14",y:"9",rx:"2"}],["path",{d:"M22 22H2"}]],Wa=[["rect",{width:"16",height:"6",x:"2",y:"4",rx:"2"}],["rect",{width:"9",height:"6",x:"9",y:"14",rx:"2"}],["path",{d:"M22 22V2"}]],Ia=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M17 22v-5"}],["path",{d:"M17 7V2"}],["path",{d:"M7 22v-3"}],["path",{d:"M7 5V2"}]],Ea=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M4 2v20"}],["path",{d:"M14 2v20"}]],Xa=[["rect",{width:"6",height:"14",x:"4",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"14",y:"7",rx:"2"}],["path",{d:"M10 2v20"}],["path",{d:"M20 2v20"}]],ja=[["rect",{width:"6",height:"14",x:"2",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"16",y:"7",rx:"2"}],["path",{d:"M12 2v20"}]],Na=[["rect",{width:"6",height:"14",x:"2",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"12",y:"7",rx:"2"}],["path",{d:"M22 2v20"}]],Ka=[["rect",{width:"6",height:"14",x:"6",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"16",y:"7",rx:"2"}],["path",{d:"M2 2v20"}]],Qa=[["rect",{width:"6",height:"10",x:"9",y:"7",rx:"2"}],["path",{d:"M4 22V2"}],["path",{d:"M20 22V2"}]],Ja=[["rect",{width:"6",height:"14",x:"3",y:"5",rx:"2"}],["rect",{width:"6",height:"10",x:"15",y:"7",rx:"2"}],["path",{d:"M3 2v20"}],["path",{d:"M21 2v20"}]],Ya=[["rect",{width:"6",height:"16",x:"4",y:"6",rx:"2"}],["rect",{width:"6",height:"9",x:"14",y:"6",rx:"2"}],["path",{d:"M22 2H2"}]],_a=[["rect",{width:"9",height:"6",x:"6",y:"14",rx:"2"}],["rect",{width:"16",height:"6",x:"6",y:"4",rx:"2"}],["path",{d:"M2 2v20"}]],xa=[["path",{d:"M22 17h-3"}],["path",{d:"M22 7h-5"}],["path",{d:"M5 17H2"}],["path",{d:"M7 7H2"}],["rect",{x:"5",y:"14",width:"14",height:"6",rx:"2"}],["rect",{x:"7",y:"4",width:"10",height:"6",rx:"2"}]],at=[["rect",{width:"14",height:"6",x:"5",y:"14",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"4",rx:"2"}],["path",{d:"M2 20h20"}],["path",{d:"M2 10h20"}]],tt=[["rect",{width:"14",height:"6",x:"5",y:"14",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"4",rx:"2"}],["path",{d:"M2 14h20"}],["path",{d:"M2 4h20"}]],ht=[["rect",{width:"14",height:"6",x:"5",y:"16",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"2",rx:"2"}],["path",{d:"M2 12h20"}]],dt=[["rect",{width:"14",height:"6",x:"5",y:"12",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"2",rx:"2"}],["path",{d:"M2 22h20"}]],ct=[["rect",{width:"14",height:"6",x:"5",y:"16",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"6",rx:"2"}],["path",{d:"M2 2h20"}]],Mt=[["rect",{width:"10",height:"6",x:"7",y:"9",rx:"2"}],["path",{d:"M22 20H2"}],["path",{d:"M22 4H2"}]],pt=[["rect",{width:"14",height:"6",x:"5",y:"15",rx:"2"}],["rect",{width:"10",height:"6",x:"7",y:"3",rx:"2"}],["path",{d:"M2 21h20"}],["path",{d:"M2 3h20"}]],it=[["path",{d:"M10 10H6"}],["path",{d:"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"}],["path",{d:"M19 18h2a1 1 0 0 0 1-1v-3.28a1 1 0 0 0-.684-.948l-1.923-.641a1 1 0 0 1-.578-.502l-1.539-3.076A1 1 0 0 0 16.382 8H14"}],["path",{d:"M8 8v4"}],["path",{d:"M9 18h6"}],["circle",{cx:"17",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],nt=[["path",{d:"M16 12h3"}],["path",{d:"M17.5 12a8 8 0 0 1-8 8A4.5 4.5 0 0 1 5 15.5c0-6 8-4 8-8.5a3 3 0 1 0-6 0c0 3 2.5 8.5 12 13"}]],lt=[["path",{d:"M10 17c-5-3-7-7-7-9a2 2 0 0 1 4 0c0 2.5-5 2.5-5 6 0 1.7 1.3 3 3 3 2.8 0 5-2.2 5-5"}],["path",{d:"M22 17c-5-3-7-7-7-9a2 2 0 0 1 4 0c0 2.5-5 2.5-5 6 0 1.7 1.3 3 3 3 2.8 0 5-2.2 5-5"}]],et=[["path",{d:"M10 2v5.632c0 .424-.272.795-.653.982A6 6 0 0 0 6 14c.006 4 3 7 5 8"}],["path",{d:"M10 5H8a2 2 0 0 0 0 4h.68"}],["path",{d:"M14 2v5.632c0 .424.272.795.652.982A6 6 0 0 1 18 14c0 4-3 7-5 8"}],["path",{d:"M14 5h2a2 2 0 0 1 0 4h-.68"}],["path",{d:"M18 22H6"}],["path",{d:"M9 2h6"}]],rt=[["path",{d:"M12 6v16"}],["path",{d:"m19 13 2-1a9 9 0 0 1-18 0l2 1"}],["path",{d:"M9 11h6"}],["circle",{cx:"12",cy:"4",r:"2"}]],ot=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 16s-1.5-2-4-2-4 2-4 2"}],["path",{d:"M7.5 8 10 9"}],["path",{d:"m14 9 2.5-1"}],["path",{d:"M9 10h.01"}],["path",{d:"M15 10h.01"}]],vt=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 15h8"}],["path",{d:"M8 9h2"}],["path",{d:"M14 9h2"}]],$t=[["path",{d:"M2 12 7 2"}],["path",{d:"m7 12 5-10"}],["path",{d:"m12 12 5-10"}],["path",{d:"m17 12 5-10"}],["path",{d:"M4.5 7h15"}],["path",{d:"M12 16v6"}]],mt=[["path",{d:"M7 10H6a4 4 0 0 1-4-4 1 1 0 0 1 1-1h4"}],["path",{d:"M7 5a1 1 0 0 1 1-1h13a1 1 0 0 1 1 1 7 7 0 0 1-7 7H8a1 1 0 0 1-1-1z"}],["path",{d:"M9 12v5"}],["path",{d:"M15 12v5"}],["path",{d:"M5 20a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3 1 1 0 0 1-1 1H6a1 1 0 0 1-1-1"}]],yt=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m14.31 8 5.74 9.94"}],["path",{d:"M9.69 8h11.48"}],["path",{d:"m7.38 12 5.74-9.94"}],["path",{d:"M9.69 16 3.95 6.06"}],["path",{d:"M14.31 16H2.83"}],["path",{d:"m16.62 12-5.74 9.94"}]],st=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 8h.01"}],["path",{d:"M10 8h.01"}],["path",{d:"M14 8h.01"}]],gt=[["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}],["path",{d:"M10 4v4"}],["path",{d:"M2 8h20"}],["path",{d:"M6 4v4"}]],Ct=[["path",{d:"M12 6.528V3a1 1 0 0 1 1-1h0"}],["path",{d:"M18.237 21A15 15 0 0 0 22 11a6 6 0 0 0-10-4.472A6 6 0 0 0 2 11a15.1 15.1 0 0 0 3.763 10 3 3 0 0 0 3.648.648 5.5 5.5 0 0 1 5.178 0A3 3 0 0 0 18.237 21"}]],ut=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h2"}],["path",{d:"M20 8v11a2 2 0 0 1-2 2h-2"}],["path",{d:"m9 15 3-3 3 3"}],["path",{d:"M12 12v9"}]],Ht=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"}],["path",{d:"m9.5 17 5-5"}],["path",{d:"m9.5 12 5 5"}]],At=[["rect",{width:"20",height:"5",x:"2",y:"3",rx:"1"}],["path",{d:"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"}],["path",{d:"M10 12h4"}]],wt=[["path",{d:"M19 9V6a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v3"}],["path",{d:"M3 16a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],Vt=[["path",{d:"M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V9a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"}],["path",{d:"M9 4h6"}]],St=[["path",{d:"M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"}]],Lt=[["path",{d:"M13 9a1 1 0 0 1-1-1V5.061a1 1 0 0 0-1.811-.75l-6.835 6.836a1.207 1.207 0 0 0 0 1.707l6.835 6.835a1 1 0 0 0 1.811-.75V16a1 1 0 0 1 1-1h2a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1z"}],["path",{d:"M20 9v6"}]],ft=[["path",{d:"M13 9a1 1 0 0 1-1-1V5.061a1 1 0 0 0-1.811-.75l-6.835 6.836a1.207 1.207 0 0 0 0 1.707l6.835 6.835a1 1 0 0 0 1.811-.75V16a1 1 0 0 1 1-1h6a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1z"}]],kt=[["path",{d:"M11 9a1 1 0 0 0 1-1V5.061a1 1 0 0 1 1.811-.75l6.836 6.836a1.207 1.207 0 0 1 0 1.707l-6.836 6.835a1 1 0 0 1-1.811-.75V16a1 1 0 0 0-1-1H9a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}],["path",{d:"M4 9v6"}]],Pt=[["path",{d:"M11 9a1 1 0 0 0 1-1V5.061a1 1 0 0 1 1.811-.75l6.836 6.836a1.207 1.207 0 0 1 0 1.707l-6.836 6.835a1 1 0 0 1-1.811-.75V16a1 1 0 0 0-1-1H5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}]],Bt=[["path",{d:"M9 13a1 1 0 0 0-1-1H5.061a1 1 0 0 1-.75-1.811l6.836-6.835a1.207 1.207 0 0 1 1.707 0l6.835 6.835a1 1 0 0 1-.75 1.811H16a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"}],["path",{d:"M9 20h6"}]],zt=[["path",{d:"M9 13a1 1 0 0 0-1-1H5.061a1 1 0 0 1-.75-1.811l6.836-6.835a1.207 1.207 0 0 1 1.707 0l6.835 6.835a1 1 0 0 1-.75 1.811H16a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"}]],Ft=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["rect",{x:"15",y:"4",width:"4",height:"6",ry:"2"}],["path",{d:"M17 20v-6h-2"}],["path",{d:"M15 20h4"}]],Dt=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M17 10V4h-2"}],["path",{d:"M15 10h4"}],["rect",{x:"15",y:"14",width:"4",height:"6",ry:"2"}]],u=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M20 8h-5"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10"}],["path",{d:"M15 14h5l-5 6h5"}]],bt=[["path",{d:"M19 3H5"}],["path",{d:"M12 21V7"}],["path",{d:"m6 15 6 6 6-6"}]],Rt=[["path",{d:"M17 7 7 17"}],["path",{d:"M17 17H7V7"}]],Tt=[["path",{d:"m7 7 10 10"}],["path",{d:"M17 7v10H7"}]],qt=[["path",{d:"M12 2v14"}],["path",{d:"m19 9-7 7-7-7"}],["circle",{cx:"12",cy:"21",r:"1"}]],Ut=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M11 4h4"}],["path",{d:"M11 8h7"}],["path",{d:"M11 12h10"}]],Ot=[["path",{d:"M12 17V3"}],["path",{d:"m6 11 6 6 6-6"}],["path",{d:"M19 21H5"}]],Zt=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"m21 8-4-4-4 4"}],["path",{d:"M17 4v16"}]],H=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 20V4"}],["path",{d:"M11 4h10"}],["path",{d:"M11 8h7"}],["path",{d:"M11 12h4"}]],A=[["path",{d:"m3 16 4 4 4-4"}],["path",{d:"M7 4v16"}],["path",{d:"M15 4h5l-5 6h5"}],["path",{d:"M15 20v-3.5a2.5 2.5 0 0 1 5 0V20"}],["path",{d:"M20 18h-5"}]],Gt=[["path",{d:"M12 5v14"}],["path",{d:"m19 12-7 7-7-7"}]],Wt=[["path",{d:"M8 3 4 7l4 4"}],["path",{d:"M4 7h16"}],["path",{d:"m16 21 4-4-4-4"}],["path",{d:"M20 17H4"}]],It=[["path",{d:"m9 6-6 6 6 6"}],["path",{d:"M3 12h14"}],["path",{d:"M21 19V5"}]],Et=[["path",{d:"M3 19V5"}],["path",{d:"m13 6-6 6 6 6"}],["path",{d:"M7 12h14"}]],Xt=[["path",{d:"m12 19-7-7 7-7"}],["path",{d:"M19 12H5"}]],jt=[["path",{d:"M3 5v14"}],["path",{d:"M21 12H7"}],["path",{d:"m15 18 6-6-6-6"}]],Nt=[["path",{d:"m16 3 4 4-4 4"}],["path",{d:"M20 7H4"}],["path",{d:"m8 21-4-4 4-4"}],["path",{d:"M4 17h16"}]],Kt=[["path",{d:"M17 12H3"}],["path",{d:"m11 18 6-6-6-6"}],["path",{d:"M21 5v14"}]],Qt=[["path",{d:"M5 12h14"}],["path",{d:"m12 5 7 7-7 7"}]],Jt=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["rect",{x:"15",y:"4",width:"4",height:"6",ry:"2"}],["path",{d:"M17 20v-6h-2"}],["path",{d:"M15 20h4"}]],Yt=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M17 10V4h-2"}],["path",{d:"M15 10h4"}],["rect",{x:"15",y:"14",width:"4",height:"6",ry:"2"}]],w=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M20 8h-5"}],["path",{d:"M15 10V6.5a2.5 2.5 0 0 1 5 0V10"}],["path",{d:"M15 14h5l-5 6h5"}]],_t=[["path",{d:"m21 16-4 4-4-4"}],["path",{d:"M17 20V4"}],["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}]],xt=[["path",{d:"m5 9 7-7 7 7"}],["path",{d:"M12 16V2"}],["circle",{cx:"12",cy:"21",r:"1"}]],ah=[["path",{d:"m18 9-6-6-6 6"}],["path",{d:"M12 3v14"}],["path",{d:"M5 21h14"}]],th=[["path",{d:"M7 17V7h10"}],["path",{d:"M17 17 7 7"}]],V=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M11 12h4"}],["path",{d:"M11 16h7"}],["path",{d:"M11 20h10"}]],hh=[["path",{d:"M7 7h10v10"}],["path",{d:"M7 17 17 7"}]],dh=[["path",{d:"M5 3h14"}],["path",{d:"m18 13-6-6-6 6"}],["path",{d:"M12 7v14"}]],ch=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M11 12h10"}],["path",{d:"M11 16h7"}],["path",{d:"M11 20h4"}]],S=[["path",{d:"m3 8 4-4 4 4"}],["path",{d:"M7 4v16"}],["path",{d:"M15 4h5l-5 6h5"}],["path",{d:"M15 20v-3.5a2.5 2.5 0 0 1 5 0V20"}],["path",{d:"M20 18h-5"}]],Mh=[["path",{d:"m5 12 7-7 7 7"}],["path",{d:"M12 19V5"}]],ph=[["path",{d:"m4 6 3-3 3 3"}],["path",{d:"M7 17V3"}],["path",{d:"m14 6 3-3 3 3"}],["path",{d:"M17 17V3"}],["path",{d:"M4 21h16"}]],ih=[["path",{d:"M12 6v12"}],["path",{d:"M17.196 9 6.804 15"}],["path",{d:"m6.804 9 10.392 6"}]],nh=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"}]],lh=[["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"}],["path",{d:"M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"}]],eh=[["path",{d:"M2 10v3"}],["path",{d:"M6 6v11"}],["path",{d:"M10 3v18"}],["path",{d:"M14 8v7"}],["path",{d:"M18 5v13"}],["path",{d:"M22 10v3"}]],rh=[["path",{d:"m15.477 12.89 1.515 8.526a.5.5 0 0 1-.81.47l-3.58-2.687a1 1 0 0 0-1.197 0l-3.586 2.686a.5.5 0 0 1-.81-.469l1.514-8.526"}],["circle",{cx:"12",cy:"8",r:"6"}]],oh=[["path",{d:"M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2"}]],vh=[["path",{d:"m14 12-8.381 8.38a1 1 0 0 1-3.001-3L11 9"}],["path",{d:"M15 15.5a.5.5 0 0 0 .5.5A6.5 6.5 0 0 0 22 9.5a.5.5 0 0 0-.5-.5h-1.672a2 2 0 0 1-1.414-.586l-5.062-5.062a1.205 1.205 0 0 0-1.704 0L9.352 5.648a1.205 1.205 0 0 0 0 1.704l5.062 5.062A2 2 0 0 1 15 13.828z"}]],L=[["path",{d:"M13.5 10.5 15 9"}],["path",{d:"M4 4v15a1 1 0 0 0 1 1h15"}],["path",{d:"M4.293 19.707 6 18"}],["path",{d:"m9 15 1.5-1.5"}]],$h=[["path",{d:"M10 16c.5.3 1.2.5 2 .5s1.5-.2 2-.5"}],["path",{d:"M15 12h.01"}],["path",{d:"M19.38 6.813A9 9 0 0 1 20.8 10.2a2 2 0 0 1 0 3.6 9 9 0 0 1-17.6 0 2 2 0 0 1 0-3.6A9 9 0 0 1 12 3c2 0 3.5 1.1 3.5 2.5s-.9 2.5-2 2.5c-.8 0-1.5-.4-1.5-1"}],["path",{d:"M9 12h.01"}]],mh=[["path",{d:"M4 10a4 4 0 0 1 4-4h8a4 4 0 0 1 4 4v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"}],["path",{d:"M8 10h8"}],["path",{d:"M8 18h8"}],["path",{d:"M8 22v-6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v6"}],["path",{d:"M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"}]],yh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]],sh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M12 7v10"}],["path",{d:"M15.4 10a4 4 0 1 0 0 4"}]],f=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m9 12 2 2 4-4"}]],gh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 18V6"}]],Ch=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M7 12h5"}],["path",{d:"M15 9.4a4 4 0 1 0 0 5.2"}]],uh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M8 8h8"}],["path",{d:"M8 12h8"}],["path",{d:"m13 17-5-1h1a4 4 0 0 0 0-8"}]],Hh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"16",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"8",y2:"8"}]],Ah=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m9 8 3 3v7"}],["path",{d:"m12 11 3-3"}],["path",{d:"M9 12h6"}],["path",{d:"M9 16h6"}]],wh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],Vh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],Sh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"12",x2:"12",y1:"8",y2:"16"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],Lh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M8 12h4"}],["path",{d:"M10 16V9.5a2.5 2.5 0 0 1 5 0"}],["path",{d:"M8 16h7"}]],k=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["line",{x1:"12",x2:"12.01",y1:"17",y2:"17"}]],fh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M9 16h5"}],["path",{d:"M9 12h5a2 2 0 1 0 0-4h-3v9"}]],kh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["path",{d:"M11 17V8h4"}],["path",{d:"M11 12h3"}],["path",{d:"M9 16h4"}]],Ph=[["path",{d:"M11 7v10a5 5 0 0 0 5-5"}],["path",{d:"m15 8-6 3"}],["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76"}]],Bh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}],["line",{x1:"15",x2:"9",y1:"9",y2:"15"}],["line",{x1:"9",x2:"15",y1:"9",y2:"15"}]],zh=[["path",{d:"M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"}]],Fh=[["path",{d:"M22 18H6a2 2 0 0 1-2-2V7a2 2 0 0 0-2-2"}],["path",{d:"M17 14V4a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2v10"}],["rect",{width:"13",height:"8",x:"8",y:"6",rx:"1"}],["circle",{cx:"18",cy:"20",r:"2"}],["circle",{cx:"9",cy:"20",r:"2"}]],Dh=[["path",{d:"M12 16v1a2 2 0 0 0 2 2h1a2 2 0 0 1 2 2v1"}],["path",{d:"M12 6a2 2 0 0 1 2 2"}],["path",{d:"M18 8c0 4-3.5 8-6 8s-6-4-6-8a6 6 0 0 1 12 0"}]],bh=[["path",{d:"M4.929 4.929 19.07 19.071"}],["circle",{cx:"12",cy:"12",r:"10"}]],Rh=[["path",{d:"M4 13c3.5-2 8-2 10 2a5.5 5.5 0 0 1 8 5"}],["path",{d:"M5.15 17.89c5.52-1.52 8.65-6.89 7-12C11.55 4 11.5 2 13 2c3.22 0 5 5.5 5 8 0 6.5-4.2 12-10.49 12C5.11 22 2 22 2 20c0-1.5 1.14-1.55 3.15-2.11Z"}]],Th=[["path",{d:"M10 10.01h.01"}],["path",{d:"M10 14.01h.01"}],["path",{d:"M14 10.01h.01"}],["path",{d:"M14 14.01h.01"}],["path",{d:"M18 6v11.5"}],["path",{d:"M6 6v12"}],["rect",{x:"2",y:"6",width:"20",height:"12",rx:"2"}]],qh=[["path",{d:"M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"m16 19 3 3 3-3"}],["path",{d:"M18 12h.01"}],["path",{d:"M19 16v6"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Uh=[["path",{d:"M12 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"M18 12h.01"}],["path",{d:"M19 22v-6"}],["path",{d:"m22 19-3-3-3 3"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Oh=[["path",{d:"M13 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5"}],["path",{d:"m17 17 5 5"}],["path",{d:"M18 12h.01"}],["path",{d:"m22 17-5 5"}],["path",{d:"M6 12h.01"}],["circle",{cx:"12",cy:"12",r:"2"}]],Zh=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M6 12h.01M18 12h.01"}]],Gh=[["path",{d:"M3 5v14"}],["path",{d:"M8 5v14"}],["path",{d:"M12 5v14"}],["path",{d:"M17 5v14"}],["path",{d:"M21 5v14"}]],Wh=[["path",{d:"M10 3a41 41 0 0 0 0 18"}],["path",{d:"M14 3a41 41 0 0 1 0 18"}],["path",{d:"M17 3a2 2 0 0 1 1.68.92 15.25 15.25 0 0 1 0 16.16A2 2 0 0 1 17 21H7a2 2 0 0 1-1.68-.92 15.25 15.25 0 0 1 0-16.16A2 2 0 0 1 7 3z"}],["path",{d:"M3.84 17h16.32"}],["path",{d:"M3.84 7h16.32"}]],Ih=[["path",{d:"M4 20h16"}],["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}]],Eh=[["path",{d:"M10 4 8 6"}],["path",{d:"M17 19v2"}],["path",{d:"M2 12h20"}],["path",{d:"M7 19v2"}],["path",{d:"M9 5 7.621 3.621A2.121 2.121 0 0 0 4 5v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5"}]],Xh=[["path",{d:"m11 7-3 5h4l-3 5"}],["path",{d:"M14.856 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.935"}],["path",{d:"M22 14v-4"}],["path",{d:"M5.14 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.936"}]],jh=[["path",{d:"M10 10v4"}],["path",{d:"M14 10v4"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 10v4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Nh=[["path",{d:"M22 14v-4"}],["path",{d:"M6 14v-4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Kh=[["path",{d:"M10 9v6"}],["path",{d:"M12.543 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.605"}],["path",{d:"M22 14v-4"}],["path",{d:"M7 12h6"}],["path",{d:"M7.606 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.606"}]],Qh=[["path",{d:"M10 14v-4"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 14v-4"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],Jh=[["path",{d:"M10 17h.01"}],["path",{d:"M10 7v6"}],["path",{d:"M14 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2"}],["path",{d:"M22 14v-4"}],["path",{d:"M6 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2"}]],Yh=[["path",{d:"M 22 14 L 22 10"}],["rect",{x:"2",y:"6",width:"16",height:"12",rx:"2"}]],_h=[["path",{d:"M4.5 3h15"}],["path",{d:"M6 3v16a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V3"}],["path",{d:"M6 14h12"}]],xh=[["path",{d:"M9 9c-.64.64-1.521.954-2.402 1.165A6 6 0 0 0 8 22a13.96 13.96 0 0 0 9.9-4.1"}],["path",{d:"M10.75 5.093A6 6 0 0 1 22 8c0 2.411-.61 4.68-1.683 6.66"}],["path",{d:"M5.341 10.62a4 4 0 0 0 6.487 1.208M10.62 5.341a4.015 4.015 0 0 1 2.039 2.04"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],a5=[["path",{d:"M10.165 6.598C9.954 7.478 9.64 8.36 9 9c-.64.64-1.521.954-2.402 1.165A6 6 0 0 0 8 22c7.732 0 14-6.268 14-14a6 6 0 0 0-11.835-1.402Z"}],["path",{d:"M5.341 10.62a4 4 0 1 0 5.279-5.28"}]],t5=[["path",{d:"M2 20v-8a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v8"}],["path",{d:"M4 10V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4"}],["path",{d:"M12 4v6"}],["path",{d:"M2 18h20"}]],h5=[["path",{d:"M3 20v-8a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v8"}],["path",{d:"M5 10V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"}],["path",{d:"M3 18h18"}]],d5=[["path",{d:"M2 4v16"}],["path",{d:"M2 8h18a2 2 0 0 1 2 2v10"}],["path",{d:"M2 17h20"}],["path",{d:"M6 8v9"}]],c5=[["path",{d:"M16.4 13.7A6.5 6.5 0 1 0 6.28 6.6c-1.1 3.13-.78 3.9-3.18 6.08A3 3 0 0 0 5 18c4 0 8.4-1.8 11.4-4.3"}],["path",{d:"m18.5 6 2.19 4.5a6.48 6.48 0 0 1-2.29 7.2C15.4 20.2 11 22 7 22a3 3 0 0 1-2.68-1.66L2.4 16.5"}],["circle",{cx:"12.5",cy:"8.5",r:"2.5"}]],M5=[["path",{d:"M13 13v5"}],["path",{d:"M17 11.47V8"}],["path",{d:"M17 11h1a3 3 0 0 1 2.745 4.211"}],["path",{d:"m2 2 20 20"}],["path",{d:"M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3"}],["path",{d:"M7.536 7.535C6.766 7.649 6.154 8 5.5 8a2.5 2.5 0 0 1-1.768-4.268"}],["path",{d:"M8.727 3.204C9.306 2.767 9.885 2 11 2c1.56 0 2 1.5 3 1.5s1.72-.5 2.5-.5a1 1 0 1 1 0 5c-.78 0-1.5-.5-2.5-.5a3.149 3.149 0 0 0-.842.12"}],["path",{d:"M9 14.6V18"}]],p5=[["path",{d:"M17 11h1a3 3 0 0 1 0 6h-1"}],["path",{d:"M9 12v6"}],["path",{d:"M13 12v6"}],["path",{d:"M14 7.5c-1 0-1.44.5-3 .5s-2-.5-3-.5-1.72.5-2.5.5a2.5 2.5 0 0 1 0-5c.78 0 1.57.5 2.5.5S9.44 2 11 2s2 1.5 3 1.5 1.72-.5 2.5-.5a2.5 2.5 0 0 1 0 5c-.78 0-1.5-.5-2.5-.5Z"}],["path",{d:"M5 8v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8"}]],i5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M13.916 2.314A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.74 7.327A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673 9 9 0 0 1-.585-.665"}],["circle",{cx:"18",cy:"8",r:"3"}]],n5=[["path",{d:"M18.518 17.347A7 7 0 0 1 14 19"}],["path",{d:"M18.8 4A11 11 0 0 1 20 9"}],["path",{d:"M9 9h.01"}],["circle",{cx:"20",cy:"16",r:"2"}],["circle",{cx:"9",cy:"9",r:"7"}],["rect",{x:"4",y:"16",width:"10",height:"6",rx:"2"}]],l5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M15 8h6"}],["path",{d:"M16.243 3.757A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673A9.4 9.4 0 0 1 18.667 12"}]],e5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05"}]],r5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M15 8h6"}],["path",{d:"M18 5v6"}],["path",{d:"M20.002 14.464a9 9 0 0 0 .738.863A1 1 0 0 1 20 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 8.75-5.332"}]],o5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M22 8c0-2.3-.8-4.3-2-6"}],["path",{d:"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"}],["path",{d:"M4 2C2.8 3.7 2 5.7 2 8"}]],v5=[["path",{d:"M10.268 21a2 2 0 0 0 3.464 0"}],["path",{d:"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"}]],P=[["rect",{width:"13",height:"7",x:"3",y:"3",rx:"1"}],["path",{d:"m22 15-3-3 3-3"}],["rect",{width:"13",height:"7",x:"3",y:"14",rx:"1"}]],B=[["rect",{width:"13",height:"7",x:"8",y:"3",rx:"1"}],["path",{d:"m2 9 3 3-3 3"}],["rect",{width:"13",height:"7",x:"8",y:"14",rx:"1"}]],$5=[["rect",{width:"7",height:"13",x:"3",y:"3",rx:"1"}],["path",{d:"m9 22 3-3 3 3"}],["rect",{width:"7",height:"13",x:"14",y:"3",rx:"1"}]],m5=[["rect",{width:"7",height:"13",x:"3",y:"8",rx:"1"}],["path",{d:"m15 2-3 3-3-3"}],["rect",{width:"7",height:"13",x:"14",y:"8",rx:"1"}]],y5=[["path",{d:"M12.409 13.017A5 5 0 0 1 22 15c0 3.866-4 7-9 7-4.077 0-8.153-.82-10.371-2.462-.426-.316-.631-.832-.62-1.362C2.118 12.723 2.627 2 10 2a3 3 0 0 1 3 3 2 2 0 0 1-2 2c-1.105 0-1.64-.444-2-1"}],["path",{d:"M15 14a5 5 0 0 0-7.584 2"}],["path",{d:"M9.964 6.825C8.019 7.977 9.5 13 8 15"}]],s5=[["circle",{cx:"18.5",cy:"17.5",r:"3.5"}],["circle",{cx:"5.5",cy:"17.5",r:"3.5"}],["circle",{cx:"15",cy:"5",r:"1"}],["path",{d:"M12 17.5V14l-3-3 4-3 2 3h2"}]],g5=[["rect",{x:"14",y:"14",width:"4",height:"6",rx:"2"}],["rect",{x:"6",y:"4",width:"4",height:"6",rx:"2"}],["path",{d:"M6 20h4"}],["path",{d:"M14 10h4"}],["path",{d:"M6 14h2v6"}],["path",{d:"M14 4h2v6"}]],C5=[["circle",{cx:"12",cy:"11.9",r:"2"}],["path",{d:"M6.7 3.4c-.9 2.5 0 5.2 2.2 6.7C6.5 9 3.7 9.6 2 11.6"}],["path",{d:"m8.9 10.1 1.4.8"}],["path",{d:"M17.3 3.4c.9 2.5 0 5.2-2.2 6.7 2.4-1.2 5.2-.6 6.9 1.5"}],["path",{d:"m15.1 10.1-1.4.8"}],["path",{d:"M16.7 20.8c-2.6-.4-4.6-2.6-4.7-5.3-.2 2.6-2.1 4.8-4.7 5.2"}],["path",{d:"M12 13.9v1.6"}],["path",{d:"M13.5 5.4c-1-.2-2-.2-3 0"}],["path",{d:"M17 16.4c.7-.7 1.2-1.6 1.5-2.5"}],["path",{d:"M5.5 13.9c.3.9.8 1.8 1.5 2.5"}]],u5=[["path",{d:"M10 10h4"}],["path",{d:"M19 7V4a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3"}],["path",{d:"M20 21a2 2 0 0 0 2-2v-3.851c0-1.39-2-2.962-2-4.829V8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v11a2 2 0 0 0 2 2z"}],["path",{d:"M 22 16 L 2 16"}],["path",{d:"M4 21a2 2 0 0 1-2-2v-3.851c0-1.39 2-2.962 2-4.829V8a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v11a2 2 0 0 1-2 2z"}],["path",{d:"M9 7V4a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v3"}]],H5=[["path",{d:"M12 18v4"}],["path",{d:"m17 18 1.956-11.468"}],["path",{d:"m3 8 7.82-5.615a2 2 0 0 1 2.36 0L21 8"}],["path",{d:"M4 18h16"}],["path",{d:"M7 18 5.044 6.532"}],["circle",{cx:"12",cy:"10",r:"2"}]],A5=[["path",{d:"M16 7h.01"}],["path",{d:"M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20"}],["path",{d:"m20 7 2 .5-2 .5"}],["path",{d:"M10 18v3"}],["path",{d:"M14 17.75V21"}],["path",{d:"M7 18a6 6 0 0 0 3.84-10.61"}]],w5=[["path",{d:"M11.767 19.089c4.924.868 6.14-6.025 1.216-6.894m-1.216 6.894L5.86 18.047m5.908 1.042-.347 1.97m1.563-8.864c4.924.869 6.14-6.025 1.215-6.893m-1.215 6.893-3.94-.694m5.155-6.2L8.29 4.26m5.908 1.042.348-1.97M7.48 20.364l3.126-17.727"}]],V5=[["circle",{cx:"9",cy:"9",r:"7"}],["circle",{cx:"15",cy:"15",r:"7"}]],S5=[["path",{d:"M3 3h18"}],["path",{d:"M20 7H8"}],["path",{d:"M20 11H8"}],["path",{d:"M10 19h10"}],["path",{d:"M8 15h12"}],["path",{d:"M4 3v14"}],["circle",{cx:"4",cy:"19",r:"2"}]],L5=[["path",{d:"M10 22V7a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-5a1 1 0 0 0-1-1H2"}],["rect",{x:"14",y:"2",width:"8",height:"8",rx:"1"}]],f5=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}],["line",{x1:"18",x2:"21",y1:"12",y2:"12"}],["line",{x1:"3",x2:"6",y1:"12",y2:"12"}]],k5=[["path",{d:"m17 17-5 5V12l-5 5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M14.5 9.5 17 7l-5-5v4.5"}]],P5=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}],["path",{d:"M20.83 14.83a4 4 0 0 0 0-5.66"}],["path",{d:"M18 12h.01"}]],B5=[["path",{d:"m7 7 10 10-5 5V2l5 5L7 17"}]],z5=[["path",{d:"M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"}]],F5=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}],["circle",{cx:"12",cy:"12",r:"4"}]],D5=[["circle",{cx:"11",cy:"13",r:"9"}],["path",{d:"M14.35 4.65 16.3 2.7a2.41 2.41 0 0 1 3.4 0l1.6 1.6a2.4 2.4 0 0 1 0 3.4l-1.95 1.95"}],["path",{d:"m22 2-1.5 1.5"}]],b5=[["path",{d:"M17 10c.7-.7 1.69 0 2.5 0a2.5 2.5 0 1 0 0-5 .5.5 0 0 1-.5-.5 2.5 2.5 0 1 0-5 0c0 .81.7 1.8 0 2.5l-7 7c-.7.7-1.69 0-2.5 0a2.5 2.5 0 0 0 0 5c.28 0 .5.22.5.5a2.5 2.5 0 1 0 5 0c0-.81-.7-1.8 0-2.5Z"}]],R5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m8 13 4-7 4 7"}],["path",{d:"M9.1 11h5.7"}]],T5=[["path",{d:"M12 13h.01"}],["path",{d:"M12 6v3"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],q5=[["path",{d:"M12 6v7"}],["path",{d:"M16 8v3"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 8v3"}]],U5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 9.5 2 2 4-4"}]],O5=[["path",{d:"M5 7a2 2 0 0 0-2 2v11"}],["path",{d:"M5.803 18H5a2 2 0 0 0 0 4h9.5a.5.5 0 0 0 .5-.5V21"}],["path",{d:"M9 15V4a2 2 0 0 1 2-2h9.5a.5.5 0 0 1 .5.5v14a.5.5 0 0 1-.5.5H11a2 2 0 0 1 0-4h10"}]],z=[["path",{d:"M12 17h1.5"}],["path",{d:"M12 22h1.5"}],["path",{d:"M12 2h1.5"}],["path",{d:"M17.5 22H19a1 1 0 0 0 1-1"}],["path",{d:"M17.5 2H19a1 1 0 0 1 1 1v1.5"}],["path",{d:"M20 14v3h-2.5"}],["path",{d:"M20 8.5V10"}],["path",{d:"M4 10V8.5"}],["path",{d:"M4 19.5V14"}],["path",{d:"M4 4.5A2.5 2.5 0 0 1 6.5 2H8"}],["path",{d:"M8 22H6.5a1 1 0 0 1 0-5H8"}]],Z5=[["path",{d:"M12 13V7"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 10 3 3 3-3"}]],G5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 12v-2a4 4 0 0 1 8 0v2"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"9",cy:"12",r:"1"}]],W5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8.62 9.8A2.25 2.25 0 1 1 12 6.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}]],I5=[["path",{d:"m19 3 1 1"}],["path",{d:"m20 2-4.5 4.5"}],["path",{d:"M20 7.898V21a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2h7.844"}],["circle",{cx:"14",cy:"8",r:"2"}]],E5=[["path",{d:"m20 13.7-2.1-2.1a2 2 0 0 0-2.8 0L9.7 17"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["circle",{cx:"10",cy:"8",r:"2"}]],X5=[["path",{d:"M18 6V4a2 2 0 1 0-4 0v2"}],["path",{d:"M20 15v6a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H10"}],["rect",{x:"12",y:"6",width:"8",height:"5",rx:"1"}]],j5=[["path",{d:"M10 2v8l3-3 3 3V2"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],N5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M9 10h6"}]],K5=[["path",{d:"M12 21V7"}],["path",{d:"m16 12 2 2 4-4"}],["path",{d:"M22 6V4a1 1 0 0 0-1-1h-5a4 4 0 0 0-4 4 4 4 0 0 0-4-4H3a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h6a3 3 0 0 1 3 3 3 3 0 0 1 3-3h6a1 1 0 0 0 1-1v-1.3"}]],Q5=[["path",{d:"M12 7v14"}],["path",{d:"M16 12h2"}],["path",{d:"M16 8h2"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"}],["path",{d:"M6 12h2"}],["path",{d:"M6 8h2"}]],J5=[["path",{d:"M12 7v14"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"}]],Y5=[["path",{d:"M12 7v6"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M9 10h6"}]],_5=[["path",{d:"M11 22H5.5a1 1 0 0 1 0-5h4.501"}],["path",{d:"m21 22-1.879-1.878"}],["path",{d:"M3 19.5v-15A2.5 2.5 0 0 1 5.5 2H18a1 1 0 0 1 1 1v8"}],["circle",{cx:"17",cy:"18",r:"3"}]],x5=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M8 11h8"}],["path",{d:"M8 7h6"}]],a4=[["path",{d:"M12 13V7"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9 10 3-3 3 3"}]],t4=[["path",{d:"M10 13h4"}],["path",{d:"M12 6v7"}],["path",{d:"M16 8V6H8v2"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],h4=[["path",{d:"M12 13V7"}],["path",{d:"M18 2h1a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2"}],["path",{d:"m9 10 3-3 3 3"}],["path",{d:"m9 5 3-3 3 3"}]],d4=[["path",{d:"M15 13a3 3 0 1 0-6 0"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["circle",{cx:"12",cy:"8",r:"2"}]],c4=[["path",{d:"m14.5 7-5 5"}],["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}],["path",{d:"m9.5 7 5 5"}]],M4=[["path",{d:"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"}]],p4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"}],["path",{d:"m9 10 2 2 4-4"}]],i4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}],["line",{x1:"15",x2:"9",y1:"10",y2:"10"}]],n4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}],["line",{x1:"12",x2:"12",y1:"7",y2:"13"}],["line",{x1:"15",x2:"9",y1:"10",y2:"10"}]],l4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"}],["path",{d:"m14.5 7.5-5 5"}],["path",{d:"m9.5 7.5 5 5"}]],e4=[["path",{d:"m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"}]],r4=[["path",{d:"M4 9V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4"}],["path",{d:"M8 8v1"}],["path",{d:"M12 8v1"}],["path",{d:"M16 8v1"}],["rect",{width:"20",height:"12",x:"2",y:"9",rx:"2"}],["circle",{cx:"8",cy:"15",r:"2"}],["circle",{cx:"16",cy:"15",r:"2"}]],o4=[["path",{d:"M12 6V2H8"}],["path",{d:"M15 11v2"}],["path",{d:"M2 12h2"}],["path",{d:"M20 12h2"}],["path",{d:"M20 16a2 2 0 0 1-2 2H8.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 4 20.286V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z"}],["path",{d:"M9 11v2"}]],v4=[["path",{d:"M13.67 8H18a2 2 0 0 1 2 2v4.33"}],["path",{d:"M2 14h2"}],["path",{d:"M20 14h2"}],["path",{d:"M22 22 2 2"}],["path",{d:"M8 8H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 1.414-.586"}],["path",{d:"M9 13v2"}],["path",{d:"M9.67 4H12v2.33"}]],$4=[["path",{d:"M12 8V4H8"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2"}],["path",{d:"M2 14h2"}],["path",{d:"M20 14h2"}],["path",{d:"M15 13v2"}],["path",{d:"M9 13v2"}]],m4=[["path",{d:"M10 3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2a6 6 0 0 0 1.2 3.6l.6.8A6 6 0 0 1 17 13v8a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-8a6 6 0 0 1 1.2-3.6l.6-.8A6 6 0 0 0 10 5z"}],["path",{d:"M17 13h-4a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h4"}]],y4=[["path",{d:"M17 3h4v4"}],["path",{d:"M18.575 11.082a13 13 0 0 1 1.048 9.027 1.17 1.17 0 0 1-1.914.597L14 17"}],["path",{d:"M7 10 3.29 6.29a1.17 1.17 0 0 1 .6-1.91 13 13 0 0 1 9.03 1.05"}],["path",{d:"M7 14a1.7 1.7 0 0 0-1.207.5l-2.646 2.646A.5.5 0 0 0 3.5 18H5a1 1 0 0 1 1 1v1.5a.5.5 0 0 0 .854.354L9.5 18.207A1.7 1.7 0 0 0 10 17v-2a1 1 0 0 0-1-1z"}],["path",{d:"M9.707 14.293 21 3"}]],s4=[["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"}],["path",{d:"m3.3 7 8.7 5 8.7-5"}],["path",{d:"M12 22V12"}]],g4=[["path",{d:"M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z"}],["path",{d:"m7 16.5-4.74-2.85"}],["path",{d:"m7 16.5 5-3"}],["path",{d:"M7 16.5v5.17"}],["path",{d:"M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z"}],["path",{d:"m17 16.5-5-3"}],["path",{d:"m17 16.5 4.74-2.85"}],["path",{d:"M17 16.5v5.17"}],["path",{d:"M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z"}],["path",{d:"M12 8 7.26 5.15"}],["path",{d:"m12 8 4.74-2.85"}],["path",{d:"M12 13.5V8"}]],F=[["path",{d:"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"}],["path",{d:"M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1"}]],C4=[["path",{d:"M16 3h3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-3"}],["path",{d:"M8 21H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h3"}]],u4=[["path",{d:"M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"}],["path",{d:"M9 13a4.5 4.5 0 0 0 3-4"}],["path",{d:"M6.003 5.125A3 3 0 0 0 6.401 6.5"}],["path",{d:"M3.477 10.896a4 4 0 0 1 .585-.396"}],["path",{d:"M6 18a4 4 0 0 1-1.967-.516"}],["path",{d:"M12 13h4"}],["path",{d:"M12 18h6a2 2 0 0 1 2 2v1"}],["path",{d:"M12 8h8"}],["path",{d:"M16 8V5a2 2 0 0 1 2-2"}],["circle",{cx:"16",cy:"13",r:".5"}],["circle",{cx:"18",cy:"3",r:".5"}],["circle",{cx:"20",cy:"21",r:".5"}],["circle",{cx:"20",cy:"8",r:".5"}]],H4=[["path",{d:"m10.852 14.772-.383.923"}],["path",{d:"m10.852 9.228-.383-.923"}],["path",{d:"m13.148 14.772.382.924"}],["path",{d:"m13.531 8.305-.383.923"}],["path",{d:"m14.772 10.852.923-.383"}],["path",{d:"m14.772 13.148.923.383"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 0 0-5.63-1.446 3 3 0 0 0-.368 1.571 4 4 0 0 0-2.525 5.771"}],["path",{d:"M17.998 5.125a4 4 0 0 1 2.525 5.771"}],["path",{d:"M19.505 10.294a4 4 0 0 1-1.5 7.706"}],["path",{d:"M4.032 17.483A4 4 0 0 0 11.464 20c.18-.311.892-.311 1.072 0a4 4 0 0 0 7.432-2.516"}],["path",{d:"M4.5 10.291A4 4 0 0 0 6 18"}],["path",{d:"M6.002 5.125a3 3 0 0 0 .4 1.375"}],["path",{d:"m9.228 10.852-.923-.383"}],["path",{d:"m9.228 13.148-.923.383"}],["circle",{cx:"12",cy:"12",r:"3"}]],A4=[["path",{d:"M12 18V5"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77"}]],w4=[["path",{d:"M16 3v2.107"}],["path",{d:"M17 9c1 3 2.5 3.5 3.5 4.5A5 5 0 0 1 22 17a5 5 0 0 1-10 0c0-.3 0-.6.1-.9a2 2 0 1 0 3.3-2C13 11.5 16 9 17 9"}],["path",{d:"M21 8.274V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.938"}],["path",{d:"M3 15h5.253"}],["path",{d:"M3 9h8.228"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],V4=[["path",{d:"M12 9v1.258"}],["path",{d:"M16 3v5.46"}],["path",{d:"M21 9.118V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h5.75"}],["path",{d:"M22 17.5c0 2.499-1.75 3.749-3.83 4.474a.5.5 0 0 1-.335-.005c-2.085-.72-3.835-1.97-3.835-4.47V14a.5.5 0 0 1 .5-.499c1 0 2.25-.6 3.12-1.36a.6.6 0 0 1 .76-.001c.875.765 2.12 1.36 3.12 1.36a.5.5 0 0 1 .5.5z"}],["path",{d:"M3 15h7"}],["path",{d:"M3 9h12.142"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],S4=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 9v6"}],["path",{d:"M16 15v6"}],["path",{d:"M16 3v6"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["path",{d:"M8 15v6"}],["path",{d:"M8 3v6"}]],L4=[["path",{d:"M12 12h.01"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M22 13a18.15 18.15 0 0 1-20 0"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],f4=[["path",{d:"M10 20v2"}],["path",{d:"M14 20v2"}],["path",{d:"M18 20v2"}],["path",{d:"M21 20H3"}],["path",{d:"M6 20v2"}],["path",{d:"M8 16V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v12"}],["rect",{x:"4",y:"6",width:"16",height:"10",rx:"2"}]],k4=[["path",{d:"M12 11v4"}],["path",{d:"M14 13h-4"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M18 6v14"}],["path",{d:"M6 6v14"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],P4=[["path",{d:"M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"}],["rect",{width:"20",height:"14",x:"2",y:"6",rx:"2"}]],B4=[["rect",{x:"8",y:"8",width:"8",height:"8",rx:"2"}],["path",{d:"M4 10a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2"}],["path",{d:"M14 20a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2"}]],z4=[["path",{d:"m16 22-1-4"}],["path",{d:"M19 14a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v1a1 1 0 0 0 1 1"}],["path",{d:"M19 14H5l-1.973 6.767A1 1 0 0 0 4 22h16a1 1 0 0 0 .973-1.233z"}],["path",{d:"m8 22 1-4"}]],F4=[["path",{d:"m11 10 3 3"}],["path",{d:"M6.5 21A3.5 3.5 0 1 0 3 17.5a2.62 2.62 0 0 1-.708 1.792A1 1 0 0 0 3 21z"}],["path",{d:"M9.969 17.031 21.378 5.624a1 1 0 0 0-3.002-3.002L6.967 14.031"}]],D4=[["path",{d:"M7.001 15.085A1.5 1.5 0 0 1 9 16.5"}],["circle",{cx:"18.5",cy:"8.5",r:"3.5"}],["circle",{cx:"7.5",cy:"16.5",r:"5.5"}],["circle",{cx:"7.5",cy:"4.5",r:"2.5"}]],b4=[["path",{d:"M12 20v-8"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M15 7.13V6a3 3 0 0 0-5.14-2.1L8 2"}],["path",{d:"M18 12.34V11a4 4 0 0 0-4-4h-1.3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M22 13h-3.34"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M6 13H2"}],["path",{d:"M7.7 7.7A4 4 0 0 0 6 11v3a6 6 0 0 0 11.13 3.13"}]],R4=[["path",{d:"M10 19.655A6 6 0 0 1 6 14v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 3.97"}],["path",{d:"M14 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M3 5a4 4 0 0 0 3.55 3.97"}],["path",{d:"M6 13H2"}],["path",{d:"m8 2 1.88 1.88"}],["path",{d:"M9 7.13V6a3 3 0 1 1 6 0v1.13"}]],T4=[["path",{d:"M12 20v-9"}],["path",{d:"M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"}],["path",{d:"M14.12 3.88 16 2"}],["path",{d:"M21 21a4 4 0 0 0-3.81-4"}],["path",{d:"M21 5a4 4 0 0 1-3.55 3.97"}],["path",{d:"M22 13h-4"}],["path",{d:"M3 21a4 4 0 0 1 3.81-4"}],["path",{d:"M3 5a4 4 0 0 0 3.55 3.97"}],["path",{d:"M6 13H2"}],["path",{d:"m8 2 1.88 1.88"}],["path",{d:"M9 7.13V6a3 3 0 1 1 6 0v1.13"}]],q4=[["path",{d:"M10 12h4"}],["path",{d:"M10 8h4"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M6 10H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-2"}],["path",{d:"M6 21V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v16"}]],U4=[["path",{d:"M12 10h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M12 6h.01"}],["path",{d:"M16 10h.01"}],["path",{d:"M16 14h.01"}],["path",{d:"M16 6h.01"}],["path",{d:"M8 10h.01"}],["path",{d:"M8 14h.01"}],["path",{d:"M8 6h.01"}],["path",{d:"M9 22v-3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2"}]],O4=[["path",{d:"M4 6 2 7"}],["path",{d:"M10 6h4"}],["path",{d:"m22 7-2-1"}],["rect",{width:"16",height:"16",x:"4",y:"3",rx:"2"}],["path",{d:"M4 11h16"}],["path",{d:"M8 15h.01"}],["path",{d:"M16 15h.01"}],["path",{d:"M6 19v2"}],["path",{d:"M18 21v-2"}]],Z4=[["path",{d:"M8 6v6"}],["path",{d:"M15 6v6"}],["path",{d:"M2 12h19.6"}],["path",{d:"M18 18h3s.5-1.7.8-2.8c.1-.4.2-.8.2-1.2 0-.4-.1-.8-.2-1.2l-1.4-5C20.1 6.8 19.1 6 18 6H4a2 2 0 0 0-2 2v10h3"}],["circle",{cx:"7",cy:"18",r:"2"}],["path",{d:"M9 18h5"}],["circle",{cx:"16",cy:"18",r:"2"}]],G4=[["path",{d:"M10 3h.01"}],["path",{d:"M14 2h.01"}],["path",{d:"m2 9 20-5"}],["path",{d:"M12 12V6.5"}],["rect",{width:"16",height:"10",x:"4",y:"12",rx:"3"}],["path",{d:"M9 12v5"}],["path",{d:"M15 12v5"}],["path",{d:"M4 17h16"}]],W4=[["path",{d:"M17 19a1 1 0 0 1-1-1v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2a1 1 0 0 1-1 1z"}],["path",{d:"M17 21v-2"}],["path",{d:"M19 14V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V10"}],["path",{d:"M21 21v-2"}],["path",{d:"M3 5V3"}],["path",{d:"M4 10a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2z"}],["path",{d:"M7 5V3"}]],I4=[["path",{d:"M16 13H3"}],["path",{d:"M16 17H3"}],["path",{d:"m7.2 7.9-3.388 2.5A2 2 0 0 0 3 12.01V20a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-8.654c0-2-2.44-6.026-6.44-8.026a1 1 0 0 0-1.082.057L10.4 5.6"}],["circle",{cx:"9",cy:"7",r:"2"}]],E4=[["path",{d:"M20 21v-8a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8"}],["path",{d:"M4 16s.5-1 2-1 2.5 2 4 2 2.5-2 4-2 2.5 2 4 2 2-1 2-1"}],["path",{d:"M2 21h20"}],["path",{d:"M7 8v3"}],["path",{d:"M12 8v3"}],["path",{d:"M17 8v3"}],["path",{d:"M7 4h.01"}],["path",{d:"M12 4h.01"}],["path",{d:"M17 4h.01"}]],X4=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["line",{x1:"8",x2:"16",y1:"6",y2:"6"}],["line",{x1:"16",x2:"16",y1:"14",y2:"18"}],["path",{d:"M16 10h.01"}],["path",{d:"M12 10h.01"}],["path",{d:"M8 10h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M8 14h.01"}],["path",{d:"M12 18h.01"}],["path",{d:"M8 18h.01"}]],j4=[["path",{d:"M11 14h1v4"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],N4=[["path",{d:"m14 18 4 4 4-4"}],["path",{d:"M16 2v4"}],["path",{d:"M18 14v8"}],["path",{d:"M21 11.354V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.343"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],K4=[["path",{d:"m14 18 4-4 4 4"}],["path",{d:"M16 2v4"}],["path",{d:"M18 22v-8"}],["path",{d:"M21 11.343V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],Q4=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8"}],["path",{d:"M3 10h18"}],["path",{d:"m16 20 2 2 4-4"}]],J4=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"m9 16 2 2 4-4"}]],Y4=[["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m15.228 19.148-.923.383"}],["path",{d:"M16 2v4"}],["path",{d:"m16.47 14.305.382.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["path",{d:"M21 10.592V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["circle",{cx:"18",cy:"18",r:"3"}]],_4=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M16 2v4"}],["path",{d:"M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5"}],["path",{d:"M3 10h5"}],["path",{d:"M8 2v4"}],["circle",{cx:"16",cy:"16",r:"6"}]],x4=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M8 14h.01"}],["path",{d:"M12 14h.01"}],["path",{d:"M16 14h.01"}],["path",{d:"M8 18h.01"}],["path",{d:"M12 18h.01"}],["path",{d:"M16 18h.01"}]],a3=[["path",{d:"M3 20a2 2 0 0 0 2 2h10a2.4 2.4 0 0 0 1.706-.706l3.588-3.588A2.4 2.4 0 0 0 21 16V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z"}],["path",{d:"M15 22v-5a1 1 0 0 1 1-1h5"}],["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}]],t3=[["path",{d:"M12.127 22H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.125"}],["path",{d:"M14.62 18.8A2.25 2.25 0 1 1 18 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],h3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M10 16h4"}]],d3=[["path",{d:"M16 19h6"}],["path",{d:"M16 2v4"}],["path",{d:"M21 15V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],c3=[["path",{d:"M4.2 4.2A2 2 0 0 0 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 1.82-1.18"}],["path",{d:"M21 15.5V6a2 2 0 0 0-2-2H9.5"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h7"}],["path",{d:"M21 10h-5.5"}],["path",{d:"m2 2 20 20"}]],M3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"M10 16h4"}],["path",{d:"M12 14v4"}]],p3=[["path",{d:"M16 19h6"}],["path",{d:"M16 2v4"}],["path",{d:"M19 16v6"}],["path",{d:"M21 12.598V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8.5"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}]],i3=[["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M16 2v4"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["path",{d:"M17 14h-6"}],["path",{d:"M13 18H7"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 18h.01"}]],n3=[["path",{d:"M16 2v4"}],["path",{d:"M21 11.75V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.25"}],["path",{d:"m22 22-1.875-1.875"}],["path",{d:"M3 10h18"}],["path",{d:"M8 2v4"}],["circle",{cx:"18",cy:"18",r:"3"}]],l3=[["path",{d:"M11 10v4h4"}],["path",{d:"m11 14 1.535-1.605a5 5 0 0 1 8 1.5"}],["path",{d:"M16 2v4"}],["path",{d:"m21 18-1.535 1.605a5 5 0 0 1-8-1.5"}],["path",{d:"M21 22v-4h-4"}],["path",{d:"M21 8.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h4.3"}],["path",{d:"M3 10h4"}],["path",{d:"M8 2v4"}]],e3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M21 13V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8"}],["path",{d:"M3 10h18"}],["path",{d:"m17 22 5-5"}],["path",{d:"m17 17 5 5"}]],r3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}],["path",{d:"m14 14-4 4"}],["path",{d:"m10 14 4 4"}]],o3=[["path",{d:"M8 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"18",height:"18",x:"3",y:"4",rx:"2"}],["path",{d:"M3 10h18"}]],v3=[["path",{d:"M12 2v2"}],["path",{d:"M15.726 21.01A2 2 0 0 1 14 22H4a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2"}],["path",{d:"M18 2v2"}],["path",{d:"M2 13h2"}],["path",{d:"M8 8h14"}],["rect",{x:"8",y:"3",width:"14",height:"14",rx:"2"}]],$3=[["path",{d:"M14.564 14.558a3 3 0 1 1-4.122-4.121"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 .819-.175"}],["path",{d:"M9.695 4.024A2 2 0 0 1 10.004 4h3.993a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v7.344"}]],m3=[["path",{d:"M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"}],["circle",{cx:"12",cy:"13",r:"3"}]],y3=[["path",{d:"M5.7 21a2 2 0 0 1-3.5-2l8.6-14a6 6 0 0 1 10.4 6 2 2 0 1 1-3.464-2 2 2 0 1 0-3.464-2Z"}],["path",{d:"M17.75 7 15 2.1"}],["path",{d:"M10.9 4.8 13 9"}],["path",{d:"m7.9 9.7 2 4.4"}],["path",{d:"M4.9 14.7 7 18.9"}]],s3=[["path",{d:"M10 10v7.9"}],["path",{d:"M11.802 6.145a5 5 0 0 1 6.053 6.053"}],["path",{d:"M14 6.1v2.243"}],["path",{d:"m15.5 15.571-.964.964a5 5 0 0 1-7.071 0 5 5 0 0 1 0-7.07l.964-.965"}],["path",{d:"M16 7V3a1 1 0 0 1 1.707-.707 2.5 2.5 0 0 0 2.152.717 1 1 0 0 1 1.131 1.131 2.5 2.5 0 0 0 .717 2.152A1 1 0 0 1 21 8h-4"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8 17v4a1 1 0 0 1-1.707.707 2.5 2.5 0 0 0-2.152-.717 1 1 0 0 1-1.131-1.131 2.5 2.5 0 0 0-.717-2.152A1 1 0 0 1 3 16h4"}]],g3=[["path",{d:"M10 7v10.9"}],["path",{d:"M14 6.1V17"}],["path",{d:"M16 7V3a1 1 0 0 1 1.707-.707 2.5 2.5 0 0 0 2.152.717 1 1 0 0 1 1.131 1.131 2.5 2.5 0 0 0 .717 2.152A1 1 0 0 1 21 8h-4"}],["path",{d:"M16.536 7.465a5 5 0 0 0-7.072 0l-2 2a5 5 0 0 0 0 7.07 5 5 0 0 0 7.072 0l2-2a5 5 0 0 0 0-7.07"}],["path",{d:"M8 17v4a1 1 0 0 1-1.707.707 2.5 2.5 0 0 0-2.152-.717 1 1 0 0 1-1.131-1.131 2.5 2.5 0 0 0-.717-2.152A1 1 0 0 1 3 16h4"}]],C3=[["path",{d:"M12 22v-4c1.5 1.5 3.5 3 6 3 0-1.5-.5-3.5-2-5"}],["path",{d:"M13.988 8.327C13.902 6.054 13.365 3.82 12 2a9.3 9.3 0 0 0-1.445 2.9"}],["path",{d:"M17.375 11.725C18.882 10.53 21 7.841 21 6c-2.324 0-5.08 1.296-6.662 2.684"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21.024 15.378A15 15 0 0 0 22 15c-.426-1.279-2.67-2.557-4.25-2.907"}],["path",{d:"M6.995 6.992C5.714 6.4 4.29 6 3 6c0 2 2.5 5 4 6-1.5 0-4.5 1.5-5 3 3.5 1.5 6 1 6 1-1.5 1.5-2 3.5-2 5 2.5 0 4.5-1.5 6-3"}]],u3=[["path",{d:"M12 22v-4"}],["path",{d:"M7 12c-1.5 0-4.5 1.5-5 3 3.5 1.5 6 1 6 1-1.5 1.5-2 3.5-2 5 2.5 0 4.5-1.5 6-3 1.5 1.5 3.5 3 6 3 0-1.5-.5-3.5-2-5 0 0 2.5.5 6-1-.5-1.5-3.5-3-5-3 1.5-1 4-4 4-6-2.5 0-5.5 1.5-7 3 0-2.5-.5-5-2-7-1.5 2-2 4.5-2 7-1.5-1.5-4.5-3-7-3 0 2 2.5 5 4 6"}]],H3=[["path",{d:"M10.5 5H19a2 2 0 0 1 2 2v8.5"}],["path",{d:"M17 11h-.5"}],["path",{d:"M19 19H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7 11h4"}],["path",{d:"M7 15h2.5"}]],D=[["rect",{width:"18",height:"14",x:"3",y:"5",rx:"2",ry:"2"}],["path",{d:"M7 15h4M15 15h2M7 11h2M13 11h4"}]],A3=[["path",{d:"m21 8-2 2-1.5-3.7A2 2 0 0 0 15.646 5H8.4a2 2 0 0 0-1.903 1.257L5 10 3 8"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 14h.01"}],["rect",{width:"18",height:"8",x:"3",y:"10",rx:"2"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],w3=[["path",{d:"M10 2h4"}],["path",{d:"m21 8-2 2-1.5-3.7A2 2 0 0 0 15.646 5H8.4a2 2 0 0 0-1.903 1.257L5 10 3 8"}],["path",{d:"M7 14h.01"}],["path",{d:"M17 14h.01"}],["rect",{width:"18",height:"8",x:"3",y:"10",rx:"2"}],["path",{d:"M5 18v2"}],["path",{d:"M19 18v2"}]],V3=[["path",{d:"M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"}],["circle",{cx:"7",cy:"17",r:"2"}],["path",{d:"M9 17h6"}],["circle",{cx:"17",cy:"17",r:"2"}]],S3=[["path",{d:"M18 19V9a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v8a2 2 0 0 0 2 2h2"}],["path",{d:"M2 9h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H2"}],["path",{d:"M22 17v1a1 1 0 0 1-1 1H10v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9"}],["circle",{cx:"8",cy:"19",r:"2"}]],L3=[["path",{d:"M12 14v4"}],["path",{d:"M14.172 2a2 2 0 0 1 1.414.586l3.828 3.828A2 2 0 0 1 20 7.828V20a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"}],["path",{d:"M8 14h8"}],["rect",{x:"8",y:"10",width:"8",height:"8",rx:"1"}]],f3=[["path",{d:"M2.27 21.7s9.87-3.5 12.73-6.36a4.5 4.5 0 0 0-6.36-6.37C5.77 11.84 2.27 21.7 2.27 21.7zM8.64 14l-2.05-2.04M15.34 15l-2.46-2.46"}],["path",{d:"M22 9s-1.33-2-3.5-2C16.86 7 15 9 15 9s1.33 2 3.5 2S22 9 22 9z"}],["path",{d:"M15 2s-2 1.33-2 3.5S15 9 15 9s2-1.84 2-3.5C17 3.33 15 2 15 2z"}]],k3=[["path",{d:"M10 9v7"}],["path",{d:"M14 6v10"}],["circle",{cx:"17.5",cy:"12.5",r:"3.5"}],["circle",{cx:"6.5",cy:"12.5",r:"3.5"}]],P3=[["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M22 9v7"}],["path",{d:"M3.304 13h6.392"}],["circle",{cx:"18.5",cy:"12.5",r:"3.5"}]],B3=[["path",{d:"M15 11h4.5a1 1 0 0 1 0 5h-4a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h3a1 1 0 0 1 0 5"}],["path",{d:"m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"}],["path",{d:"M3.304 13h6.392"}]],z3=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["circle",{cx:"8",cy:"10",r:"2"}],["path",{d:"M8 12h8"}],["circle",{cx:"16",cy:"10",r:"2"}],["path",{d:"m6 20 .7-2.9A1.4 1.4 0 0 1 8.1 16h7.8a1.4 1.4 0 0 1 1.4 1l.7 3"}]],F3=[["path",{d:"M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"}],["path",{d:"M2 12a9 9 0 0 1 8 8"}],["path",{d:"M2 16a5 5 0 0 1 4 4"}],["line",{x1:"2",x2:"2.01",y1:"20",y2:"20"}]],D3=[["path",{d:"M10 5V3"}],["path",{d:"M14 5V3"}],["path",{d:"M15 21v-3a3 3 0 0 0-6 0v3"}],["path",{d:"M18 3v8"}],["path",{d:"M18 5H6"}],["path",{d:"M22 11H2"}],["path",{d:"M22 9v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9"}],["path",{d:"M6 3v8"}]],b3=[["path",{d:"M12 5c.67 0 1.35.09 2 .26 1.78-2 5.03-2.84 6.42-2.26 1.4.58-.42 7-.42 7 .57 1.07 1 2.24 1 3.44C21 17.9 16.97 21 12 21s-9-3-9-7.56c0-1.25.5-2.4 1-3.44 0 0-1.89-6.42-.5-7 1.39-.58 4.72.23 6.5 2.23A9.04 9.04 0 0 1 12 5Z"}],["path",{d:"M8 14v.5"}],["path",{d:"M16 14v.5"}],["path",{d:"M11.25 16.25h1.5L12 17l-.75-.75Z"}]],R3=[["path",{d:"M16.75 12h3.632a1 1 0 0 1 .894 1.447l-2.034 4.069a1 1 0 0 1-1.708.134l-2.124-2.97"}],["path",{d:"M17.106 9.053a1 1 0 0 1 .447 1.341l-3.106 6.211a1 1 0 0 1-1.342.447L3.61 12.3a2.92 2.92 0 0 1-1.3-3.91L3.69 5.6a2.92 2.92 0 0 1 3.92-1.3z"}],["path",{d:"M2 19h3.76a2 2 0 0 0 1.8-1.1L9 15"}],["path",{d:"M2 21v-4"}],["path",{d:"M7 9h.01"}]],b=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11.207a.5.5 0 0 1 .146-.353l2-2a.5.5 0 0 1 .708 0l3.292 3.292a.5.5 0 0 0 .708 0l4.292-4.292a.5.5 0 0 1 .854.353V16a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1z"}]],R=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"7",y:"13",width:"9",height:"4",rx:"1"}],["rect",{x:"7",y:"5",width:"12",height:"4",rx:"1"}]],T3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11h8"}],["path",{d:"M7 16h3"}],["path",{d:"M7 6h12"}]],q3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 11h8"}],["path",{d:"M7 16h12"}],["path",{d:"M7 6h3"}]],U3=[["path",{d:"M11 13v4"}],["path",{d:"M15 5v4"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"7",y:"13",width:"9",height:"4",rx:"1"}],["rect",{x:"7",y:"5",width:"12",height:"4",rx:"1"}]],T=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 16h8"}],["path",{d:"M7 11h12"}],["path",{d:"M7 6h3"}]],q=[["path",{d:"M9 5v4"}],["rect",{width:"4",height:"6",x:"7",y:"9",rx:"1"}],["path",{d:"M9 15v2"}],["path",{d:"M17 3v2"}],["rect",{width:"4",height:"8",x:"15",y:"5",rx:"1"}],["path",{d:"M17 13v3"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}]],U=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"15",y:"5",width:"4",height:"12",rx:"1"}],["rect",{x:"7",y:"8",width:"4",height:"9",rx:"1"}]],O3=[["path",{d:"M13 17V9"}],["path",{d:"M18 17v-3"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 17V5"}]],O=[["path",{d:"M13 17V9"}],["path",{d:"M18 17V5"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 17v-3"}]],Z3=[["path",{d:"M11 13H7"}],["path",{d:"M19 9h-4"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["rect",{x:"15",y:"5",width:"4",height:"12",rx:"1"}],["rect",{x:"7",y:"8",width:"4",height:"9",rx:"1"}]],Z=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M18 17V9"}],["path",{d:"M13 17V5"}],["path",{d:"M8 17v-3"}]],G3=[["path",{d:"M10 6h8"}],["path",{d:"M12 16h6"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M8 11h7"}]],G=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"m19 9-5 5-4-4-3 3"}]],W3=[["path",{d:"m13.11 7.664 1.78 2.672"}],["path",{d:"m14.162 12.788-3.324 1.424"}],["path",{d:"m20 4-6.06 1.515"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["circle",{cx:"12",cy:"6",r:"2"}],["circle",{cx:"16",cy:"12",r:"2"}],["circle",{cx:"9",cy:"15",r:"2"}]],I3=[["path",{d:"M5 21V3"}],["path",{d:"M12 21V9"}],["path",{d:"M19 21v-6"}]],W=[["path",{d:"M5 21v-6"}],["path",{d:"M12 21V9"}],["path",{d:"M19 21V3"}]],I=[["path",{d:"M5 21v-6"}],["path",{d:"M12 21V3"}],["path",{d:"M19 21V9"}]],E3=[["path",{d:"M12 16v5"}],["path",{d:"M16 14v7"}],["path",{d:"M20 10v11"}],["path",{d:"m22 3-8.646 8.646a.5.5 0 0 1-.708 0L9.354 8.354a.5.5 0 0 0-.707 0L2 15"}],["path",{d:"M4 18v3"}],["path",{d:"M8 14v7"}]],E=[["path",{d:"M6 5h12"}],["path",{d:"M4 12h10"}],["path",{d:"M12 19h8"}]],X=[["path",{d:"M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z"}],["path",{d:"M21.21 15.89A10 10 0 1 1 8 2.83"}]],j=[["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}],["circle",{cx:"18.5",cy:"5.5",r:".5",fill:"currentColor"}],["circle",{cx:"11.5",cy:"11.5",r:".5",fill:"currentColor"}],["circle",{cx:"7.5",cy:"16.5",r:".5",fill:"currentColor"}],["circle",{cx:"17.5",cy:"14.5",r:".5",fill:"currentColor"}],["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}]],X3=[["path",{d:"M3 3v16a2 2 0 0 0 2 2h16"}],["path",{d:"M7 16c.5-2 1.5-7 4-7 2 0 2 3 4 3 2.5 0 4.5-5 5-7"}]],j3=[["path",{d:"M18 6 7 17l-5-5"}],["path",{d:"m22 10-7.5 7.5L13 16"}]],N3=[["path",{d:"M20 4L9 15"}],["path",{d:"M21 19L3 19"}],["path",{d:"M9 15L4 10"}]],K3=[["path",{d:"M20 6 9 17l-5-5"}]],Q3=[["path",{d:"M17 21a1 1 0 0 0 1-1v-5.35c0-.457.316-.844.727-1.041a4 4 0 0 0-2.134-7.589 5 5 0 0 0-9.186 0 4 4 0 0 0-2.134 7.588c.411.198.727.585.727 1.041V20a1 1 0 0 0 1 1Z"}],["path",{d:"M6 17h12"}]],J3=[["path",{d:"M2 17a5 5 0 0 0 10 0c0-2.76-2.5-5-5-3-2.5-2-5 .24-5 3Z"}],["path",{d:"M12 17a5 5 0 0 0 10 0c0-2.76-2.5-5-5-3-2.5-2-5 .24-5 3Z"}],["path",{d:"M7 14c3.22-2.91 4.29-8.75 5-12 1.66 2.38 4.94 9 5 12"}],["path",{d:"M22 9c-4.29 0-7.14-2.33-10-7 5.71 0 10 4.67 10 7Z"}]],Y3=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"M15 18c1.5-.615 3-2.461 3-4.923C18 8.769 14.5 4.462 12 2 9.5 4.462 6 8.77 6 13.077 6 15.539 7.5 17.385 9 18"}],["path",{d:"m16 7-2.5 2.5"}],["path",{d:"M9 2h6"}]],_3=[["path",{d:"M4 20a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"}],["path",{d:"m6.7 18-1-1C4.35 15.682 3 14.09 3 12a5 5 0 0 1 4.95-5c1.584 0 2.7.455 4.05 1.818C13.35 7.455 14.466 7 16.05 7A5 5 0 0 1 21 12c0 2.082-1.359 3.673-2.7 5l-1 1"}],["path",{d:"M10 4h4"}],["path",{d:"M12 2v6.818"}]],x3=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"M16.5 18c1-2 2.5-5 2.5-9a7 7 0 0 0-7-7H6.635a1 1 0 0 0-.768 1.64L7 5l-2.32 5.802a2 2 0 0 0 .95 2.526l2.87 1.456"}],["path",{d:"m15 5 1.425-1.425"}],["path",{d:"m17 8 1.53-1.53"}],["path",{d:"M9.713 12.185 7 18"}]],ad=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"m14.5 10 1.5 8"}],["path",{d:"M7 10h10"}],["path",{d:"m8 18 1.5-8"}],["circle",{cx:"12",cy:"6",r:"4"}]],td=[["path",{d:"M4 20a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"}],["path",{d:"m12.474 5.943 1.567 5.34a1 1 0 0 0 1.75.328l2.616-3.402"}],["path",{d:"m20 9-3 9"}],["path",{d:"m5.594 8.209 2.615 3.403a1 1 0 0 0 1.75-.329l1.567-5.34"}],["path",{d:"M7 18 4 9"}],["circle",{cx:"12",cy:"4",r:"2"}],["circle",{cx:"20",cy:"7",r:"2"}],["circle",{cx:"4",cy:"7",r:"2"}]],hd=[["path",{d:"m6 9 6 6 6-6"}]],dd=[["path",{d:"m17 18-6-6 6-6"}],["path",{d:"M7 6v12"}]],cd=[["path",{d:"M5 20a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1z"}],["path",{d:"M10 2v2"}],["path",{d:"M14 2v2"}],["path",{d:"m17 18-1-9"}],["path",{d:"M6 2v5a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2"}],["path",{d:"M6 4h12"}],["path",{d:"m7 18 1-9"}]],Md=[["path",{d:"m7 18 6-6-6-6"}],["path",{d:"M17 6v12"}]],pd=[["path",{d:"m15 18-6-6 6-6"}]],id=[["path",{d:"m9 18 6-6-6-6"}]],nd=[["path",{d:"m18 15-6-6-6 6"}]],ld=[["path",{d:"m7 20 5-5 5 5"}],["path",{d:"m7 4 5 5 5-5"}]],ed=[["path",{d:"m7 6 5 5 5-5"}],["path",{d:"m7 13 5 5 5-5"}]],rd=[["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"m17 7 5 5-5 5"}],["path",{d:"m7 7-5 5 5 5"}],["path",{d:"M8 12h.01"}]],od=[["path",{d:"m9 7-5 5 5 5"}],["path",{d:"m15 7 5 5-5 5"}]],vd=[["path",{d:"m11 17-5-5 5-5"}],["path",{d:"m18 17-5-5 5-5"}]],$d=[["path",{d:"m20 17-5-5 5-5"}],["path",{d:"m4 17 5-5-5-5"}]],md=[["path",{d:"m6 17 5-5-5-5"}],["path",{d:"m13 17 5-5-5-5"}]],yd=[["path",{d:"m7 15 5 5 5-5"}],["path",{d:"m7 9 5-5 5 5"}]],sd=[["path",{d:"m17 11-5-5-5 5"}],["path",{d:"m17 18-5-5-5 5"}]],gd=[["path",{d:"M10 9h4"}],["path",{d:"M12 7v5"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"m18 9 3.52 2.147a1 1 0 0 1 .48.854V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.999a1 1 0 0 1 .48-.854L6 9"}],["path",{d:"M6 21V7a1 1 0 0 1 .376-.782l5-3.999a1 1 0 0 1 1.249.001l5 4A1 1 0 0 1 18 7v14"}]],Cd=[["path",{d:"M12 12H3a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h13"}],["path",{d:"M18 8c0-2.5-2-2.5-2-5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 12a1 1 0 0 1 1 1v2a1 1 0 0 1-.5.866"}],["path",{d:"M22 8c0-2.5-2-2.5-2-5"}],["path",{d:"M7 12v4"}]],N=[["path",{d:"M10.88 21.94 15.46 14"}],["path",{d:"M21.17 8H12"}],["path",{d:"M3.95 6.06 8.54 14"}],["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"4"}]],ud=[["path",{d:"M17 12H3a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h14"}],["path",{d:"M18 8c0-2.5-2-2.5-2-5"}],["path",{d:"M21 16a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"}],["path",{d:"M22 8c0-2.5-2-2.5-2-5"}],["path",{d:"M7 12v4"}]],K=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]],Q=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 8v8"}],["path",{d:"m8 12 4 4 4-4"}]],J=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m12 8-4 4 4 4"}],["path",{d:"M16 12H8"}]],Y=[["path",{d:"M2 12a10 10 0 1 1 10 10"}],["path",{d:"m2 22 10-10"}],["path",{d:"M8 22H2v-6"}]],_=[["path",{d:"M12 22a10 10 0 1 1 10-10"}],["path",{d:"M22 22 12 12"}],["path",{d:"M22 16v6h-6"}]],x=[["path",{d:"M2 8V2h6"}],["path",{d:"m2 2 10 10"}],["path",{d:"M12 2A10 10 0 1 1 2 12"}]],a1=[["path",{d:"M22 12A10 10 0 1 1 12 2"}],["path",{d:"M22 2 12 12"}],["path",{d:"M16 2h6v6"}]],t1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m12 16 4-4-4-4"}],["path",{d:"M8 12h8"}]],h1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}]],d1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m9 12 2 2 4-4"}]],c1=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335"}],["path",{d:"m9 11 3 3L22 4"}]],M1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m16 10-4 4-4-4"}]],p1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m14 16-4-4 4-4"}]],i1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m10 8 4 4-4 4"}]],n1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m8 14 4-4 4 4"}]],Hd=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0"}],["path",{d:"M17.609 3.721a10 10 0 0 1 2.69 2.7"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8"}],["path",{d:"M20.279 17.609a10 10 0 0 1-2.7 2.69"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69"}],["path",{d:"M6.391 20.279a10 10 0 0 1-2.69-2.7"}]],l1=[["line",{x1:"8",x2:"16",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"16",y2:"16"}],["line",{x1:"12",x2:"12",y1:"8",y2:"8"}],["circle",{cx:"12",cy:"12",r:"10"}]],Ad=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 18V6"}]],wd=[["path",{d:"M10.1 2.18a9.93 9.93 0 0 1 3.8 0"}],["path",{d:"M17.6 3.71a9.95 9.95 0 0 1 2.69 2.7"}],["path",{d:"M21.82 10.1a9.93 9.93 0 0 1 0 3.8"}],["path",{d:"M20.29 17.6a9.95 9.95 0 0 1-2.7 2.69"}],["path",{d:"M13.9 21.82a9.94 9.94 0 0 1-3.8 0"}],["path",{d:"M6.4 20.29a9.95 9.95 0 0 1-2.69-2.7"}],["path",{d:"M2.18 13.9a9.93 9.93 0 0 1 0-3.8"}],["path",{d:"M3.71 6.4a9.95 9.95 0 0 1 2.7-2.69"}],["circle",{cx:"12",cy:"12",r:"1"}]],Vd=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"1"}]],Sd=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M17 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M7 12h.01"}]],Ld=[["path",{d:"M7 10h10"}],["path",{d:"M7 14h10"}],["circle",{cx:"12",cy:"12",r:"10"}]],fd=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],kd=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"M12 8v8"}],["path",{d:"M16 12H8"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],e1=[["path",{d:"M15.6 2.7a10 10 0 1 0 5.7 5.7"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M13.4 10.6 19 5"}]],r1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 12h8"}]],Pd=[["path",{d:"m2 2 20 20"}],["path",{d:"M8.35 2.69A10 10 0 0 1 21.3 15.65"}],["path",{d:"M19.08 19.08A10 10 0 1 1 4.92 4.92"}]],o1=[["path",{d:"M12.656 7H13a3 3 0 0 1 2.984 3.307"}],["path",{d:"M13 13H9"}],["path",{d:"M19.071 19.071A1 1 0 0 1 4.93 4.93"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.357 2.687a10 10 0 0 1 12.956 12.956"}],["path",{d:"M9 17V9"}]],v1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9 17V7h4a3 3 0 0 1 0 6H9"}]],$1=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"10",x2:"10",y1:"15",y2:"9"}],["line",{x1:"14",x2:"14",y1:"15",y2:"9"}]],m1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],Bd=[["circle",{cx:"12",cy:"19",r:"2"}],["circle",{cx:"12",cy:"5",r:"2"}],["circle",{cx:"16",cy:"12",r:"2"}],["circle",{cx:"20",cy:"19",r:"2"}],["circle",{cx:"4",cy:"19",r:"2"}],["circle",{cx:"8",cy:"12",r:"2"}]],y1=[["path",{d:"M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z"}],["circle",{cx:"12",cy:"12",r:"10"}]],zd=[["path",{d:"M10 16V9.5a1 1 0 0 1 5 0"}],["path",{d:"M8 12h4"}],["path",{d:"M8 16h7"}],["circle",{cx:"12",cy:"12",r:"10"}]],s1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],g1=[["path",{d:"M12 7v4"}],["path",{d:"M7.998 9.003a5 5 0 1 0 8-.005"}],["circle",{cx:"12",cy:"12",r:"10"}]],l=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],C1=[["path",{d:"M22 2 2 22"}],["circle",{cx:"12",cy:"12",r:"10"}]],Fd=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9"}]],Dd=[["circle",{cx:"12",cy:"12",r:"6"}]],bd=[["path",{d:"M11.051 7.616a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.867l-1.156-1.152a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["circle",{cx:"12",cy:"12",r:"10"}]],u1=[["circle",{cx:"12",cy:"12",r:"10"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1"}]],H1=[["path",{d:"M18 20a6 6 0 0 0-12 0"}],["circle",{cx:"12",cy:"10",r:"4"}],["circle",{cx:"12",cy:"12",r:"10"}]],A1=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"}]],w1=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],Rd=[["circle",{cx:"12",cy:"12",r:"10"}]],Td=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M11 9h4a2 2 0 0 0 2-2V3"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"M7 21v-4a2 2 0 0 1 2-2h4"}],["circle",{cx:"15",cy:"15",r:"2"}]],qd=[["path",{d:"M21.66 17.67a1.08 1.08 0 0 1-.04 1.6A12 12 0 0 1 4.73 2.38a1.1 1.1 0 0 1 1.61-.04z"}],["path",{d:"M19.65 15.66A8 8 0 0 1 8.35 4.34"}],["path",{d:"m14 10-5.5 5.5"}],["path",{d:"M14 17.85V10H6.15"}]],Ud=[["path",{d:"M20.2 6 3 11l-.9-2.4c-.3-1.1.3-2.2 1.3-2.5l13.5-4c1.1-.3 2.2.3 2.5 1.3Z"}],["path",{d:"m6.2 5.3 3.1 3.9"}],["path",{d:"m12.4 3.4 3.1 4"}],["path",{d:"M3 11h18v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"}]],Od=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"m9 14 2 2 4-4"}]],Zd=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v.832"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2"}],["circle",{cx:"16",cy:"16",r:"6"}],["rect",{x:"8",y:"2",width:"8",height:"4",rx:"1"}]],Gd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v4"}],["path",{d:"M21 14H11"}],["path",{d:"m15 10-4 4 4 4"}]],Wd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M12 11h4"}],["path",{d:"M12 16h4"}],["path",{d:"M8 11h.01"}],["path",{d:"M8 16h.01"}]],Id=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 14h6"}]],Ed=[["path",{d:"M11 14h10"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v1.344"}],["path",{d:"m17 18 4-4-4-4"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 1.793-1.113"}],["rect",{x:"8",y:"2",width:"8",height:"4",rx:"1"}]],V1=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1"}],["path",{d:"M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-.5"}],["path",{d:"M16 4h2a2 2 0 0 1 1.73 1"}],["path",{d:"M8 18h1"}],["path",{d:"M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],S1=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-5.5"}],["path",{d:"M4 13.5V6a2 2 0 0 1 2-2h2"}],["path",{d:"M13.378 15.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],Xd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 14h6"}],["path",{d:"M12 17v-6"}]],jd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"M9 12v-1h6v1"}],["path",{d:"M11 17h2"}],["path",{d:"M12 11v6"}]],Nd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}],["path",{d:"m15 11-6 6"}],["path",{d:"m9 11 6 6"}]],Kd=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"}]],Qd=[["path",{d:"M12 6v6l2-4"}],["circle",{cx:"12",cy:"12",r:"10"}]],Jd=[["path",{d:"M12 6v6l-4-2"}],["circle",{cx:"12",cy:"12",r:"10"}]],Yd=[["path",{d:"M12 6v6l-2-4"}],["circle",{cx:"12",cy:"12",r:"10"}]],_d=[["path",{d:"M12 6v6"}],["circle",{cx:"12",cy:"12",r:"10"}]],xd=[["path",{d:"M12 6v6l4-2"}],["circle",{cx:"12",cy:"12",r:"10"}]],a8=[["path",{d:"M12 6v6h4"}],["circle",{cx:"12",cy:"12",r:"10"}]],t8=[["path",{d:"M12 6v6l4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],h8=[["path",{d:"M12 6v6l2 4"}],["circle",{cx:"12",cy:"12",r:"10"}]],d8=[["path",{d:"M12 6v10"}],["circle",{cx:"12",cy:"12",r:"10"}]],c8=[["path",{d:"M12 6v6l-2 4"}],["circle",{cx:"12",cy:"12",r:"10"}]],M8=[["path",{d:"M12 6v6l-4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],p8=[["path",{d:"M12 6v6H8"}],["circle",{cx:"12",cy:"12",r:"10"}]],i8=[["path",{d:"M12 6v6l4 2"}],["path",{d:"M20 12v5"}],["path",{d:"M20 21h.01"}],["path",{d:"M21.25 8.2A10 10 0 1 0 16 21.16"}]],n8=[["path",{d:"M12 6v6l2 1"}],["path",{d:"M12.337 21.994a10 10 0 1 1 9.588-8.767"}],["path",{d:"m14 18 4 4 4-4"}],["path",{d:"M18 14v8"}]],l8=[["path",{d:"M12 6v6l1.56.78"}],["path",{d:"M13.227 21.925a10 10 0 1 1 8.767-9.588"}],["path",{d:"m14 18 4-4 4 4"}],["path",{d:"M18 22v-8"}]],e8=[["path",{d:"M12 6v6l4 2"}],["path",{d:"M22 12a10 10 0 1 0-11 9.95"}],["path",{d:"m22 16-5.5 5.5L14 19"}]],r8=[["path",{d:"M12 2a10 10 0 0 1 7.38 16.75"}],["path",{d:"M12 6v6l4 2"}],["path",{d:"M2.5 8.875a10 10 0 0 0-.5 3"}],["path",{d:"M2.83 16a10 10 0 0 0 2.43 3.4"}],["path",{d:"M4.636 5.235a10 10 0 0 1 .891-.857"}],["path",{d:"M8.644 21.42a10 10 0 0 0 7.631-.38"}]],o8=[["path",{d:"M12 6v6l3.644 1.822"}],["path",{d:"M16 19h6"}],["path",{d:"M19 16v6"}],["path",{d:"M21.92 13.267a10 10 0 1 0-8.653 8.653"}]],v8=[["path",{d:"M12 6v6l4 2"}],["circle",{cx:"12",cy:"12",r:"10"}]],$8=[["path",{d:"M10 9.17a3 3 0 1 0 0 5.66"}],["path",{d:"M17 9.17a3 3 0 1 0 0 5.66"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],m8=[["path",{d:"M12 12v4"}],["path",{d:"M12 20h.01"}],["path",{d:"M17 18h.5a1 1 0 0 0 0-9h-1.79A7 7 0 1 0 7 17.708"}]],y8=[["path",{d:"M21 15.251A4.5 4.5 0 0 0 17.5 8h-1.79A7 7 0 1 0 3 13.607"}],["path",{d:"M7 11v4h4"}],["path",{d:"M8 19a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5 4.82 4.82 0 0 0-3.41 1.41L7 15"}]],s8=[["path",{d:"m17 15-5.5 5.5L9 18"}],["path",{d:"M5 17.743A7 7 0 1 1 15.71 10h1.79a4.5 4.5 0 0 1 1.5 8.742"}]],g8=[["path",{d:"m10.852 19.772-.383.924"}],["path",{d:"m13.148 14.228.383-.923"}],["path",{d:"M13.148 19.772a3 3 0 1 0-2.296-5.544l-.383-.923"}],["path",{d:"m13.53 20.696-.382-.924a3 3 0 1 1-2.296-5.544"}],["path",{d:"m14.772 15.852.923-.383"}],["path",{d:"m14.772 18.148.923.383"}],["path",{d:"M4.2 15.1a7 7 0 1 1 9.93-9.858A7 7 0 0 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.2"}],["path",{d:"m9.228 15.852-.923-.383"}],["path",{d:"m9.228 18.148-.923.383"}]],L1=[["path",{d:"M12 13v8l-4-4"}],["path",{d:"m12 21 4-4"}],["path",{d:"M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"}]],C8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M8 19v1"}],["path",{d:"M8 14v1"}],["path",{d:"M16 19v1"}],["path",{d:"M16 14v1"}],["path",{d:"M12 21v1"}],["path",{d:"M12 16v1"}]],u8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 17H7"}],["path",{d:"M17 21H9"}]],H8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 14v2"}],["path",{d:"M8 14v2"}],["path",{d:"M16 20h.01"}],["path",{d:"M8 20h.01"}],["path",{d:"M12 16v2"}],["path",{d:"M12 22h.01"}]],A8=[["path",{d:"M6 16.326A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 .5 8.973"}],["path",{d:"m13 12-3 5h4l-3 5"}]],w8=[["path",{d:"M11 20v2"}],["path",{d:"M18.376 14.512a6 6 0 0 0 3.461-4.127c.148-.625-.659-.97-1.248-.714a4 4 0 0 1-5.259-5.26c.255-.589-.09-1.395-.716-1.248a6 6 0 0 0-4.594 5.36"}],["path",{d:"M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"}],["path",{d:"M7 19v2"}]],V8=[["path",{d:"M13 16a3 3 0 0 1 0 6H7a5 5 0 1 1 4.9-6z"}],["path",{d:"M18.376 14.512a6 6 0 0 0 3.461-4.127c.148-.625-.659-.97-1.248-.714a4 4 0 0 1-5.259-5.26c.255-.589-.09-1.395-.716-1.248a6 6 0 0 0-4.594 5.36"}]],S8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"m9.2 22 3-7"}],["path",{d:"m9 13-3 7"}],["path",{d:"m17 13-3 7"}]],L8=[["path",{d:"m2 2 20 20"}],["path",{d:"M5.782 5.782A7 7 0 0 0 9 19h8.5a4.5 4.5 0 0 0 1.307-.193"}],["path",{d:"M21.532 16.5A4.5 4.5 0 0 0 17.5 10h-1.79A7.008 7.008 0 0 0 10 5.07"}]],f8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M16 14v6"}],["path",{d:"M8 14v6"}],["path",{d:"M12 16v6"}]],k8=[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"M8 15h.01"}],["path",{d:"M8 19h.01"}],["path",{d:"M12 17h.01"}],["path",{d:"M12 21h.01"}],["path",{d:"M16 15h.01"}],["path",{d:"M16 19h.01"}]],P8=[["path",{d:"M12 2v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"M20 12h2"}],["path",{d:"m19.07 4.93-1.41 1.41"}],["path",{d:"M15.947 12.65a4 4 0 0 0-5.925-4.128"}],["path",{d:"M3 20a5 5 0 1 1 8.9-4H13a3 3 0 0 1 2 5.24"}],["path",{d:"M11 20v2"}],["path",{d:"M7 19v2"}]],B8=[["path",{d:"M12 2v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"M20 12h2"}],["path",{d:"m19.07 4.93-1.41 1.41"}],["path",{d:"M15.947 12.65a4 4 0 0 0-5.925-4.128"}],["path",{d:"M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"}]],z8=[["path",{d:"m17 18-1.535 1.605a5 5 0 0 1-8-1.5"}],["path",{d:"M17 22v-4h-4"}],["path",{d:"M20.996 15.251A4.5 4.5 0 0 0 17.495 8h-1.79a7 7 0 1 0-12.709 5.607"}],["path",{d:"M7 10v4h4"}],["path",{d:"m7 14 1.535-1.605a5 5 0 0 1 8 1.5"}]],f1=[["path",{d:"M12 13v8"}],["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"}],["path",{d:"m8 17 4-4 4 4"}]],F8=[["path",{d:"M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"}]],D8=[["path",{d:"M17.5 21H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"}],["path",{d:"M22 10a3 3 0 0 0-3-3h-2.207a5.502 5.502 0 0 0-10.702.5"}]],b8=[["path",{d:"M16.17 7.83 2 22"}],["path",{d:"M4.02 12a2.827 2.827 0 1 1 3.81-4.17A2.827 2.827 0 1 1 12 4.02a2.827 2.827 0 1 1 4.17 3.81A2.827 2.827 0 1 1 19.98 12a2.827 2.827 0 1 1-3.81 4.17A2.827 2.827 0 1 1 12 19.98a2.827 2.827 0 1 1-4.17-3.81A1 1 0 1 1 4 12"}],["path",{d:"m7.83 7.83 8.34 8.34"}]],R8=[["path",{d:"M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z"}],["path",{d:"M12 17.66L12 22"}]],k1=[["path",{d:"m18 16 4-4-4-4"}],["path",{d:"m6 8-4 4 4 4"}],["path",{d:"m14.5 4-5 16"}]],T8=[["path",{d:"m16 18 6-6-6-6"}],["path",{d:"m8 6-6 6 6 6"}]],q8=[["polygon",{points:"12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"}],["line",{x1:"12",x2:"12",y1:"22",y2:"15.5"}],["polyline",{points:"22 8.5 12 15.5 2 8.5"}],["polyline",{points:"2 15.5 12 8.5 22 15.5"}],["line",{x1:"12",x2:"12",y1:"2",y2:"8.5"}]],U8=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}],["polyline",{points:"7.5 4.21 12 6.81 16.5 4.21"}],["polyline",{points:"7.5 19.79 7.5 14.6 3 12"}],["polyline",{points:"21 12 16.5 14.6 16.5 19.79"}],["polyline",{points:"3.27 6.96 12 12.01 20.73 6.96"}],["line",{x1:"12",x2:"12",y1:"22.08",y2:"12"}]],O8=[["path",{d:"M10 2v2"}],["path",{d:"M14 2v2"}],["path",{d:"M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1"}],["path",{d:"M6 2v2"}]],Z8=[["circle",{cx:"8",cy:"8",r:"6"}],["path",{d:"M18.09 10.37A6 6 0 1 1 10.34 18"}],["path",{d:"M7 6h1v4"}],["path",{d:"m16.71 13.88.7.71-2.82 2.82"}]],G8=[["path",{d:"M11 10.27 7 3.34"}],["path",{d:"m11 13.73-4 6.93"}],["path",{d:"M12 22v-2"}],["path",{d:"M12 2v2"}],["path",{d:"M14 12h8"}],["path",{d:"m17 20.66-1-1.73"}],["path",{d:"m17 3.34-1 1.73"}],["path",{d:"M2 12h2"}],["path",{d:"m20.66 17-1.73-1"}],["path",{d:"m20.66 7-1.73 1"}],["path",{d:"m3.34 17 1.73-1"}],["path",{d:"m3.34 7 1.73 1"}],["circle",{cx:"12",cy:"12",r:"2"}],["circle",{cx:"12",cy:"12",r:"8"}]],P1=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 3v18"}]],e=[["path",{d:"M10.5 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.5"}],["path",{d:"m14.3 19.6 1-.4"}],["path",{d:"M15 3v7.5"}],["path",{d:"m15.2 16.9-.9-.3"}],["path",{d:"m16.6 21.7.3-.9"}],["path",{d:"m16.8 15.3-.4-1"}],["path",{d:"m19.1 15.2.3-.9"}],["path",{d:"m19.6 21.7-.4-1"}],["path",{d:"m20.7 16.8 1-.4"}],["path",{d:"m21.7 19.4-.9-.3"}],["path",{d:"M9 3v18"}],["circle",{cx:"18",cy:"18",r:"3"}]],B1=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"M15 3v18"}]],W8=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7.5 3v18"}],["path",{d:"M12 3v18"}],["path",{d:"M16.5 3v18"}]],I8=[["path",{d:"M14 3a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M19 3a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"m7 15 3 3"}],["path",{d:"m7 21 3-3H5a2 2 0 0 1-2-2v-2"}],["rect",{x:"14",y:"14",width:"7",height:"7",rx:"1"}],["rect",{x:"3",y:"3",width:"7",height:"7",rx:"1"}]],E8=[["path",{d:"M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"}]],X8=[["path",{d:"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"}],["circle",{cx:"12",cy:"12",r:"10"}]],j8=[["path",{d:"M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"}],["path",{d:"M2.297 11.293a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0z"}],["path",{d:"M8.916 17.912a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0z"}],["path",{d:"M8.916 4.674a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z"}]],N8=[["rect",{width:"14",height:"8",x:"5",y:"2",rx:"2"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h2"}],["path",{d:"M12 18h6"}]],K8=[["path",{d:"M3 20a1 1 0 0 1-1-1v-1a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1Z"}],["path",{d:"M20 16a8 8 0 1 0-16 0"}],["path",{d:"M12 4v4"}],["path",{d:"M10 4h4"}]],Q8=[["path",{d:"m20.9 18.55-8-15.98a1 1 0 0 0-1.8 0l-8 15.98"}],["ellipse",{cx:"12",cy:"19",rx:"9",ry:"3"}]],J8=[["rect",{x:"2",y:"6",width:"20",height:"8",rx:"1"}],["path",{d:"M17 14v7"}],["path",{d:"M7 14v7"}],["path",{d:"M17 3v3"}],["path",{d:"M7 3v3"}],["path",{d:"M10 14 2.3 6.3"}],["path",{d:"m14 6 7.7 7.7"}],["path",{d:"m8 6 8 8"}]],z1=[["path",{d:"M16 2v2"}],["path",{d:"M17.915 22a6 6 0 0 0-12 0"}],["path",{d:"M8 2v2"}],["circle",{cx:"12",cy:"12",r:"4"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],Y8=[["path",{d:"M16 2v2"}],["path",{d:"M7 22v-2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2"}],["path",{d:"M8 2v2"}],["circle",{cx:"12",cy:"11",r:"3"}],["rect",{x:"3",y:"4",width:"18",height:"18",rx:"2"}]],_8=[["path",{d:"M22 7.7c0-.6-.4-1.2-.8-1.5l-6.3-3.9a1.72 1.72 0 0 0-1.7 0l-10.3 6c-.5.2-.9.8-.9 1.4v6.6c0 .5.4 1.2.8 1.5l6.3 3.9a1.72 1.72 0 0 0 1.7 0l10.3-6c.5-.3.9-1 .9-1.5Z"}],["path",{d:"M10 21.9V14L2.1 9.1"}],["path",{d:"m10 14 11.9-6.9"}],["path",{d:"M14 19.8v-8.1"}],["path",{d:"M18 17.5V9.4"}]],x8=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 18a6 6 0 0 0 0-12v12z"}]],a6=[["path",{d:"M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"}],["path",{d:"M8.5 8.5v.01"}],["path",{d:"M16 15.5v.01"}],["path",{d:"M12 12v.01"}],["path",{d:"M11 17v.01"}],["path",{d:"M7 14v.01"}]],t6=[["path",{d:"M2 12h20"}],["path",{d:"M20 12v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8"}],["path",{d:"m4 8 16-4"}],["path",{d:"m8.86 6.78-.45-1.81a2 2 0 0 1 1.45-2.43l1.94-.48a2 2 0 0 1 2.43 1.46l.45 1.8"}]],h6=[["path",{d:"m12 15 2 2 4-4"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],d6=[["line",{x1:"12",x2:"18",y1:"15",y2:"15"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],c6=[["line",{x1:"15",x2:"15",y1:"12",y2:"18"}],["line",{x1:"12",x2:"18",y1:"15",y2:"15"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],M6=[["line",{x1:"12",x2:"18",y1:"18",y2:"12"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],p6=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],i6=[["line",{x1:"12",x2:"18",y1:"12",y2:"18"}],["line",{x1:"12",x2:"18",y1:"18",y2:"12"}],["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]],n6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M9.17 14.83a4 4 0 1 0 0-5.66"}]],l6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M14.83 14.83a4 4 0 1 1 0-5.66"}]],e6=[["path",{d:"m15 10 5 5-5 5"}],["path",{d:"M4 4v7a4 4 0 0 0 4 4h12"}]],r6=[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4"}],["path",{d:"m9 10-5 5 5 5"}]],o6=[["path",{d:"m14 15-5 5-5-5"}],["path",{d:"M20 4h-7a4 4 0 0 0-4 4v12"}]],v6=[["path",{d:"M14 9 9 4 4 9"}],["path",{d:"M20 20h-7a4 4 0 0 1-4-4V4"}]],$6=[["path",{d:"m10 15 5 5 5-5"}],["path",{d:"M4 4h7a4 4 0 0 1 4 4v12"}]],m6=[["path",{d:"m10 9 5-5 5 5"}],["path",{d:"M4 20h7a4 4 0 0 0 4-4V4"}]],y6=[["path",{d:"M20 20v-7a4 4 0 0 0-4-4H4"}],["path",{d:"M9 14 4 9l5-5"}]],s6=[["path",{d:"m15 14 5-5-5-5"}],["path",{d:"M4 20v-7a4 4 0 0 1 4-4h12"}]],g6=[["path",{d:"M12 20v2"}],["path",{d:"M12 2v2"}],["path",{d:"M17 20v2"}],["path",{d:"M17 2v2"}],["path",{d:"M2 12h2"}],["path",{d:"M2 17h2"}],["path",{d:"M2 7h2"}],["path",{d:"M20 12h2"}],["path",{d:"M20 17h2"}],["path",{d:"M20 7h2"}],["path",{d:"M7 20v2"}],["path",{d:"M7 2v2"}],["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1"}]],C6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M10 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"}],["path",{d:"M17 9.3a2.8 2.8 0 0 0-3.5 1 3.1 3.1 0 0 0 0 3.4 2.7 2.7 0 0 0 3.5 1"}]],u6=[["rect",{width:"20",height:"14",x:"2",y:"5",rx:"2"}],["line",{x1:"2",x2:"22",y1:"10",y2:"10"}]],H6=[["path",{d:"M10.2 18H4.774a1.5 1.5 0 0 1-1.352-.97 11 11 0 0 1 .132-6.487"}],["path",{d:"M18 10.2V4.774a1.5 1.5 0 0 0-.97-1.352 11 11 0 0 0-6.486.132"}],["path",{d:"M18 5a4 3 0 0 1 4 3 2 2 0 0 1-2 2 10 10 0 0 0-5.139 1.42"}],["path",{d:"M5 18a3 4 0 0 0 3 4 2 2 0 0 0 2-2 10 10 0 0 1 1.42-5.14"}],["path",{d:"M8.709 2.554a10 10 0 0 0-6.155 6.155 1.5 1.5 0 0 0 .676 1.626l9.807 5.42a2 2 0 0 0 2.718-2.718l-5.42-9.807a1.5 1.5 0 0 0-1.626-.676"}]],A6=[["path",{d:"M6 2v14a2 2 0 0 0 2 2h14"}],["path",{d:"M18 22V8a2 2 0 0 0-2-2H2"}]],w6=[["path",{d:"M4 9a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h4a1 1 0 0 1 1 1v4a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-4a1 1 0 0 1 1-1h4a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-4a1 1 0 0 1-1-1V4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4a1 1 0 0 1-1 1z"}]],V6=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"22",x2:"18",y1:"12",y2:"12"}],["line",{x1:"6",x2:"2",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"6",y2:"2"}],["line",{x1:"12",x2:"12",y1:"22",y2:"18"}]],S6=[["path",{d:"M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z"}],["path",{d:"M5 21h14"}]],L6=[["path",{d:"m21.12 6.4-6.05-4.06a2 2 0 0 0-2.17-.05L2.95 8.41a2 2 0 0 0-.95 1.7v5.82a2 2 0 0 0 .88 1.66l6.05 4.07a2 2 0 0 0 2.17.05l9.95-6.12a2 2 0 0 0 .95-1.7V8.06a2 2 0 0 0-.88-1.66Z"}],["path",{d:"M10 22v-8L2.25 9.15"}],["path",{d:"m10 14 11.77-6.87"}]],f6=[["path",{d:"m6 8 1.75 12.28a2 2 0 0 0 2 1.72h4.54a2 2 0 0 0 2-1.72L18 8"}],["path",{d:"M5 8h14"}],["path",{d:"M7 15a6.47 6.47 0 0 1 5 0 6.47 6.47 0 0 0 5 0"}],["path",{d:"m12 8 1-6h2"}]],k6=[["circle",{cx:"12",cy:"12",r:"8"}],["line",{x1:"3",x2:"6",y1:"3",y2:"6"}],["line",{x1:"21",x2:"18",y1:"3",y2:"6"}],["line",{x1:"3",x2:"6",y1:"21",y2:"18"}],["line",{x1:"21",x2:"18",y1:"21",y2:"18"}]],P6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5v14a9 3 0 0 0 18 0V5"}]],B6=[["path",{d:"M11 11.31c1.17.56 1.54 1.69 3.5 1.69 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M11.75 18c.35.5 1.45 1 2.75 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["path",{d:"M2 6h4"}],["path",{d:"M7 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1L10 4a1 1 0 0 0-1-1z"}]],z6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 12a9 3 0 0 0 5 2.69"}],["path",{d:"M21 9.3V5"}],["path",{d:"M3 5v14a9 3 0 0 0 6.47 2.88"}],["path",{d:"M12 12v4h4"}],["path",{d:"M13 20a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5c-1.33 0-2.54.54-3.41 1.41L12 16"}]],F6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5V19A9 3 0 0 0 15 21.84"}],["path",{d:"M21 5V8"}],["path",{d:"M21 12L18 17H22L19 22"}],["path",{d:"M3 12A9 3 0 0 0 14.59 14.87"}]],D6=[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5"}],["path",{d:"M3 12A9 3 0 0 0 21 12"}]],b6=[["path",{d:"m13 21-3-3 3-3"}],["path",{d:"M20 18H10"}],["path",{d:"M3 11h.01"}],["rect",{x:"6",y:"3",width:"5",height:"8",rx:"2.5"}]],R6=[["path",{d:"M10 18h10"}],["path",{d:"m17 21 3-3-3-3"}],["path",{d:"M3 11h.01"}],["rect",{x:"15",y:"3",width:"5",height:"8",rx:"2.5"}],["rect",{x:"6",y:"3",width:"5",height:"8",rx:"2.5"}]],T6=[["path",{d:"M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z"}],["path",{d:"m12 9 6 6"}],["path",{d:"m18 9-6 6"}]],q6=[["path",{d:"M10.162 3.167A10 10 0 0 0 2 13a2 2 0 0 0 4 0v-1a2 2 0 0 1 4 0v4a2 2 0 0 0 4 0v-4a2 2 0 0 1 4 0v1a2 2 0 0 0 4-.006 10 10 0 0 0-8.161-9.826"}],["path",{d:"M20.804 14.869a9 9 0 0 1-17.608 0"}],["circle",{cx:"12",cy:"4",r:"2"}]],U6=[["circle",{cx:"19",cy:"19",r:"2"}],["circle",{cx:"5",cy:"5",r:"2"}],["path",{d:"M6.48 3.66a10 10 0 0 1 13.86 13.86"}],["path",{d:"m6.41 6.41 11.18 11.18"}],["path",{d:"M3.66 6.48a10 10 0 0 0 13.86 13.86"}]],O6=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0z"}],["path",{d:"M8 12h8"}]],F1=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0Z"}],["path",{d:"M9.2 9.2h.01"}],["path",{d:"m14.5 9.5-5 5"}],["path",{d:"M14.7 14.8h.01"}]],Z6=[["path",{d:"M12 8v8"}],["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41L13.7 2.71a2.41 2.41 0 0 0-3.41 0z"}],["path",{d:"M8 12h8"}]],G6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M12 12h.01"}]],W6=[["path",{d:"M2.7 10.3a2.41 2.41 0 0 0 0 3.41l7.59 7.59a2.41 2.41 0 0 0 3.41 0l7.59-7.59a2.41 2.41 0 0 0 0-3.41l-7.59-7.59a2.41 2.41 0 0 0-3.41 0Z"}]],I6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M15 9h.01"}],["path",{d:"M9 15h.01"}]],E6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M8 16h.01"}]],X6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 16h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M12 12h.01"}]],j6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 12h.01"}],["path",{d:"M8 16h.01"}]],N6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M16 8h.01"}],["path",{d:"M8 8h.01"}],["path",{d:"M8 16h.01"}],["path",{d:"M16 16h.01"}]],K6=[["rect",{width:"12",height:"12",x:"2",y:"10",rx:"2",ry:"2"}],["path",{d:"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 14h.01"}],["path",{d:"M15 6h.01"}],["path",{d:"M18 9h.01"}]],Q6=[["path",{d:"M12 3v14"}],["path",{d:"M5 10h14"}],["path",{d:"M5 21h14"}]],J6=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 12h.01"}]],Y6=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M6 12c0-1.7.7-3.2 1.8-4.2"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M18 12c0 1.7-.7 3.2-1.8 4.2"}]],_6=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"12",r:"5"}],["path",{d:"M12 12h.01"}]],x6=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"2"}]],ac=[["circle",{cx:"12",cy:"6",r:"1"}],["line",{x1:"5",x2:"19",y1:"12",y2:"12"}],["circle",{cx:"12",cy:"18",r:"1"}]],tc=[["path",{d:"M15 2c-1.35 1.5-2.092 3-2.5 4.5L14 8"}],["path",{d:"m17 6-2.891-2.891"}],["path",{d:"M2 15c3.333-3 6.667-3 10-3"}],["path",{d:"m2 2 20 20"}],["path",{d:"m20 9 .891.891"}],["path",{d:"M22 9c-1.5 1.35-3 2.092-4.5 2.5l-1-1"}],["path",{d:"M3.109 14.109 4 15"}],["path",{d:"m6.5 12.5 1 1"}],["path",{d:"m7 18 2.891 2.891"}],["path",{d:"M9 22c1.35-1.5 2.092-3 2.5-4.5L10 16"}]],hc=[["path",{d:"M2 8h20"}],["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 16h12"}]],dc=[["path",{d:"m10 16 1.5 1.5"}],["path",{d:"m14 8-1.5-1.5"}],["path",{d:"M15 2c-1.798 1.998-2.518 3.995-2.807 5.993"}],["path",{d:"m16.5 10.5 1 1"}],["path",{d:"m17 6-2.891-2.891"}],["path",{d:"M2 15c6.667-6 13.333 0 20-6"}],["path",{d:"m20 9 .891.891"}],["path",{d:"M3.109 14.109 4 15"}],["path",{d:"m6.5 12.5 1 1"}],["path",{d:"m7 18 2.891 2.891"}],["path",{d:"M9 22c1.798-1.998 2.518-3.995 2.807-5.993"}]],cc=[["path",{d:"M11.25 16.25h1.5L12 17z"}],["path",{d:"M16 14v.5"}],["path",{d:"M4.42 11.247A13.152 13.152 0 0 0 4 14.556C4 18.728 7.582 21 12 21s8-2.272 8-6.444a11.702 11.702 0 0 0-.493-3.309"}],["path",{d:"M8 14v.5"}],["path",{d:"M8.5 8.5c-.384 1.05-1.083 2.028-2.344 2.5-1.931.722-3.576-.297-3.656-1-.113-.994 1.177-6.53 4-7 1.923-.321 3.651.845 3.651 2.235A7.497 7.497 0 0 1 14 5.277c0-1.39 1.844-2.598 3.767-2.277 2.823.47 4.113 6.006 4 7-.08.703-1.725 1.722-3.656 1-1.261-.472-1.855-1.45-2.239-2.5"}]],Mc=[["line",{x1:"12",x2:"12",y1:"2",y2:"22"}],["path",{d:"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"}]],pc=[["path",{d:"M20.5 10a2.5 2.5 0 0 1-2.4-3H18a2.95 2.95 0 0 1-2.6-4.4 10 10 0 1 0 6.3 7.1c-.3.2-.8.3-1.2.3"}],["circle",{cx:"12",cy:"12",r:"3"}]],ic=[["path",{d:"M10 12h.01"}],["path",{d:"M18 9V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"}],["path",{d:"M2 20h8"}],["path",{d:"M20 17v-2a2 2 0 1 0-4 0v2"}],["rect",{x:"14",y:"17",width:"8",height:"5",rx:"1"}]],nc=[["path",{d:"M10 12h.01"}],["path",{d:"M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"}],["path",{d:"M2 20h20"}]],lc=[["circle",{cx:"12.1",cy:"12.1",r:"1"}]],ec=[["path",{d:"M11 20H2"}],["path",{d:"M11 4.562v16.157a1 1 0 0 0 1.242.97L19 20V5.562a2 2 0 0 0-1.515-1.94l-4-1A2 2 0 0 0 11 4.561z"}],["path",{d:"M11 4H8a2 2 0 0 0-2 2v14"}],["path",{d:"M14 12h.01"}],["path",{d:"M22 20h-3"}]],rc=[["path",{d:"M12 15V3"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}],["path",{d:"m7 10 5 5 5-5"}]],oc=[["path",{d:"m12.99 6.74 1.93 3.44"}],["path",{d:"M19.136 12a10 10 0 0 1-14.271 0"}],["path",{d:"m21 21-2.16-3.84"}],["path",{d:"m3 21 8.02-14.26"}],["circle",{cx:"12",cy:"5",r:"2"}]],vc=[["path",{d:"M10 11h.01"}],["path",{d:"M14 6h.01"}],["path",{d:"M18 6h.01"}],["path",{d:"M6.5 13.1h.01"}],["path",{d:"M22 5c0 9-4 12-6 12s-6-3-6-12c0-2 2-3 6-3s6 1 6 3"}],["path",{d:"M17.4 9.9c-.8.8-2 .8-2.8 0"}],["path",{d:"M10.1 7.1C9 7.2 7.7 7.7 6 8.6c-3.5 2-4.7 3.9-3.7 5.6 4.5 7.8 9.5 8.4 11.2 7.4.9-.5 1.9-2.1 1.9-4.7"}],["path",{d:"M9.1 16.5c.3-1.1 1.4-1.7 2.4-1.4"}]],$c=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M19.13 5.09C15.22 9.14 10 10.44 2.25 10.94"}],["path",{d:"M21.75 12.84c-6.62-1.41-12.14 1-16.38 6.32"}],["path",{d:"M8.56 2.75c4.37 6 6 9.42 8 17.72"}]],mc=[["path",{d:"M10 18a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H5a3 3 0 0 1-3-3 1 1 0 0 1 1-1z"}],["path",{d:"M13 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1l-.81 3.242a1 1 0 0 1-.97.758H8"}],["path",{d:"M14 4h3a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-3"}],["path",{d:"M18 6h4"}],["path",{d:"m5 10-2 8"}],["path",{d:"m7 18 2-8"}]],yc=[["path",{d:"M10 10 7 7"}],["path",{d:"m10 14-3 3"}],["path",{d:"m14 10 3-3"}],["path",{d:"m14 14 3 3"}],["path",{d:"M14.205 4.139a4 4 0 1 1 5.439 5.863"}],["path",{d:"M19.637 14a4 4 0 1 1-5.432 5.868"}],["path",{d:"M4.367 10a4 4 0 1 1 5.438-5.862"}],["path",{d:"M9.795 19.862a4 4 0 1 1-5.429-5.873"}],["rect",{x:"10",y:"8",width:"4",height:"8",rx:"1"}]],sc=[["path",{d:"M18.715 13.186C18.29 11.858 17.384 10.607 16 9.5c-2-1.6-3.5-4-4-6.5a10.7 10.7 0 0 1-.884 2.586"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.795 8.797A11 11 0 0 1 8 9.5C6 11.1 5 13 5 15a7 7 0 0 0 13.222 3.208"}]],gc=[["path",{d:"M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z"}]],Cc=[["path",{d:"M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"}],["path",{d:"M12.56 6.6A10.97 10.97 0 0 0 14 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 0 1-11.91 4.97"}]],uc=[["path",{d:"m2 2 8 8"}],["path",{d:"m22 2-8 8"}],["ellipse",{cx:"12",cy:"9",rx:"10",ry:"5"}],["path",{d:"M7 13.4v7.9"}],["path",{d:"M12 14v8"}],["path",{d:"M17 13.4v7.9"}],["path",{d:"M2 9v8a10 5 0 0 0 20 0V9"}]],Hc=[["path",{d:"M15.4 15.63a7.875 6 135 1 1 6.23-6.23 4.5 3.43 135 0 0-6.23 6.23"}],["path",{d:"m8.29 12.71-2.6 2.6a2.5 2.5 0 1 0-1.65 4.65A2.5 2.5 0 1 0 8.7 18.3l2.59-2.59"}]],Ac=[["path",{d:"M17.596 12.768a2 2 0 1 0 2.829-2.829l-1.768-1.767a2 2 0 0 0 2.828-2.829l-2.828-2.828a2 2 0 0 0-2.829 2.828l-1.767-1.768a2 2 0 1 0-2.829 2.829z"}],["path",{d:"m2.5 21.5 1.4-1.4"}],["path",{d:"m20.1 3.9 1.4-1.4"}],["path",{d:"M5.343 21.485a2 2 0 1 0 2.829-2.828l1.767 1.768a2 2 0 1 0 2.829-2.829l-6.364-6.364a2 2 0 1 0-2.829 2.829l1.768 1.767a2 2 0 0 0-2.828 2.829z"}],["path",{d:"m9.6 14.4 4.8-4.8"}]],wc=[["path",{d:"M6 18.5a3.5 3.5 0 1 0 7 0c0-1.57.92-2.52 2.04-3.46"}],["path",{d:"M6 8.5c0-.75.13-1.47.36-2.14"}],["path",{d:"M8.8 3.15A6.5 6.5 0 0 1 19 8.5c0 1.63-.44 2.81-1.09 3.76"}],["path",{d:"M12.5 6A2.5 2.5 0 0 1 15 8.5M10 13a2 2 0 0 0 1.82-1.18"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Vc=[["path",{d:"M6 8.5a6.5 6.5 0 1 1 13 0c0 6-6 6-6 10a3.5 3.5 0 1 1-7 0"}],["path",{d:"M15 8.5a2.5 2.5 0 0 0-5 0v1a2 2 0 1 1 0 4"}]],Sc=[["path",{d:"M7 3.34V5a3 3 0 0 0 3 3"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2 2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"}],["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54"}],["path",{d:"M12 2a10 10 0 1 0 9.54 13"}],["path",{d:"M20 6V4a2 2 0 1 0-4 0v2"}],["rect",{width:"8",height:"5",x:"14",y:"6",rx:"1"}]],D1=[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"}],["circle",{cx:"12",cy:"12",r:"10"}]],Lc=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 2a7 7 0 1 0 10 10"}]],fc=[["circle",{cx:"11.5",cy:"12.5",r:"3.5"}],["path",{d:"M3 8c0-3.5 2.5-6 6.5-6 5 0 4.83 3 7.5 5s5 2 5 6c0 4.5-2.5 6.5-7 6.5-2.5 0-2.5 2.5-6 2.5s-7-2-7-5.5c0-3 1.5-3 1.5-5C3.5 10 3 9 3 8Z"}]],kc=[["path",{d:"m2 2 20 20"}],["path",{d:"M20 14.347V14c0-6-4-12-8-12-1.078 0-2.157.436-3.157 1.19"}],["path",{d:"M6.206 6.21C4.871 8.4 4 11.2 4 14a8 8 0 0 0 14.568 4.568"}]],Pc=[["path",{d:"M12 2C8 2 4 8 4 14a8 8 0 0 0 16 0c0-6-4-12-8-12"}]],b1=[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"12",cy:"5",r:"1"}],["circle",{cx:"12",cy:"19",r:"1"}]],R1=[["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}]],Bc=[["path",{d:"M5 15a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0"}],["path",{d:"M5 9a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0"}]],zc=[["line",{x1:"5",x2:"19",y1:"9",y2:"9"}],["line",{x1:"5",x2:"19",y1:"15",y2:"15"}],["line",{x1:"19",x2:"5",y1:"5",y2:"19"}]],Fc=[["path",{d:"M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21"}],["path",{d:"m5.082 11.09 8.828 8.828"}]],Dc=[["line",{x1:"5",x2:"19",y1:"9",y2:"9"}],["line",{x1:"5",x2:"19",y1:"15",y2:"15"}]],bc=[["path",{d:"m15 20 3-3h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2l3 3z"}],["path",{d:"M6 8v1"}],["path",{d:"M10 8v1"}],["path",{d:"M14 8v1"}],["path",{d:"M18 8v1"}]],Rc=[["path",{d:"M4 10h12"}],["path",{d:"M4 14h9"}],["path",{d:"M19 6a7.7 7.7 0 0 0-5.2-2A7.9 7.9 0 0 0 6 12c0 4.4 3.5 8 7.8 8 2 0 3.8-.8 5.2-2"}]],Tc=[["path",{d:"M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 4 0v-6.998a2 2 0 0 0-.59-1.42L18 5"}],["path",{d:"M14 21V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v16"}],["path",{d:"M2 21h13"}],["path",{d:"M3 7h11"}],["path",{d:"m9 11-2 3h3l-2 3"}]],qc=[["path",{d:"M15 3h6v6"}],["path",{d:"M10 14 21 3"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"}]],Uc=[["path",{d:"m15 15 6 6"}],["path",{d:"m15 9 6-6"}],["path",{d:"M21 16v5h-5"}],["path",{d:"M21 8V3h-5"}],["path",{d:"M3 16v5h5"}],["path",{d:"m3 21 6-6"}],["path",{d:"M3 8V3h5"}],["path",{d:"M9 9 3 3"}]],Oc=[["path",{d:"m15 18-.722-3.25"}],["path",{d:"M2 8a10.645 10.645 0 0 0 20 0"}],["path",{d:"m20 15-1.726-2.05"}],["path",{d:"m4 15 1.726-2.05"}],["path",{d:"m9 18 .722-3.25"}]],Zc=[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"}],["path",{d:"m2 2 20 20"}]],Gc=[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"}],["circle",{cx:"12",cy:"12",r:"3"}]],Wc=[["path",{d:"M12 16h.01"}],["path",{d:"M16 16h.01"}],["path",{d:"M3 19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.5a.5.5 0 0 0-.769-.422l-4.462 2.844A.5.5 0 0 1 15 10.5v-2a.5.5 0 0 0-.769-.422L9.77 10.922A.5.5 0 0 1 9 10.5V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2z"}],["path",{d:"M8 16h.01"}]],Ic=[["path",{d:"M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"}]],Ec=[["path",{d:"M10.827 16.379a6.082 6.082 0 0 1-8.618-7.002l5.412 1.45a6.082 6.082 0 0 1 7.002-8.618l-1.45 5.412a6.082 6.082 0 0 1 8.618 7.002l-5.412-1.45a6.082 6.082 0 0 1-7.002 8.618l1.45-5.412Z"}],["path",{d:"M12 12v.01"}]],Xc=[["path",{d:"M12 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 12 18z"}],["path",{d:"M2 6a2 2 0 0 1 3.414-1.414l6 6a2 2 0 0 1 0 2.828l-6 6A2 2 0 0 1 2 18z"}]],jc=[["path",{d:"M12.67 19a2 2 0 0 0 1.416-.588l6.154-6.172a6 6 0 0 0-8.49-8.49L5.586 9.914A2 2 0 0 0 5 11.328V18a1 1 0 0 0 1 1z"}],["path",{d:"M16 8 2 22"}],["path",{d:"M17.5 15H9"}]],Nc=[["path",{d:"M4 3 2 5v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}],["path",{d:"M6 8h4"}],["path",{d:"M6 18h4"}],["path",{d:"m12 3-2 2v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}],["path",{d:"M14 8h4"}],["path",{d:"M14 18h4"}],["path",{d:"m20 3-2 2v15c0 .6.4 1 1 1h2c.6 0 1-.4 1-1V5Z"}]],Kc=[["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"M12 2v4"}],["path",{d:"m6.8 15-3.5 2"}],["path",{d:"m20.7 7-3.5 2"}],["path",{d:"M6.8 9 3.3 7"}],["path",{d:"m20.7 17-3.5-2"}],["path",{d:"m9 22 3-8 3 8"}],["path",{d:"M8 22h8"}],["path",{d:"M18 18.7a9 9 0 1 0-12 0"}]],Qc=[["path",{d:"M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"}],["path",{d:"M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"}],["path",{d:"M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"}],["path",{d:"M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"}],["path",{d:"M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"}]],Jc=[["path",{d:"M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 12v-1"}],["path",{d:"M8 18v-2"}],["path",{d:"M8 7V6"}],["circle",{cx:"8",cy:"20",r:"2"}]],T1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m8 18 4-4"}],["path",{d:"M8 10v8h8"}]],q1=[["path",{d:"M13 22h5a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.3"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m7.69 16.479 1.29 4.88a.5.5 0 0 1-.698.591l-1.843-.849a1 1 0 0 0-.879.001l-1.846.85a.5.5 0 0 1-.692-.593l1.29-4.88"}],["circle",{cx:"6",cy:"14",r:"3"}]],U1=[["path",{d:"M14 22h4a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M5 14a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1 1 1 0 0 1 1 1v2a1 1 0 0 0 1 1"}],["path",{d:"M9 22a1 1 0 0 0 1-1v-2a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-2a1 1 0 0 0-1-1"}]],Yc=[["path",{d:"M14.5 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.8"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M11.7 14.2 7 17l-4.7-2.8"}],["path",{d:"M3 13.1a2 2 0 0 0-.999 1.76v3.24a2 2 0 0 0 .969 1.78L6 21.7a2 2 0 0 0 2.03.01L11 19.9a2 2 0 0 0 1-1.76V14.9a2 2 0 0 0-.97-1.78L8 11.3a2 2 0 0 0-2.03-.01z"}],["path",{d:"M7 17v5"}]],O1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"}],["path",{d:"M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"}]],Z1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 18v-2"}],["path",{d:"M12 18v-4"}],["path",{d:"M16 18v-6"}]],G1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 18v-1"}],["path",{d:"M12 18v-6"}],["path",{d:"M16 18v-3"}]],W1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m16 13-3.5 3.5-2-2L8 17"}]],I1=[["path",{d:"M15.941 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.704l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.512"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M4.017 11.512a6 6 0 1 0 8.466 8.475"}],["path",{d:"M9 16a1 1 0 0 1-1-1v-4c0-.552.45-1.008.995-.917a6 6 0 0 1 4.922 4.922c.091.544-.365.995-.917.995z"}]],E1=[["path",{d:"M10.5 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m14 20 2 2 4-4"}]],_c=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m9 15 2 2 4-4"}]],xc=[["path",{d:"M16 22h2a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v2.85"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 14v2.2l1.6 1"}],["circle",{cx:"8",cy:"16",r:"6"}]],X1=[["path",{d:"M4 12.15V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-3.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m5 16-3 3 3 3"}],["path",{d:"m9 22 3-3-3-3"}]],a7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 12.5 8 15l2 2.5"}],["path",{d:"m14 12.5 2 2.5-2 2.5"}]],j1=[["path",{d:"M13.85 22H18a2 2 0 0 0 2-2V8a2 2 0 0 0-.586-1.414l-4-4A2 2 0 0 0 14 2H6a2 2 0 0 0-2 2v6.6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m3.305 19.53.923-.382"}],["path",{d:"m4.228 16.852-.924-.383"}],["path",{d:"m5.852 15.228-.383-.923"}],["path",{d:"m5.852 20.772-.383.924"}],["path",{d:"m8.148 15.228.383-.923"}],["path",{d:"m8.53 21.696-.382-.924"}],["path",{d:"m9.773 16.852.922-.383"}],["path",{d:"m9.773 19.148.922.383"}],["circle",{cx:"7",cy:"18",r:"3"}]],t7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M9 10h6"}],["path",{d:"M12 13V7"}],["path",{d:"M9 17h6"}]],h7=[["path",{d:"M4 12V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 16h2v6"}],["path",{d:"M10 22h4"}],["rect",{x:"2",y:"16",width:"4",height:"6",rx:"2"}]],d7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M12 18v-6"}],["path",{d:"m9 15 3 3 3-3"}]],N1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M12 9v4"}],["path",{d:"M12 17h.01"}]],r=[["path",{d:"M4 6.835V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-.343"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M2 19a2 2 0 0 1 4 0v1a2 2 0 0 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 0 1-4 0v-1a2 2 0 0 1 4 0"}]],c7=[["path",{d:"M13 22h5a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v7"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M3.62 18.8A2.25 2.25 0 1 1 7 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a1 1 0 0 1-1.507 0z"}]],M7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["circle",{cx:"10",cy:"12",r:"2"}],["path",{d:"m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22"}]],p7=[["path",{d:"M4 11V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-1"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M2 15h10"}],["path",{d:"m9 18 3-3-3-3"}]],K1=[["path",{d:"M10.65 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v10.1"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m10 15 1 1"}],["path",{d:"m11 14-4.586 4.586"}],["circle",{cx:"5",cy:"20",r:"2"}]],Q1=[["path",{d:"M4 9.8V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-3"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M9 17v-2a2 2 0 0 0-4 0v2"}],["rect",{width:"8",height:"5",x:"3",y:"17",rx:"1"}]],J1=[["path",{d:"M20 14V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M14 18h6"}]],i7=[["path",{d:"M11.65 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v10.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 20v-7l3 1.474"}],["circle",{cx:"6",cy:"20",r:"2"}]],n7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M9 15h6"}]],l7=[["path",{d:"M4.226 20.925A2 2 0 0 0 6 22h12a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v3.127"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m5 11-3 3"}],["path",{d:"m5 17-3-3h10"}]],Y1=[["path",{d:"m18.226 5.226-2.52-2.52A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-.351"}],["path",{d:"M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["path",{d:"M8 18h1"}]],_1=[["path",{d:"M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z"}]],x1=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z"}]],e7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M9 15h6"}],["path",{d:"M12 18v-6"}]],a2=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M12 17h.01"}],["path",{d:"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3"}]],t2=[["path",{d:"M11.35 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v5.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M14 19h6"}],["path",{d:"M17 16v6"}]],r7=[["path",{d:"M20 10V8a2.4 2.4 0 0 0-.706-1.704l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.35"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M16 14a2 2 0 0 0-2 2"}],["path",{d:"M16 22a2 2 0 0 1-2-2"}],["path",{d:"M20 14a2 2 0 0 1 2 2"}],["path",{d:"M20 22a2 2 0 0 0 2-2"}]],h2=[["path",{d:"M11.1 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.589 3.588A2.4 2.4 0 0 1 20 8v3.25"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m21 22-2.88-2.88"}],["circle",{cx:"16",cy:"17",r:"3"}]],o7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["circle",{cx:"11.5",cy:"14.5",r:"2.5"}],["path",{d:"M13.3 16.3 15 18"}]],d2=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 15h.01"}],["path",{d:"M11.5 13.5a2.5 2.5 0 0 1 0 3"}],["path",{d:"M15 12a5 5 0 0 1 0 6"}]],v7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 13h2"}],["path",{d:"M14 13h2"}],["path",{d:"M8 17h2"}],["path",{d:"M14 17h2"}]],$7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 12h8"}],["path",{d:"M10 11v2"}],["path",{d:"M8 17h8"}],["path",{d:"M14 16v2"}]],m7=[["path",{d:"M11 21a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1"}],["path",{d:"M16 16a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1"}],["path",{d:"M21 6a2 2 0 0 0-.586-1.414l-2-2A2 2 0 0 0 17 2h-3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1z"}]],y7=[["path",{d:"M4 11V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h7"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m10 18 3-3-3-3"}]],s7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m8 16 2-2-2-2"}],["path",{d:"M12 18h4"}]],g7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 9H8"}],["path",{d:"M16 13H8"}],["path",{d:"M16 17H8"}]],c2=[["path",{d:"M12 22h6a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M3 16v-1.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5V16"}],["path",{d:"M6 22h2"}],["path",{d:"M7 14v8"}]],C7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M11 18h2"}],["path",{d:"M12 12v6"}],["path",{d:"M9 13v-.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v.5"}]],u7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M12 12v6"}],["path",{d:"m15 15-3-3-3 3"}]],H7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M16 22a4 4 0 0 0-8 0"}],["circle",{cx:"12",cy:"15",r:"3"}]],A7=[["path",{d:"M4 11.55V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-1.95"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M12 15a5 5 0 0 1 0 6"}],["path",{d:"M8 14.502a.5.5 0 0 0-.826-.381l-1.893 1.631a1 1 0 0 1-.651.243H3.5a.5.5 0 0 0-.5.501v3.006a.5.5 0 0 0 .5.501h1.129a1 1 0 0 1 .652.243l1.893 1.633a.5.5 0 0 0 .826-.38z"}]],M2=[["path",{d:"M4 12V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m10 17.843 3.033-1.755a.64.64 0 0 1 .967.56v4.704a.65.65 0 0 1-.967.56L10 20.157"}],["rect",{width:"7",height:"6",x:"3",y:"16",rx:"1"}]],p2=[["path",{d:"M11 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v5"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m15 17 5 5"}],["path",{d:"m20 17-5 5"}]],w7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"m14.5 12.5-5 5"}],["path",{d:"m9.5 12.5 5 5"}]],V7=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}]],S7=[["path",{d:"M15 2h-4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8"}],["path",{d:"M16.706 2.706A2.4 2.4 0 0 0 15 2v5a1 1 0 0 0 1 1h5a2.4 2.4 0 0 0-.706-1.706z"}],["path",{d:"M5 7a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 1.732-1"}]],L7=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 3v18"}],["path",{d:"M3 7.5h4"}],["path",{d:"M3 12h18"}],["path",{d:"M3 16.5h4"}],["path",{d:"M17 3v18"}],["path",{d:"M17 7.5h4"}],["path",{d:"M17 16.5h4"}]],i2=[["path",{d:"M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"}],["path",{d:"M14 13.12c0 2.38 0 6.38-1 8.88"}],["path",{d:"M17.29 21.02c.12-.6.43-2.3.5-3.02"}],["path",{d:"M2 12a10 10 0 0 1 18-6"}],["path",{d:"M2 16h.01"}],["path",{d:"M21.8 16c.2-2 .131-5.354 0-6"}],["path",{d:"M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2"}],["path",{d:"M8.65 22c.21-.66.45-1.32.57-2"}],["path",{d:"M9 6.8a6 6 0 0 1 9 5.2v2"}]],f7=[["path",{d:"M15 6.5V3a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3.5"}],["path",{d:"M9 18h8"}],["path",{d:"M18 3h-3"}],["path",{d:"M11 3a6 6 0 0 0-6 6v11"}],["path",{d:"M5 13h4"}],["path",{d:"M17 10a4 4 0 0 0-8 0v10a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2Z"}]],k7=[["path",{d:"M18 12.47v.03m0-.5v.47m-.475 5.056A6.744 6.744 0 0 1 15 18c-3.56 0-7.56-2.53-8.5-6 .348-1.28 1.114-2.433 2.121-3.38m3.444-2.088A8.802 8.802 0 0 1 15 6c3.56 0 6.06 2.54 7 6-.309 1.14-.786 2.177-1.413 3.058"}],["path",{d:"M7 10.67C7 8 5.58 5.97 2.73 5.5c-1 1.5-1 5 .23 6.5-1.24 1.5-1.24 5-.23 6.5C5.58 18.03 7 16 7 13.33m7.48-4.372A9.77 9.77 0 0 1 16 6.07m0 11.86a9.77 9.77 0 0 1-1.728-3.618"}],["path",{d:"m16.01 17.93-.23 1.4A2 2 0 0 1 13.8 21H9.5a5.96 5.96 0 0 0 1.49-3.98M8.53 3h5.27a2 2 0 0 1 1.98 1.67l.23 1.4M2 2l20 20"}]],P7=[["path",{d:"M2 16s9-15 20-4C11 23 2 8 2 8"}]],B7=[["path",{d:"M6.5 12c.94-3.46 4.94-6 8.5-6 3.56 0 6.06 2.54 7 6-.94 3.47-3.44 6-7 6s-7.56-2.53-8.5-6Z"}],["path",{d:"M18 12v.5"}],["path",{d:"M16 17.93a9.77 9.77 0 0 1 0-11.86"}],["path",{d:"M7 10.67C7 8 5.58 5.97 2.73 5.5c-1 1.5-1 5 .23 6.5-1.24 1.5-1.24 5-.23 6.5C5.58 18.03 7 16 7 13.33"}],["path",{d:"M10.46 7.26C10.2 5.88 9.17 4.24 8 3h5.8a2 2 0 0 1 1.98 1.67l.23 1.4"}],["path",{d:"m16.01 17.93-.23 1.4A2 2 0 0 1 13.8 21H9.5a5.96 5.96 0 0 0 1.49-3.98"}]],z7=[["path",{d:"m17.586 11.414-5.93 5.93a1 1 0 0 1-8-8l3.137-3.137a.707.707 0 0 1 1.207.5V10"}],["path",{d:"M20.414 8.586 22 7"}],["circle",{cx:"19",cy:"10",r:"2"}]],F7=[["path",{d:"M16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"}],["path",{d:"m2 2 20 20"}],["path",{d:"M4 22V4"}],["path",{d:"M7.656 2H8c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10.347"}]],D7=[["path",{d:"M18 22V2.8a.8.8 0 0 0-1.17-.71L5.45 7.78a.8.8 0 0 0 0 1.44L18 15.5"}]],b7=[["path",{d:"M6 22V2.8a.8.8 0 0 1 1.17-.71l11.38 5.69a.8.8 0 0 1 0 1.44L6 15.5"}]],R7=[["path",{d:"M12 2c1 3 2.5 3.5 3.5 4.5A5 5 0 0 1 17 10a5 5 0 1 1-10 0c0-.3 0-.6.1-.9a2 2 0 1 0 3.3-2C8 4.5 11 2 12 2Z"}],["path",{d:"m5 22 14-4"}],["path",{d:"m5 18 14 4"}]],T7=[["path",{d:"M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528"}]],q7=[["path",{d:"M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4"}]],U7=[["path",{d:"M11.652 6H18"}],["path",{d:"M12 13v1"}],["path",{d:"M16 16v4a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-8a4 4 0 0 0-.8-2.4l-.6-.8A3 3 0 0 1 6 7V6"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7.649 2H17a1 1 0 0 1 1 1v4a3 3 0 0 1-.6 1.8l-.6.8a4 4 0 0 0-.55 1.007"}]],O7=[["path",{d:"M12 13v1"}],["path",{d:"M17 2a1 1 0 0 1 1 1v4a3 3 0 0 1-.6 1.8l-.6.8A4 4 0 0 0 16 12v8a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2v-8a4 4 0 0 0-.8-2.4l-.6-.8A3 3 0 0 1 6 7V3a1 1 0 0 1 1-1z"}],["path",{d:"M6 6h12"}]],Z7=[["path",{d:"M10 2v2.343"}],["path",{d:"M14 2v6.343"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20a2 2 0 0 1-2 2H6a2 2 0 0 1-1.755-2.96l5.227-9.563"}],["path",{d:"M6.453 15H15"}],["path",{d:"M8.5 2h7"}]],G7=[["path",{d:"M10 2v6.292a7 7 0 1 0 4 0V2"}],["path",{d:"M5 15h14"}],["path",{d:"M8.5 2h7"}]],W7=[["path",{d:"M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"}],["path",{d:"M6.453 15h11.094"}],["path",{d:"M8.5 2h7"}]],I7=[["path",{d:"M8 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h3"}],["path",{d:"M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"}],["path",{d:"M12 20v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 2v2"}]],E7=[["path",{d:"m3 7 5 5-5 5V7"}],["path",{d:"m21 7-5 5 5 5V7"}],["path",{d:"M12 20v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 2v2"}]],X7=[["path",{d:"m17 3-5 5-5-5h10"}],["path",{d:"m17 21-5-5-5 5h10"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],j7=[["path",{d:"M21 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v3"}],["path",{d:"M21 16v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],N7=[["path",{d:"M12 5a3 3 0 1 1 3 3m-3-3a3 3 0 1 0-3 3m3-3v1M9 8a3 3 0 1 0 3 3M9 8h1m5 0a3 3 0 1 1-3 3m3-3h-1m-2 3v-1"}],["circle",{cx:"12",cy:"8",r:"2"}],["path",{d:"M12 10v12"}],["path",{d:"M12 22c4.2 0 7-1.667 7-5-4.2 0-7 1.667-7 5Z"}],["path",{d:"M12 22c-4.2 0-7-1.667-7-5 4.2 0 7 1.667 7 5Z"}]],K7=[["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M12 16.5A4.5 4.5 0 1 1 7.5 12 4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 1 1 4.5 4.5 4.5 4.5 0 1 1-4.5 4.5"}],["path",{d:"M12 7.5V9"}],["path",{d:"M7.5 12H9"}],["path",{d:"M16.5 12H15"}],["path",{d:"M12 16.5V15"}],["path",{d:"m8 8 1.88 1.88"}],["path",{d:"M14.12 9.88 16 8"}],["path",{d:"m8 16 1.88-1.88"}],["path",{d:"M14.12 14.12 16 16"}]],Q7=[["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}]],J7=[["path",{d:"M2 12h6"}],["path",{d:"M22 12h-6"}],["path",{d:"M12 2v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 20v2"}],["path",{d:"m19 9-3 3 3 3"}],["path",{d:"m5 15 3-3-3-3"}]],Y7=[["path",{d:"M12 22v-6"}],["path",{d:"M12 8V2"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}],["path",{d:"m15 19-3-3-3 3"}],["path",{d:"m15 5-3 3-3-3"}]],_7=[["circle",{cx:"15",cy:"19",r:"2"}],["path",{d:"M20.9 19.8A2 2 0 0 0 22 18V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h5.1"}],["path",{d:"M15 11v-1"}],["path",{d:"M15 17v-2"}]],x7=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"m9 13 2 2 4-4"}]],aM=[["path",{d:"M16 14v2.2l1.6 1"}],["path",{d:"M7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2"}],["circle",{cx:"16",cy:"16",r:"6"}]],tM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M2 10h20"}]],hM=[["path",{d:"M10 10.5 8 13l2 2.5"}],["path",{d:"m14 10.5 2 2.5-2 2.5"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2z"}]],n2=[["path",{d:"M10.3 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.98a2 2 0 0 1 1.69.9l.66 1.2A2 2 0 0 0 12 6h8a2 2 0 0 1 2 2v3.3"}],["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["circle",{cx:"18",cy:"18",r:"3"}]],dM=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["circle",{cx:"12",cy:"13",r:"1"}]],cM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M12 10v6"}],["path",{d:"m15 13-3 3-3-3"}]],MM=[["path",{d:"M18 19a5 5 0 0 1-5-5v8"}],["path",{d:"M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5"}],["circle",{cx:"13",cy:"12",r:"2"}],["circle",{cx:"20",cy:"19",r:"2"}]],pM=[["circle",{cx:"12",cy:"13",r:"2"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M14 13h3"}],["path",{d:"M7 13h3"}]],iM=[["path",{d:"M10.638 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v3.417"}],["path",{d:"M14.62 18.8A2.25 2.25 0 1 1 18 15.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}]],nM=[["path",{d:"M2 9V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1"}],["path",{d:"M2 13h10"}],["path",{d:"m9 16 3-3-3-3"}]],lM=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["path",{d:"M8 10v4"}],["path",{d:"M12 10v2"}],["path",{d:"M16 10v6"}]],eM=[["circle",{cx:"16",cy:"20",r:"2"}],["path",{d:"M10 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v2"}],["path",{d:"m22 14-4.5 4.5"}],["path",{d:"m21 15 1 1"}]],rM=[["rect",{width:"8",height:"5",x:"14",y:"17",rx:"1"}],["path",{d:"M10 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v2.5"}],["path",{d:"M20 17v-2a2 2 0 1 0-4 0v2"}]],oM=[["path",{d:"M9 13h6"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],vM=[["path",{d:"m6 14 1.45-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6a2 2 0 0 1-1.94 1.5H4a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h3.93a2 2 0 0 1 1.66.9l.82 1.2a2 2 0 0 0 1.66.9H18a2 2 0 0 1 2 2v2"}],["circle",{cx:"14",cy:"15",r:"1"}]],$M=[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"}]],mM=[["path",{d:"M2 7.5V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-1.5"}],["path",{d:"M2 13h10"}],["path",{d:"m5 10-3 3 3 3"}]],l2=[["path",{d:"M2 11.5V5a2 2 0 0 1 2-2h3.9c.7 0 1.3.3 1.7.9l.8 1.2c.4.6 1 .9 1.7.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-9.5"}],["path",{d:"M11.378 13.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],yM=[["path",{d:"M12 10v6"}],["path",{d:"M9 13h6"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],sM=[["path",{d:"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"}],["circle",{cx:"12",cy:"13",r:"2"}],["path",{d:"M12 15v5"}]],gM=[["circle",{cx:"11.5",cy:"12.5",r:"2.5"}],["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M13.3 14.3 15 16"}]],CM=[["path",{d:"M10.7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4.1"}],["path",{d:"m21 21-1.9-1.9"}],["circle",{cx:"17",cy:"17",r:"3"}]],uM=[["path",{d:"M2 9.35V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h7"}],["path",{d:"m8 16 3-3-3-3"}]],HM=[["path",{d:"M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v.5"}],["path",{d:"M12 10v4h4"}],["path",{d:"m12 14 1.535-1.605a5 5 0 0 1 8 1.5"}],["path",{d:"M22 22v-4h-4"}],["path",{d:"m22 18-1.535 1.605a5 5 0 0 1-8-1.5"}]],AM=[["path",{d:"M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"}],["path",{d:"M3 5a2 2 0 0 0 2 2h3"}],["path",{d:"M3 3v13a2 2 0 0 0 2 2h3"}]],wM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"M12 10v6"}],["path",{d:"m9 13 3-3 3 3"}]],VM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}],["path",{d:"m9.5 10.5 5 5"}],["path",{d:"m14.5 10.5-5 5"}]],SM=[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"}]],LM=[["path",{d:"M20 5a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h2.5a1.5 1.5 0 0 1 1.2.6l.6.8a1.5 1.5 0 0 0 1.2.6z"}],["path",{d:"M3 8.268a2 2 0 0 0-1 1.738V19a2 2 0 0 0 2 2h11a2 2 0 0 0 1.732-1"}]],fM=[["path",{d:"M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"}],["path",{d:"M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"}],["path",{d:"M16 17h4"}],["path",{d:"M4 13h4"}]],kM=[["path",{d:"M12 12H5a2 2 0 0 0-2 2v5"}],["circle",{cx:"13",cy:"19",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}],["path",{d:"M8 19h3m5-17v17h6M6 12V7c0-1.1.9-2 2-2h3l5 5"}]],PM=[["path",{d:"M4 14h6"}],["path",{d:"M4 2h10"}],["rect",{x:"4",y:"18",width:"16",height:"4",rx:"1"}],["rect",{x:"4",y:"6",width:"16",height:"4",rx:"1"}]],BM=[["path",{d:"m15 17 5-5-5-5"}],["path",{d:"M4 18v-2a4 4 0 0 1 4-4h12"}]],zM=[["line",{x1:"22",x2:"2",y1:"6",y2:"6"}],["line",{x1:"22",x2:"2",y1:"18",y2:"18"}],["line",{x1:"6",x2:"6",y1:"2",y2:"22"}],["line",{x1:"18",x2:"18",y1:"2",y2:"22"}]],FM=[["path",{d:"M5 16V9h14V2H5l14 14h-7m-7 0 7 7v-7m-7 0h7"}]],DM=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M16 16s-1.5-2-4-2-4 2-4 2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],bM=[["path",{d:"M14 13h2a2 2 0 0 1 2 2v2a2 2 0 0 0 4 0v-6.998a2 2 0 0 0-.59-1.42L18 5"}],["path",{d:"M14 21V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v16"}],["path",{d:"M2 21h13"}],["path",{d:"M3 9h11"}]],RM=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["rect",{width:"10",height:"8",x:"7",y:"8",rx:"1"}]],TM=[["path",{d:"M13.354 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l1.218-1.348"}],["path",{d:"M16 6h6"}],["path",{d:"M19 3v6"}]],e2=[["path",{d:"M12.531 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14v6a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341l.427-.473"}],["path",{d:"m16.5 3.5 5 5"}],["path",{d:"m21.5 3.5-5 5"}]],r2=[["path",{d:"M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z"}]],qM=[["path",{d:"M2 7v10"}],["path",{d:"M6 5v14"}],["rect",{width:"12",height:"18",x:"10",y:"3",rx:"2"}]],UM=[["path",{d:"M2 3v18"}],["rect",{width:"12",height:"18",x:"6",y:"3",rx:"2"}],["path",{d:"M22 3v18"}]],OM=[["rect",{width:"18",height:"14",x:"3",y:"3",rx:"2"}],["path",{d:"M4 21h1"}],["path",{d:"M9 21h1"}],["path",{d:"M14 21h1"}],["path",{d:"M19 21h1"}]],ZM=[["path",{d:"M7 2h10"}],["path",{d:"M5 6h14"}],["rect",{width:"18",height:"12",x:"3",y:"10",rx:"2"}]],GM=[["path",{d:"M3 2h18"}],["rect",{width:"18",height:"12",x:"3",y:"6",rx:"2"}],["path",{d:"M3 22h18"}]],WM=[["line",{x1:"6",x2:"10",y1:"11",y2:"11"}],["line",{x1:"8",x2:"8",y1:"9",y2:"13"}],["line",{x1:"15",x2:"15.01",y1:"12",y2:"12"}],["line",{x1:"18",x2:"18.01",y1:"10",y2:"10"}],["path",{d:"M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z"}]],IM=[["path",{d:"M11.146 15.854a1.207 1.207 0 0 1 1.708 0l1.56 1.56A2 2 0 0 1 15 18.828V21a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-2.172a2 2 0 0 1 .586-1.414z"}],["path",{d:"M18.828 15a2 2 0 0 1-1.414-.586l-1.56-1.56a1.207 1.207 0 0 1 0-1.708l1.56-1.56A2 2 0 0 1 18.828 9H21a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1z"}],["path",{d:"M6.586 14.414A2 2 0 0 1 5.172 15H3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2.172a2 2 0 0 1 1.414.586l1.56 1.56a1.207 1.207 0 0 1 0 1.708z"}],["path",{d:"M9 3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2.172a2 2 0 0 1-.586 1.414l-1.56 1.56a1.207 1.207 0 0 1-1.708 0l-1.56-1.56A2 2 0 0 1 9 5.172z"}]],EM=[["line",{x1:"6",x2:"10",y1:"12",y2:"12"}],["line",{x1:"8",x2:"8",y1:"10",y2:"14"}],["line",{x1:"15",x2:"15.01",y1:"13",y2:"13"}],["line",{x1:"18",x2:"18.01",y1:"11",y2:"11"}],["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],XM=[["path",{d:"m12 14 4-4"}],["path",{d:"M3.34 19a10 10 0 1 1 17.32 0"}]],jM=[["path",{d:"m14 13-8.381 8.38a1 1 0 0 1-3.001-3l8.384-8.381"}],["path",{d:"m16 16 6-6"}],["path",{d:"m21.5 10.5-8-8"}],["path",{d:"m8 8 6-6"}],["path",{d:"m8.5 7.5 8 8"}]],NM=[["path",{d:"M10.5 3 8 9l4 13 4-13-2.5-6"}],["path",{d:"M17 3a2 2 0 0 1 1.6.8l3 4a2 2 0 0 1 .013 2.382l-7.99 10.986a2 2 0 0 1-3.247 0l-7.99-10.986A2 2 0 0 1 2.4 7.8l2.998-3.997A2 2 0 0 1 7 3z"}],["path",{d:"M2 9h20"}]],KM=[["path",{d:"M11.5 21a7.5 7.5 0 1 1 7.35-9"}],["path",{d:"M13 12V3"}],["path",{d:"M4 21h16"}],["path",{d:"M9 12V3"}]],QM=[["path",{d:"M9 10h.01"}],["path",{d:"M15 10h.01"}],["path",{d:"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z"}]],JM=[["rect",{x:"3",y:"8",width:"18",height:"4",rx:"1"}],["path",{d:"M12 8v13"}],["path",{d:"M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7"}],["path",{d:"M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5"}]],YM=[["path",{d:"M15 6a9 9 0 0 0-9 9V3"}],["path",{d:"M21 18h-6"}],["circle",{cx:"18",cy:"6",r:"3"}],["circle",{cx:"6",cy:"18",r:"3"}]],_M=[["line",{x1:"6",x2:"6",y1:"3",y2:"15"}],["circle",{cx:"18",cy:"6",r:"3"}],["circle",{cx:"6",cy:"18",r:"3"}],["path",{d:"M18 9a9 9 0 0 1-9 9"}]],xM=[["path",{d:"M6 3v12"}],["path",{d:"M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"}],["path",{d:"M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"}],["path",{d:"M15 6a9 9 0 0 0-9 9"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}]],o2=[["circle",{cx:"12",cy:"12",r:"3"}],["line",{x1:"3",x2:"9",y1:"12",y2:"12"}],["line",{x1:"15",x2:"21",y1:"12",y2:"12"}]],a9=[["path",{d:"M12 3v6"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"M12 15v6"}]],t9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v7"}],["path",{d:"m15 9-3-3 3-3"}],["circle",{cx:"19",cy:"18",r:"3"}],["path",{d:"M12 18H7a2 2 0 0 1-2-2V9"}],["path",{d:"m9 15 3 3-3 3"}]],h9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7"}],["path",{d:"M11 18H8a2 2 0 0 1-2-2V9"}]],d9=[["circle",{cx:"12",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["circle",{cx:"18",cy:"6",r:"3"}],["path",{d:"M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9"}],["path",{d:"M12 12v3"}]],c9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v6"}],["circle",{cx:"5",cy:"18",r:"3"}],["path",{d:"M12 3v18"}],["circle",{cx:"19",cy:"6",r:"3"}],["path",{d:"M16 15.7A9 9 0 0 0 19 9"}]],M9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 21V9a9 9 0 0 0 9 9"}]],p9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v12"}],["circle",{cx:"19",cy:"18",r:"3"}],["path",{d:"m15 9-3-3 3-3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v7"}]],i9=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 9v12"}],["path",{d:"m21 3-6 6"}],["path",{d:"m21 9-6-6"}],["path",{d:"M18 11.5V15"}],["circle",{cx:"18",cy:"18",r:"3"}]],n9=[["circle",{cx:"5",cy:"6",r:"3"}],["path",{d:"M5 9v12"}],["path",{d:"m15 9-3-3 3-3"}],["path",{d:"M12 6h5a2 2 0 0 1 2 2v3"}],["path",{d:"M19 15v6"}],["path",{d:"M22 18h-6"}]],l9=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M6 9v12"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v3"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}]],e9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M18 6V5"}],["path",{d:"M18 11v-1"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21"}]],r9=[["circle",{cx:"18",cy:"18",r:"3"}],["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M13 6h3a2 2 0 0 1 2 2v7"}],["line",{x1:"6",x2:"6",y1:"9",y2:"21"}]],o9=[["path",{d:"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"}],["path",{d:"M9 18c-4.51 2-5-2-7-2"}]],v9=[["path",{d:"m22 13.29-3.33-10a.42.42 0 0 0-.14-.18.38.38 0 0 0-.22-.11.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18l-2.26 6.67H8.32L6.1 3.26a.42.42 0 0 0-.1-.18.38.38 0 0 0-.26-.08.39.39 0 0 0-.23.07.42.42 0 0 0-.14.18L2 13.29a.74.74 0 0 0 .27.83L12 21l9.69-6.88a.71.71 0 0 0 .31-.83Z"}]],$9=[["path",{d:"M5.116 4.104A1 1 0 0 1 6.11 3h11.78a1 1 0 0 1 .994 1.105L17.19 20.21A2 2 0 0 1 15.2 22H8.8a2 2 0 0 1-2-1.79z"}],["path",{d:"M6 12a5 5 0 0 1 6 0 5 5 0 0 0 6 0"}]],m9=[["circle",{cx:"6",cy:"15",r:"4"}],["circle",{cx:"18",cy:"15",r:"4"}],["path",{d:"M14 15a2 2 0 0 0-2-2 2 2 0 0 0-2 2"}],["path",{d:"M2.5 13 5 7c.7-1.3 1.4-2 3-2"}],["path",{d:"M21.5 13 19 7c-.7-1.3-1.5-2-3-2"}]],y9=[["path",{d:"M15.686 15A14.5 14.5 0 0 1 12 22a14.5 14.5 0 0 1 0-20 10 10 0 1 0 9.542 13"}],["path",{d:"M2 12h8.5"}],["path",{d:"M20 6V4a2 2 0 1 0-4 0v2"}],["rect",{width:"8",height:"5",x:"14",y:"6",rx:"1"}]],s9=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"}],["path",{d:"M2 12h20"}]],g9=[["path",{d:"M12 13V2l8 4-8 4"}],["path",{d:"M20.561 10.222a9 9 0 1 1-12.55-5.29"}],["path",{d:"M8.002 9.997a5 5 0 1 0 8.9 2.02"}]],C9=[["path",{d:"M2 21V3"}],["path",{d:"M2 5h18a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2.26"}],["path",{d:"M7 17v3a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-3"}],["circle",{cx:"16",cy:"11",r:"2"}],["circle",{cx:"8",cy:"11",r:"2"}]],u9=[["path",{d:"M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z"}],["path",{d:"M22 10v6"}],["path",{d:"M6 12.5V16a6 3 0 0 0 12 0v-3.5"}]],H9=[["path",{d:"M22 5V2l-5.89 5.89"}],["circle",{cx:"16.6",cy:"15.89",r:"3"}],["circle",{cx:"8.11",cy:"7.4",r:"3"}],["circle",{cx:"12.35",cy:"11.65",r:"3"}],["circle",{cx:"13.91",cy:"5.85",r:"3"}],["circle",{cx:"18.15",cy:"10.09",r:"3"}],["circle",{cx:"6.56",cy:"13.2",r:"3"}],["circle",{cx:"10.8",cy:"17.44",r:"3"}],["circle",{cx:"5",cy:"19",r:"3"}]],v2=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"m16 19 2 2 4-4"}]],$2=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"M16 19h6"}],["path",{d:"M19 22v-6"}]],m2=[["path",{d:"M12 3v17a1 1 0 0 1-1 1H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v6a1 1 0 0 1-1 1H3"}],["path",{d:"m16 16 5 5"}],["path",{d:"m16 21 5-5"}]],y2=[["path",{d:"M12 3v18"}],["path",{d:"M3 12h18"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],A9=[["path",{d:"M15 3v18"}],["path",{d:"M3 12h18"}],["path",{d:"M9 3v18"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],o=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M3 15h18"}],["path",{d:"M9 3v18"}],["path",{d:"M15 3v18"}]],w9=[["circle",{cx:"12",cy:"9",r:"1"}],["circle",{cx:"19",cy:"9",r:"1"}],["circle",{cx:"5",cy:"9",r:"1"}],["circle",{cx:"12",cy:"15",r:"1"}],["circle",{cx:"19",cy:"15",r:"1"}],["circle",{cx:"5",cy:"15",r:"1"}]],V9=[["circle",{cx:"9",cy:"12",r:"1"}],["circle",{cx:"9",cy:"5",r:"1"}],["circle",{cx:"9",cy:"19",r:"1"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"15",cy:"5",r:"1"}],["circle",{cx:"15",cy:"19",r:"1"}]],S9=[["circle",{cx:"12",cy:"5",r:"1"}],["circle",{cx:"19",cy:"5",r:"1"}],["circle",{cx:"5",cy:"5",r:"1"}],["circle",{cx:"12",cy:"12",r:"1"}],["circle",{cx:"19",cy:"12",r:"1"}],["circle",{cx:"5",cy:"12",r:"1"}],["circle",{cx:"12",cy:"19",r:"1"}],["circle",{cx:"19",cy:"19",r:"1"}],["circle",{cx:"5",cy:"19",r:"1"}]],L9=[["path",{d:"M3 7V5c0-1.1.9-2 2-2h2"}],["path",{d:"M17 3h2c1.1 0 2 .9 2 2v2"}],["path",{d:"M21 17v2c0 1.1-.9 2-2 2h-2"}],["path",{d:"M7 21H5c-1.1 0-2-.9-2-2v-2"}],["rect",{width:"7",height:"5",x:"7",y:"7",rx:"1"}],["rect",{width:"7",height:"5",x:"10",y:"12",rx:"1"}]],f9=[["path",{d:"M13.144 21.144A7.274 10.445 45 1 0 2.856 10.856"}],["path",{d:"M13.144 21.144A7.274 4.365 45 0 0 2.856 10.856a7.274 4.365 45 0 0 10.288 10.288"}],["path",{d:"M16.565 10.435 18.6 8.4a2.501 2.501 0 1 0 1.65-4.65 2.5 2.5 0 1 0-4.66 1.66l-2.024 2.025"}],["path",{d:"m8.5 16.5-1-1"}]],k9=[["path",{d:"m11.9 12.1 4.514-4.514"}],["path",{d:"M20.1 2.3a1 1 0 0 0-1.4 0l-1.114 1.114A2 2 0 0 0 17 4.828v1.344a2 2 0 0 1-.586 1.414A2 2 0 0 1 17.828 7h1.344a2 2 0 0 0 1.414-.586L21.7 5.3a1 1 0 0 0 0-1.4z"}],["path",{d:"m6 16 2 2"}],["path",{d:"M8.23 9.85A3 3 0 0 1 11 8a5 5 0 0 1 5 5 3 3 0 0 1-1.85 2.77l-.92.38A2 2 0 0 0 12 18a4 4 0 0 1-4 4 6 6 0 0 1-6-6 4 4 0 0 1 4-4 2 2 0 0 0 1.85-1.23z"}]],P9=[["path",{d:"M12 16H4a2 2 0 1 1 0-4h16a2 2 0 1 1 0 4h-4.25"}],["path",{d:"M5 12a2 2 0 0 1-2-2 9 7 0 0 1 18 0 2 2 0 0 1-2 2"}],["path",{d:"M5 16a2 2 0 0 0-2 2 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 2 2 0 0 0-2-2q0 0 0 0"}],["path",{d:"m6.67 12 6.13 4.6a2 2 0 0 0 2.8-.4l3.15-4.2"}]],B9=[["path",{d:"m15 12-9.373 9.373a1 1 0 0 1-3.001-3L12 9"}],["path",{d:"m18 15 4-4"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172v-.344a2 2 0 0 0-.586-1.414l-1.657-1.657A6 6 0 0 0 12.516 3H9l1.243 1.243A6 6 0 0 1 12 8.485V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5"}]],z9=[["path",{d:"M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"}],["path",{d:"m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"}],["path",{d:"m2 16 6 6"}],["circle",{cx:"16",cy:"9",r:"2.9"}],["circle",{cx:"6",cy:"5",r:"3"}]],F9=[["path",{d:"M12.035 17.012a3 3 0 0 0-3-3l-.311-.002a.72.72 0 0 1-.505-1.229l1.195-1.195A2 2 0 0 1 10.828 11H12a2 2 0 0 0 0-4H9.243a3 3 0 0 0-2.122.879l-2.707 2.707A4.83 4.83 0 0 0 3 14a8 8 0 0 0 8 8h2a8 8 0 0 0 8-8V7a2 2 0 1 0-4 0v2a2 2 0 1 0 4 0"}],["path",{d:"M13.888 9.662A2 2 0 0 0 17 8V5A2 2 0 1 0 13 5"}],["path",{d:"M9 5A2 2 0 1 0 5 5V10"}],["path",{d:"M9 7V4A2 2 0 1 1 13 4V7.268"}]],s2=[["path",{d:"M18 11.5V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1.4"}],["path",{d:"M14 10V8a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"}],["path",{d:"M10 9.9V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v5"}],["path",{d:"M6 14a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-4a8 8 0 0 1-8-8 2 2 0 1 1 4 0"}]],D9=[["path",{d:"M11 14h2a2 2 0 0 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 16"}],["path",{d:"m14.45 13.39 5.05-4.694C20.196 8 21 6.85 21 5.75a2.75 2.75 0 0 0-4.797-1.837.276.276 0 0 1-.406 0A2.75 2.75 0 0 0 11 5.75c0 1.2.802 2.248 1.5 2.946L16 11.95"}],["path",{d:"m2 15 6 6"}],["path",{d:"m7 20 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a1 1 0 0 0-2.75-2.91"}]],g2=[["path",{d:"M11 12h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 14"}],["path",{d:"m7 18 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"}],["path",{d:"m2 13 6 6"}]],b9=[["path",{d:"M18 12.5V10a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1.4"}],["path",{d:"M14 11V9a2 2 0 1 0-4 0v2"}],["path",{d:"M10 10.5V5a2 2 0 1 0-4 0v9"}],["path",{d:"m7 15-1.76-1.76a2 2 0 0 0-2.83 2.82l3.6 3.6C7.5 21.14 9.2 22 12 22h2a8 8 0 0 0 8-8V7a2 2 0 1 0-4 0v5"}]],R9=[["path",{d:"M12 3V2"}],["path",{d:"m15.4 17.4 3.2-2.8a2 2 0 1 1 2.8 2.9l-3.6 3.3c-.7.8-1.7 1.2-2.8 1.2h-4c-1.1 0-2.1-.4-2.8-1.2l-1.302-1.464A1 1 0 0 0 6.151 19H5"}],["path",{d:"M2 14h12a2 2 0 0 1 0 4h-2"}],["path",{d:"M4 10h16"}],["path",{d:"M5 10a7 7 0 0 1 14 0"}],["path",{d:"M5 14v6a1 1 0 0 1-1 1H2"}]],T9=[["path",{d:"M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"}],["path",{d:"M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"}],["path",{d:"M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"}]],q9=[["path",{d:"M2.048 18.566A2 2 0 0 0 4 21h16a2 2 0 0 0 1.952-2.434l-2-9A2 2 0 0 0 18 8H6a2 2 0 0 0-1.952 1.566z"}],["path",{d:"M8 11V6a4 4 0 0 1 8 0v5"}]],U9=[["path",{d:"m11 17 2 2a1 1 0 1 0 3-3"}],["path",{d:"m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"}],["path",{d:"m21 3 1 11h-2"}],["path",{d:"M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"}],["path",{d:"M3 4h8"}]],O9=[["path",{d:"M12 2v8"}],["path",{d:"m16 6-4 4-4-4"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 18h.01"}]],Z9=[["path",{d:"m16 6-4-4-4 4"}],["path",{d:"M12 2v8"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6 18h.01"}],["path",{d:"M10 18h.01"}]],G9=[["path",{d:"M10 10V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v5"}],["path",{d:"M14 6a6 6 0 0 1 6 6v3"}],["path",{d:"M4 15v-3a6 6 0 0 1 6-6"}],["rect",{x:"2",y:"15",width:"20",height:"4",rx:"1"}]],W9=[["line",{x1:"4",x2:"20",y1:"9",y2:"9"}],["line",{x1:"4",x2:"20",y1:"15",y2:"15"}],["line",{x1:"10",x2:"8",y1:"3",y2:"21"}],["line",{x1:"16",x2:"14",y1:"3",y2:"21"}]],I9=[["line",{x1:"22",x2:"2",y1:"12",y2:"12"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16"}]],E9=[["path",{d:"M14 18a2 2 0 0 0-4 0"}],["path",{d:"m19 11-2.11-6.657a2 2 0 0 0-2.752-1.148l-1.276.61A2 2 0 0 1 12 4H8.5a2 2 0 0 0-1.925 1.456L5 11"}],["path",{d:"M2 11h20"}],["circle",{cx:"17",cy:"18",r:"3"}],["circle",{cx:"7",cy:"18",r:"3"}]],X9=[["path",{d:"m5.2 6.2 1.4 1.4"}],["path",{d:"M2 13h2"}],["path",{d:"M20 13h2"}],["path",{d:"m17.4 7.6 1.4-1.4"}],["path",{d:"M22 17H2"}],["path",{d:"M22 21H2"}],["path",{d:"M16 13a4 4 0 0 0-8 0"}],["path",{d:"M12 5V2.5"}]],j9=[["path",{d:"M10 12H6"}],["path",{d:"M10 15V9"}],["path",{d:"M14 14.5a.5.5 0 0 0 .5.5h1a2.5 2.5 0 0 0 2.5-2.5v-1A2.5 2.5 0 0 0 15.5 9h-1a.5.5 0 0 0-.5.5z"}],["path",{d:"M6 15V9"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],N9=[["path",{d:"M22 9a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h1l2 2h12l2-2h1a1 1 0 0 0 1-1Z"}],["path",{d:"M7.5 12h9"}]],K9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"m17 12 3-2v8"}]],Q9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M17.5 10.5c1.7-1 3.5 0 3.5 1.5a2 2 0 0 1-2 2"}],["path",{d:"M17 17.5c2 1.5 4 .3 4-1.5a2 2 0 0 0-2-2"}]],J9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M21 18h-4c0-4 4-3 4-6 0-1.5-2-2.5-4-1"}]],Y9=[["path",{d:"M12 18V6"}],["path",{d:"M17 10v3a1 1 0 0 0 1 1h3"}],["path",{d:"M21 10v8"}],["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}]],_9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["path",{d:"M17 13v-3h4"}],["path",{d:"M17 17.7c.4.2.8.3 1.3.3 1.5 0 2.7-1.1 2.7-2.5S19.8 13 18.3 13H17"}]],x9=[["path",{d:"M4 12h8"}],["path",{d:"M4 18V6"}],["path",{d:"M12 18V6"}],["circle",{cx:"19",cy:"16",r:"2"}],["path",{d:"M20 10c-2 2-3 3.5-3 6"}]],ap=[["path",{d:"M6 12h12"}],["path",{d:"M6 20V4"}],["path",{d:"M18 20V4"}]],tp=[["path",{d:"M21 14h-1.343"}],["path",{d:"M9.128 3.47A9 9 0 0 1 21 12v3.343"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20.414 20.414A2 2 0 0 1 19 21h-1a2 2 0 0 1-2-2v-3"}],["path",{d:"M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 2.636-6.364"}]],hp=[["path",{d:"M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3"}]],dp=[["path",{d:"M3 11h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-5Zm0 0a9 9 0 1 1 18 0m0 0v5a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3Z"}],["path",{d:"M21 16v2a4 4 0 0 1-4 4h-5"}]],cp=[["path",{d:"M12.409 5.824c-.702.792-1.15 1.496-1.415 2.166l2.153 2.156a.5.5 0 0 1 0 .707l-2.293 2.293a.5.5 0 0 0 0 .707L12 15"}],["path",{d:"M13.508 20.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.677.6.6 0 0 0 .818.001A5.5 5.5 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5z"}]],Mp=[["path",{d:"M19.414 14.414C21 12.828 22 11.5 22 9.5a5.5 5.5 0 0 0-9.591-3.676.6.6 0 0 1-.818.001A5.5 5.5 0 0 0 2 9.5c0 2.3 1.5 4 3 5.5l5.535 5.362a2 2 0 0 0 2.879.052 2.12 2.12 0 0 0-.004-3 2.124 2.124 0 1 0 3-3 2.124 2.124 0 0 0 3.004 0 2 2 0 0 0 0-2.828l-1.881-1.882a2.41 2.41 0 0 0-3.409 0l-1.71 1.71a2 2 0 0 1-2.828 0 2 2 0 0 1 0-2.828l2.823-2.762"}]],pp=[["path",{d:"m14.876 18.99-1.368 1.323a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5a5.2 5.2 0 0 1-.244 1.572"}],["path",{d:"M15 15h6"}]],ip=[["path",{d:"M10.5 4.893a5.5 5.5 0 0 1 1.091.931.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 1.872-1.002 3.356-2.187 4.655"}],["path",{d:"m16.967 16.967-3.459 3.346a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 2.747-4.761"}],["path",{d:"m2 2 20 20"}]],np=[["path",{d:"m14.479 19.374-.971.939a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5a5.2 5.2 0 0 1-.219 1.49"}],["path",{d:"M15 15h6"}],["path",{d:"M18 12v6"}]],lp=[["path",{d:"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"}],["path",{d:"M3.22 13H9.5l.5-1 2 4.5 2-7 1.5 3.5h5.27"}]],ep=[["path",{d:"M11 8c2-3-2-3 0-6"}],["path",{d:"M15.5 8c2-3-2-3 0-6"}],["path",{d:"M6 10h.01"}],["path",{d:"M6 14h.01"}],["path",{d:"M10 16v-4"}],["path",{d:"M14 16v-4"}],["path",{d:"M18 16v-4"}],["path",{d:"M20 6a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3"}],["path",{d:"M5 20v2"}],["path",{d:"M19 20v2"}]],rp=[["path",{d:"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"}]],op=[["path",{d:"M11 17v4"}],["path",{d:"M14 3v8a2 2 0 0 0 2 2h5.865"}],["path",{d:"M17 17v4"}],["path",{d:"M18 17a4 4 0 0 0 4-4 8 6 0 0 0-8-6 6 5 0 0 0-6 5v3a2 2 0 0 0 2 2z"}],["path",{d:"M2 10v5"}],["path",{d:"M6 3h16"}],["path",{d:"M7 21h14"}],["path",{d:"M8 13H2"}]],vp=[["path",{d:"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"}]],$p=[["path",{d:"m9 11-6 6v3h9l3-3"}],["path",{d:"m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"}]],mp=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M12 7v5l4 2"}]],yp=[["path",{d:"M10.82 16.12c1.69.6 3.91.79 5.18.85.55.03 1-.42.97-.97-.06-1.27-.26-3.5-.85-5.18"}],["path",{d:"M11.5 6.5c1.64 0 5-.38 6.71-1.07.52-.2.55-.82.12-1.17A10 10 0 0 0 4.26 18.33c.35.43.96.4 1.17-.12.69-1.71 1.07-5.07 1.07-6.71 1.34.45 3.1.9 4.88.62a.88.88 0 0 0 .73-.74c.3-2.14-.15-3.5-.61-4.88"}],["path",{d:"M15.62 16.95c.2.85.62 2.76.5 4.28a.77.77 0 0 1-.9.7 16.64 16.64 0 0 1-4.08-1.36"}],["path",{d:"M16.13 21.05c1.65.63 3.68.84 4.87.91a.9.9 0 0 0 .96-.96 17.68 17.68 0 0 0-.9-4.87"}],["path",{d:"M16.94 15.62c.86.2 2.77.62 4.29.5a.77.77 0 0 0 .7-.9 16.64 16.64 0 0 0-1.36-4.08"}],["path",{d:"M17.99 5.52a20.82 20.82 0 0 1 3.15 4.5.8.8 0 0 1-.68 1.13c-2.33.2-5.3-.32-8.27-1.57"}],["path",{d:"M4.93 4.93 3 3a.7.7 0 0 1 0-1"}],["path",{d:"M9.58 12.18c1.24 2.98 1.77 5.95 1.57 8.28a.8.8 0 0 1-1.13.68 20.82 20.82 0 0 1-4.5-3.15"}]],sp=[["path",{d:"M10.82 16.12c1.69.6 3.91.79 5.18.85.28.01.53-.09.7-.27"}],["path",{d:"M11.14 20.57c.52.24 2.44 1.12 4.08 1.37.46.06.86-.25.9-.71.12-1.52-.3-3.43-.5-4.28"}],["path",{d:"M16.13 21.05c1.65.63 3.68.84 4.87.91a.9.9 0 0 0 .7-.26"}],["path",{d:"M17.99 5.52a20.83 20.83 0 0 1 3.15 4.5.8.8 0 0 1-.68 1.13c-1.17.1-2.5.02-3.9-.25"}],["path",{d:"M20.57 11.14c.24.52 1.12 2.44 1.37 4.08.04.3-.08.59-.31.75"}],["path",{d:"M4.93 4.93a10 10 0 0 0-.67 13.4c.35.43.96.4 1.17-.12.69-1.71 1.07-5.07 1.07-6.71 1.34.45 3.1.9 4.88.62a.85.85 0 0 0 .48-.24"}],["path",{d:"M5.52 17.99c1.05.95 2.91 2.42 4.5 3.15a.8.8 0 0 0 1.13-.68c.2-2.34-.33-5.3-1.57-8.28"}],["path",{d:"M8.35 2.68a10 10 0 0 1 9.98 1.58c.43.35.4.96-.12 1.17-1.5.6-4.3.98-6.07 1.05"}],["path",{d:"m2 2 20 20"}]],gp=[["path",{d:"M12 7v4"}],["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M14 9h-4"}],["path",{d:"M18 11h2a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h2"}],["path",{d:"M18 21V5a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16"}]],Cp=[["path",{d:"M10 22v-6.57"}],["path",{d:"M12 11h.01"}],["path",{d:"M12 7h.01"}],["path",{d:"M14 15.43V22"}],["path",{d:"M15 16a5 5 0 0 0-6 0"}],["path",{d:"M16 11h.01"}],["path",{d:"M16 7h.01"}],["path",{d:"M8 11h.01"}],["path",{d:"M8 7h.01"}],["rect",{x:"4",y:"2",width:"16",height:"20",rx:"2"}]],up=[["path",{d:"M5 22h14"}],["path",{d:"M5 2h14"}],["path",{d:"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"}],["path",{d:"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"}]],Hp=[["path",{d:"M8.62 13.8A2.25 2.25 0 1 1 12 10.836a2.25 2.25 0 1 1 3.38 2.966l-2.626 2.856a.998.998 0 0 1-1.507 0z"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}]],Ap=[["path",{d:"M10 12V8.964"}],["path",{d:"M14 12V8.964"}],["path",{d:"M15 12a1 1 0 0 1 1 1v2a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2a1 1 0 0 1 1-1z"}],["path",{d:"M8.5 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-2"}]],wp=[["path",{d:"M12.35 21H5a2 2 0 0 1-2-2v-9a2 2 0 0 1 .71-1.53l7-6a2 2 0 0 1 2.58 0l7 6A2 2 0 0 1 21 10v2.35"}],["path",{d:"M14.8 12.4A1 1 0 0 0 14 12h-4a1 1 0 0 0-1 1v8"}],["path",{d:"M15 18h6"}],["path",{d:"M18 15v6"}]],Vp=[["path",{d:"M9.5 13.866a4 4 0 0 1 5 .01"}],["path",{d:"M12 17h.01"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}],["path",{d:"M7 10.754a8 8 0 0 1 10 0"}]],C2=[["path",{d:"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"}],["path",{d:"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"}]],u2=[["path",{d:"M12 17c5 0 8-2.69 8-6H4c0 3.31 3 6 8 6m-4 4h8m-4-3v3M5.14 11a3.5 3.5 0 1 1 6.71 0"}],["path",{d:"M12.14 11a3.5 3.5 0 1 1 6.71 0"}],["path",{d:"M15.5 6.5a3.5 3.5 0 1 0-7 0"}]],H2=[["path",{d:"m7 11 4.08 10.35a1 1 0 0 0 1.84 0L17 11"}],["path",{d:"M17 7A5 5 0 0 0 7 7"}],["path",{d:"M17 7a2 2 0 0 1 0 4H7a2 2 0 0 1 0-4"}]],Sp=[["path",{d:"M13.5 8h-3"}],["path",{d:"m15 2-1 2h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h3"}],["path",{d:"M16.899 22A5 5 0 0 0 7.1 22"}],["path",{d:"m9 2 3 6"}],["circle",{cx:"12",cy:"15",r:"3"}]],Lp=[["path",{d:"M16 10h2"}],["path",{d:"M16 14h2"}],["path",{d:"M6.17 15a3 3 0 0 1 5.66 0"}],["circle",{cx:"9",cy:"11",r:"2"}],["rect",{x:"2",y:"5",width:"20",height:"14",rx:"2"}]],fp=[["path",{d:"M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"}],["path",{d:"m14 19 3 3v-5.5"}],["path",{d:"m17 22 3-3"}],["circle",{cx:"9",cy:"9",r:"2"}]],kp=[["path",{d:"M21 9v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"}],["line",{x1:"16",x2:"22",y1:"5",y2:"5"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],Pp=[["line",{x1:"2",x2:"22",y1:"2",y2:"22"}],["path",{d:"M10.41 10.41a2 2 0 1 1-2.83-2.83"}],["line",{x1:"13.5",x2:"6",y1:"13.5",y2:"21"}],["line",{x1:"18",x2:"21",y1:"12",y2:"15"}],["path",{d:"M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59"}],["path",{d:"M21 15V5a2 2 0 0 0-2-2H9"}]],Bp=[["path",{d:"M16 5h6"}],["path",{d:"M19 2v6"}],["path",{d:"M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}],["circle",{cx:"9",cy:"9",r:"2"}]],zp=[["path",{d:"M15 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}],["path",{d:"M21 12.17V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"m6 21 5-5"}],["circle",{cx:"9",cy:"9",r:"2"}]],Fp=[["path",{d:"M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21"}],["path",{d:"m14 19.5 3-3 3 3"}],["path",{d:"M17 22v-5.5"}],["circle",{cx:"9",cy:"9",r:"2"}]],Dp=[["path",{d:"M16 3h5v5"}],["path",{d:"M17 21h2a2 2 0 0 0 2-2"}],["path",{d:"M21 12v3"}],["path",{d:"m21 3-5 5"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2"}],["path",{d:"m5 21 4.144-4.144a1.21 1.21 0 0 1 1.712 0L13 19"}],["path",{d:"M9 3h3"}],["rect",{x:"3",y:"11",width:"10",height:"10",rx:"1"}]],bp=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["circle",{cx:"9",cy:"9",r:"2"}],["path",{d:"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"}]],Rp=[["path",{d:"m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16"}],["path",{d:"M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2"}],["circle",{cx:"13",cy:"7",r:"1",fill:"currentColor"}],["rect",{x:"8",y:"2",width:"14",height:"14",rx:"2"}]],Tp=[["path",{d:"M12 3v12"}],["path",{d:"m8 11 4 4 4-4"}],["path",{d:"M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4"}]],qp=[["polyline",{points:"22 12 16 12 14 15 10 15 8 12 2 12"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"}]],Up=[["path",{d:"M6 3h12"}],["path",{d:"M6 8h12"}],["path",{d:"m6 13 8.5 8"}],["path",{d:"M6 13h3"}],["path",{d:"M9 13c6.667 0 6.667-10 0-10"}]],Op=[["path",{d:"M6 16c5 0 7-8 12-8a4 4 0 0 1 0 8c-5 0-7-8-12-8a4 4 0 1 0 0 8"}]],Zp=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 16v-4"}],["path",{d:"M12 8h.01"}]],Gp=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7h.01"}],["path",{d:"M17 7h.01"}],["path",{d:"M7 17h.01"}],["path",{d:"M17 17h.01"}]],Wp=[["rect",{width:"20",height:"20",x:"2",y:"2",rx:"5",ry:"5"}],["path",{d:"M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"}],["line",{x1:"17.5",x2:"17.51",y1:"6.5",y2:"6.5"}]],Ip=[["line",{x1:"19",x2:"10",y1:"4",y2:"4"}],["line",{x1:"14",x2:"5",y1:"20",y2:"20"}],["line",{x1:"15",x2:"9",y1:"4",y2:"20"}]],Ep=[["path",{d:"m16 14 4 4-4 4"}],["path",{d:"M20 10a8 8 0 1 0-8 8h8"}]],Xp=[["path",{d:"M4 10a8 8 0 1 1 8 8H4"}],["path",{d:"m8 22-4-4 4-4"}]],jp=[["path",{d:"M12 9.5V21m0-11.5L6 3m6 6.5L18 3"}],["path",{d:"M6 15h12"}],["path",{d:"M6 11h12"}]],Np=[["path",{d:"M21 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-2Z"}],["path",{d:"M6 15v-2"}],["path",{d:"M12 15V9"}],["circle",{cx:"12",cy:"6",r:"3"}]],Kp=[["path",{d:"M5 3v14"}],["path",{d:"M12 3v8"}],["path",{d:"M19 3v18"}]],Qp=[["path",{d:"M18 17a1 1 0 0 0-1 1v1a2 2 0 1 0 2-2z"}],["path",{d:"M20.97 3.61a.45.45 0 0 0-.58-.58C10.2 6.6 6.6 10.2 3.03 20.39a.45.45 0 0 0 .58.58C13.8 17.4 17.4 13.8 20.97 3.61"}],["path",{d:"m6.707 6.707 10.586 10.586"}],["path",{d:"M7 5a2 2 0 1 0-2 2h1a1 1 0 0 0 1-1z"}]],Jp=[["path",{d:"M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"}],["circle",{cx:"16.5",cy:"7.5",r:".5",fill:"currentColor"}]],Yp=[["path",{d:"M12.4 2.7a2.5 2.5 0 0 1 3.4 0l5.5 5.5a2.5 2.5 0 0 1 0 3.4l-3.7 3.7a2.5 2.5 0 0 1-3.4 0L8.7 9.8a2.5 2.5 0 0 1 0-3.4z"}],["path",{d:"m14 7 3 3"}],["path",{d:"m9.4 10.6-6.814 6.814A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814"}]],_p=[["path",{d:"m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"}],["path",{d:"m21 2-9.6 9.6"}],["circle",{cx:"7.5",cy:"15.5",r:"5.5"}]],xp=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M6 8h4"}],["path",{d:"M14 8h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"M2 12h20"}],["path",{d:"M6 12v4"}],["path",{d:"M10 12v4"}],["path",{d:"M14 12v4"}],["path",{d:"M18 12v4"}]],ai=[["path",{d:"M 20 4 A2 2 0 0 1 22 6"}],["path",{d:"M 22 6 L 22 16.41"}],["path",{d:"M 7 16 L 16 16"}],["path",{d:"M 9.69 4 L 20 4"}],["path",{d:"M14 8h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"}],["path",{d:"M6 8h.01"}],["path",{d:"M8 12h.01"}]],ti=[["path",{d:"M10 8h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M14 8h.01"}],["path",{d:"M16 12h.01"}],["path",{d:"M18 8h.01"}],["path",{d:"M6 8h.01"}],["path",{d:"M7 16h10"}],["path",{d:"M8 12h.01"}],["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}]],hi=[["path",{d:"M12 2v5"}],["path",{d:"M14.829 15.998a3 3 0 1 1-5.658 0"}],["path",{d:"M20.92 14.606A1 1 0 0 1 20 16H4a1 1 0 0 1-.92-1.394l3-7A1 1 0 0 1 7 7h10a1 1 0 0 1 .92.606z"}]],di=[["path",{d:"M10.293 2.293a1 1 0 0 1 1.414 0l2.5 2.5 5.994 1.227a1 1 0 0 1 .506 1.687l-7 7a1 1 0 0 1-1.687-.506l-1.227-5.994-2.5-2.5a1 1 0 0 1 0-1.414z"}],["path",{d:"m14.207 4.793-3.414 3.414"}],["path",{d:"M3 20a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"}],["path",{d:"m9.086 6.5-4.793 4.793a1 1 0 0 0-.18 1.17L7 18"}]],ci=[["path",{d:"M12 10v12"}],["path",{d:"M17.929 7.629A1 1 0 0 1 17 9H7a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 9 2h6a1 1 0 0 1 .928.629z"}],["path",{d:"M9 22h6"}]],Mi=[["path",{d:"M19.929 18.629A1 1 0 0 1 19 20H9a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 11 13h6a1 1 0 0 1 .928.629z"}],["path",{d:"M6 3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"}],["path",{d:"M8 6h4a2 2 0 0 1 2 2v5"}]],pi=[["path",{d:"M19.929 9.629A1 1 0 0 1 19 11H9a1 1 0 0 1-.928-1.371l2-5A1 1 0 0 1 11 4h6a1 1 0 0 1 .928.629z"}],["path",{d:"M6 15a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1z"}],["path",{d:"M8 18h4a2 2 0 0 0 2-2v-5"}]],ii=[["path",{d:"M12 12v6"}],["path",{d:"M4.077 10.615A1 1 0 0 0 5 12h14a1 1 0 0 0 .923-1.385l-3.077-7.384A2 2 0 0 0 15 2H9a2 2 0 0 0-1.846 1.23Z"}],["path",{d:"M8 20a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1z"}]],ni=[["path",{d:"m12 8 6-3-6-3v10"}],["path",{d:"m8 11.99-5.5 3.14a1 1 0 0 0 0 1.74l8.5 4.86a2 2 0 0 0 2 0l8.5-4.86a1 1 0 0 0 0-1.74L16 12"}],["path",{d:"m6.49 12.85 11.02 6.3"}],["path",{d:"M17.51 12.85 6.5 19.15"}]],li=[["path",{d:"M10 18v-7"}],["path",{d:"M11.12 2.198a2 2 0 0 1 1.76.006l7.866 3.847c.476.233.31.949-.22.949H3.474c-.53 0-.695-.716-.22-.949z"}],["path",{d:"M14 18v-7"}],["path",{d:"M18 18v-7"}],["path",{d:"M3 22h18"}],["path",{d:"M6 18v-7"}]],ei=[["path",{d:"m5 8 6 6"}],["path",{d:"m4 14 6-6 2-3"}],["path",{d:"M2 5h12"}],["path",{d:"M7 2h1"}],["path",{d:"m22 22-5-10-5 10"}],["path",{d:"M14 18h6"}]],ri=[["path",{d:"M2 20h20"}],["path",{d:"m9 10 2 2 4-4"}],["rect",{x:"3",y:"4",width:"18",height:"12",rx:"2"}]],A2=[["rect",{width:"18",height:"12",x:"3",y:"4",rx:"2",ry:"2"}],["line",{x1:"2",x2:"22",y1:"20",y2:"20"}]],oi=[["path",{d:"M18 5a2 2 0 0 1 2 2v8.526a2 2 0 0 0 .212.897l1.068 2.127a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45l1.068-2.127A2 2 0 0 0 4 15.526V7a2 2 0 0 1 2-2z"}],["path",{d:"M20.054 15.987H3.946"}]],vi=[["path",{d:"M7 22a5 5 0 0 1-2-4"}],["path",{d:"M7 16.93c.96.43 1.96.74 2.99.91"}],["path",{d:"M3.34 14A6.8 6.8 0 0 1 2 10c0-4.42 4.48-8 10-8s10 3.58 10 8a7.19 7.19 0 0 1-.33 2"}],["path",{d:"M5 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"}],["path",{d:"M14.33 22h-.09a.35.35 0 0 1-.24-.32v-10a.34.34 0 0 1 .33-.34c.08 0 .15.03.21.08l7.34 6a.33.33 0 0 1-.21.59h-4.49l-2.57 3.85a.35.35 0 0 1-.28.14z"}]],$i=[["path",{d:"M3.704 14.467A10 8 0 0 1 2 10a10 8 0 0 1 20 0 10 8 0 0 1-10 8 10 8 0 0 1-5.181-1.158"}],["path",{d:"M7 22a5 5 0 0 1-2-3.994"}],["circle",{cx:"5",cy:"16",r:"2"}]],mi=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M18 13a6 6 0 0 1-6 5 6 6 0 0 1-6-5h12Z"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],yi=[["path",{d:"M13 13.74a2 2 0 0 1-2 0L2.5 8.87a1 1 0 0 1 0-1.74L11 2.26a2 2 0 0 1 2 0l8.5 4.87a1 1 0 0 1 0 1.74z"}],["path",{d:"m20 14.285 1.5.845a1 1 0 0 1 0 1.74L13 21.74a2 2 0 0 1-2 0l-8.5-4.87a1 1 0 0 1 0-1.74l1.5-.845"}]],w2=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17"}]],si=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 .83.18 2 2 0 0 0 .83-.18l8.58-3.9a1 1 0 0 0 0-1.831z"}],["path",{d:"M16 17h6"}],["path",{d:"M19 14v6"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 .825.178"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l2.116-.962"}]],gi=[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1"}]],Ci=[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}]],ui=[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}],["path",{d:"M14 4h7"}],["path",{d:"M14 9h7"}],["path",{d:"M14 15h7"}],["path",{d:"M14 20h7"}]],Hi=[["rect",{width:"7",height:"18",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}]],Ai=[["rect",{width:"18",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1"}]],wi=[["rect",{width:"18",height:"7",x:"3",y:"3",rx:"1"}],["rect",{width:"9",height:"7",x:"3",y:"14",rx:"1"}],["rect",{width:"5",height:"7",x:"16",y:"14",rx:"1"}]],Vi=[["path",{d:"M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z"}],["path",{d:"M2 21c0-3 1.85-5.36 5.08-6C9.5 14.52 12 13 13 12"}]],Si=[["path",{d:"M2 22c1.25-.987 2.27-1.975 3.9-2.2a5.56 5.56 0 0 1 3.8 1.5 4 4 0 0 0 6.187-2.353 3.5 3.5 0 0 0 3.69-5.116A3.5 3.5 0 0 0 20.95 8 3.5 3.5 0 1 0 16 3.05a3.5 3.5 0 0 0-5.831 1.373 3.5 3.5 0 0 0-5.116 3.69 4 4 0 0 0-2.348 6.155C3.499 15.42 4.409 16.712 4.2 18.1 3.926 19.743 3.014 20.732 2 22"}],["path",{d:"M2 22 17 7"}]],Li=[["path",{d:"M16 12h3a2 2 0 0 0 1.902-1.38l1.056-3.333A1 1 0 0 0 21 6H3a1 1 0 0 0-.958 1.287l1.056 3.334A2 2 0 0 0 5 12h3"}],["path",{d:"M18 6V3a1 1 0 0 0-1-1h-3"}],["rect",{width:"8",height:"12",x:"8",y:"10",rx:"1"}]],fi=[["rect",{width:"8",height:"18",x:"3",y:"3",rx:"1"}],["path",{d:"M7 3v18"}],["path",{d:"M20.4 18.9c.2.5-.1 1.1-.6 1.3l-1.9.7c-.5.2-1.1-.1-1.3-.6L11.1 5.1c-.2-.5.1-1.1.6-1.3l1.9-.7c.5-.2 1.1.1 1.3.6Z"}]],ki=[["path",{d:"m16 6 4 14"}],["path",{d:"M12 6v14"}],["path",{d:"M8 8v12"}],["path",{d:"M4 4v16"}]],Pi=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m4.93 4.93 4.24 4.24"}],["path",{d:"m14.83 9.17 4.24-4.24"}],["path",{d:"m14.83 14.83 4.24 4.24"}],["path",{d:"m9.17 14.83-4.24 4.24"}],["circle",{cx:"12",cy:"12",r:"4"}]],Bi=[["path",{d:"M14 12h2v8"}],["path",{d:"M14 20h4"}],["path",{d:"M6 12h4"}],["path",{d:"M6 20h4"}],["path",{d:"M8 20V8a4 4 0 0 1 7.464-2"}]],zi=[["path",{d:"M16.8 11.2c.8-.9 1.2-2 1.2-3.2a6 6 0 0 0-9.3-5"}],["path",{d:"m2 2 20 20"}],["path",{d:"M6.3 6.3a4.67 4.67 0 0 0 1.2 5.2c.7.7 1.3 1.5 1.5 2.5"}],["path",{d:"M9 18h6"}],["path",{d:"M10 22h4"}]],Fi=[["path",{d:"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"}],["path",{d:"M9 18h6"}],["path",{d:"M10 22h4"}]],Di=[["path",{d:"M7 3.5c5-2 7 2.5 3 4C1.5 10 2 15 5 16c5 2 9-10 14-7s.5 13.5-4 12c-5-2.5.5-11 6-2"}]],bi=[["path",{d:"M9 17H7A5 5 0 0 1 7 7h2"}],["path",{d:"M15 7h2a5 5 0 1 1 0 10h-2"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}]],Ri=[["path",{d:"M9 17H7A5 5 0 0 1 7 7"}],["path",{d:"M15 7h2a5 5 0 0 1 4 8"}],["line",{x1:"8",x2:"12",y1:"12",y2:"12"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Ti=[["path",{d:"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"}],["path",{d:"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"}]],qi=[["path",{d:"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"}],["rect",{width:"4",height:"12",x:"2",y:"9"}],["circle",{cx:"4",cy:"4",r:"2"}]],Ui=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M11 19H3"}],["path",{d:"m15 18 2 2 4-4"}]],Oi=[["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"m3 17 2 2 4-4"}],["path",{d:"m3 7 2 2 4-4"}]],Zi=[["path",{d:"M3 5h8"}],["path",{d:"M3 12h8"}],["path",{d:"M3 19h8"}],["path",{d:"m15 5 3 3 3-3"}],["path",{d:"m15 19 3-3 3 3"}]],Gi=[["path",{d:"M3 5h8"}],["path",{d:"M3 12h8"}],["path",{d:"M3 19h8"}],["path",{d:"m15 8 3-3 3 3"}],["path",{d:"m15 16 3 3 3-3"}]],Wi=[["path",{d:"M10 5h11"}],["path",{d:"M10 12h11"}],["path",{d:"M10 19h11"}],["path",{d:"m3 10 3-3-3-3"}],["path",{d:"m3 20 3-3-3-3"}]],Ii=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M9 19H3"}],["path",{d:"m16 16-3 3 3 3"}],["path",{d:"M21 5v12a2 2 0 0 1-2 2h-6"}]],Ei=[["path",{d:"M12 5H2"}],["path",{d:"M6 12h12"}],["path",{d:"M9 19h6"}],["path",{d:"M16 5h6"}],["path",{d:"M19 8V2"}]],Xi=[["path",{d:"M2 5h20"}],["path",{d:"M6 12h12"}],["path",{d:"M9 19h6"}]],v=[["path",{d:"M21 5H11"}],["path",{d:"M21 12H11"}],["path",{d:"M21 19H11"}],["path",{d:"m7 8-4 4 4 4"}]],$=[["path",{d:"M21 5H11"}],["path",{d:"M21 12H11"}],["path",{d:"M21 19H11"}],["path",{d:"m3 8 4 4-4 4"}]],ji=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M21 12h-6"}]],Ni=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M11 19H3"}],["path",{d:"M21 16V5"}],["circle",{cx:"18",cy:"16",r:"3"}]],Ki=[["path",{d:"M11 5h10"}],["path",{d:"M11 12h10"}],["path",{d:"M11 19h10"}],["path",{d:"M4 4h1v5"}],["path",{d:"M4 9h2"}],["path",{d:"M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02"}]],Qi=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M18 9v6"}],["path",{d:"M21 12h-6"}]],Ji=[["path",{d:"M21 5H3"}],["path",{d:"M7 12H3"}],["path",{d:"M7 19H3"}],["path",{d:"M12 18a5 5 0 0 0 9-3 4.5 4.5 0 0 0-4.5-4.5c-1.33 0-2.54.54-3.41 1.41L11 14"}],["path",{d:"M11 10v4h4"}]],Yi=[["path",{d:"M3 5h6"}],["path",{d:"M3 12h13"}],["path",{d:"M3 19h13"}],["path",{d:"m16 8-3-3 3-3"}],["path",{d:"M21 19V7a2 2 0 0 0-2-2h-6"}]],_i=[["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"m3 17 2 2 4-4"}],["rect",{x:"3",y:"4",width:"6",height:"6",rx:"1"}]],xi=[["path",{d:"M8 5h13"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}],["path",{d:"M3 10a2 2 0 0 0 2 2h3"}],["path",{d:"M3 5v12a2 2 0 0 0 2 2h3"}]],an=[["path",{d:"M21 5H3"}],["path",{d:"M10 12H3"}],["path",{d:"M10 19H3"}],["path",{d:"M15 12.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"}]],tn=[["path",{d:"M16 5H3"}],["path",{d:"M11 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"m15.5 9.5 5 5"}],["path",{d:"m20.5 9.5-5 5"}]],V2=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56"}]],hn=[["path",{d:"M3 5h.01"}],["path",{d:"M3 12h.01"}],["path",{d:"M3 19h.01"}],["path",{d:"M8 5h13"}],["path",{d:"M8 12h13"}],["path",{d:"M8 19h13"}]],dn=[["path",{d:"M22 12a1 1 0 0 1-10 0 1 1 0 0 0-10 0"}],["path",{d:"M7 20.7a1 1 0 1 1 5-8.7 1 1 0 1 0 5-8.6"}],["path",{d:"M7 3.3a1 1 0 1 1 5 8.6 1 1 0 1 0 5 8.6"}],["circle",{cx:"12",cy:"12",r:"10"}]],cn=[["path",{d:"M12 2v4"}],["path",{d:"m16.2 7.8 2.9-2.9"}],["path",{d:"M18 12h4"}],["path",{d:"m16.2 16.2 2.9 2.9"}],["path",{d:"M12 18v4"}],["path",{d:"m4.9 19.1 2.9-2.9"}],["path",{d:"M2 12h4"}],["path",{d:"m4.9 4.9 2.9 2.9"}]],Mn=[["line",{x1:"2",x2:"5",y1:"12",y2:"12"}],["line",{x1:"19",x2:"22",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"2",y2:"5"}],["line",{x1:"12",x2:"12",y1:"19",y2:"22"}],["circle",{cx:"12",cy:"12",r:"7"}],["circle",{cx:"12",cy:"12",r:"3"}]],pn=[["path",{d:"M12 19v3"}],["path",{d:"M12 2v3"}],["path",{d:"M18.89 13.24a7 7 0 0 0-8.13-8.13"}],["path",{d:"M19 12h3"}],["path",{d:"M2 12h3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M7.05 7.05a7 7 0 0 0 9.9 9.9"}]],nn=[["line",{x1:"2",x2:"5",y1:"12",y2:"12"}],["line",{x1:"19",x2:"22",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"2",y2:"5"}],["line",{x1:"12",x2:"12",y1:"19",y2:"22"}],["circle",{cx:"12",cy:"12",r:"7"}]],S2=[["circle",{cx:"12",cy:"16",r:"1"}],["rect",{width:"18",height:"12",x:"3",y:"10",rx:"2"}],["path",{d:"M7 10V7a5 5 0 0 1 9.33-2.5"}]],ln=[["circle",{cx:"12",cy:"16",r:"1"}],["rect",{x:"3",y:"10",width:"18",height:"12",rx:"2"}],["path",{d:"M7 10V7a5 5 0 0 1 10 0v3"}]],L2=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}],["path",{d:"M7 11V7a5 5 0 0 1 9.9-1"}]],en=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4"}]],rn=[["path",{d:"m10 17 5-5-5-5"}],["path",{d:"M15 12H3"}],["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"}]],on=[["path",{d:"m16 17 5-5-5-5"}],["path",{d:"M21 12H9"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"}]],vn=[["path",{d:"M3 5h1"}],["path",{d:"M3 12h1"}],["path",{d:"M3 19h1"}],["path",{d:"M8 5h1"}],["path",{d:"M8 12h1"}],["path",{d:"M8 19h1"}],["path",{d:"M13 5h8"}],["path",{d:"M13 12h8"}],["path",{d:"M13 19h8"}]],$n=[["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M11 11a2 2 0 0 0 4 0 4 4 0 0 0-8 0 6 6 0 0 0 12 0"}]],mn=[["path",{d:"M6 20a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2"}],["path",{d:"M8 18V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v14"}],["path",{d:"M10 20h4"}],["circle",{cx:"16",cy:"20",r:"2"}],["circle",{cx:"8",cy:"20",r:"2"}]],yn=[["path",{d:"m12 15 4 4"}],["path",{d:"M2.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.029-6.029a1 1 0 1 1 3 3l-6.029 6.029a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l6.365-6.367A1 1 0 0 0 8.716 4.282z"}],["path",{d:"m5 8 4 4"}]],sn=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"m16 19 2 2 4-4"}]],gn=[["path",{d:"M22 15V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M16 19h6"}]],Cn=[["path",{d:"M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"}],["path",{d:"m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"}]],un=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M19 16v6"}],["path",{d:"M16 19h6"}]],f2=[["path",{d:"M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M18 15.28c.2-.4.5-.8.9-1a2.1 2.1 0 0 1 2.6.4c.3.4.5.8.5 1.3 0 1.3-2 2-2 2"}],["path",{d:"M20 22v.01"}]],Hn=[["path",{d:"M22 12.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h7.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"}],["circle",{cx:"18",cy:"18",r:"3"}],["path",{d:"m22 22-1.5-1.5"}]],An=[["path",{d:"M22 10.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h12.5"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"M20 14v4"}],["path",{d:"M20 22v.01"}]],wn=[["path",{d:"M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h9"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"}],["path",{d:"m17 17 4 4"}],["path",{d:"m21 17-4 4"}]],Vn=[["path",{d:"m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}]],Sn=[["path",{d:"M22 17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9.5C2 7 4 5 6.5 5H18c2.2 0 4 1.8 4 4v8Z"}],["polyline",{points:"15,9 18,9 18,11"}],["path",{d:"M6.5 5C9 5 11 7 11 9.5V17a2 2 0 0 1-2 2"}],["line",{x1:"6",x2:"7",y1:"10",y2:"10"}]],Ln=[["path",{d:"M17 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 1-1.732"}],["path",{d:"m22 5.5-6.419 4.179a2 2 0 0 1-2.162 0L7 5.5"}],["rect",{x:"7",y:"3",width:"15",height:"12",rx:"2"}]],fn=[["path",{d:"m11 19-1.106-.552a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0l4.212 2.106a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619V14"}],["path",{d:"M15 5.764V14"}],["path",{d:"M21 18h-6"}],["path",{d:"M9 3.236v15"}]],kn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"m9 10 2 2 4-4"}]],Pn=[["path",{d:"M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"m16 18 2 2 4-4"}]],Bn=[["path",{d:"M15 22a1 1 0 0 1-1-1v-4a1 1 0 0 1 .445-.832l3-2a1 1 0 0 1 1.11 0l3 2A1 1 0 0 1 22 17v4a1 1 0 0 1-1 1z"}],["path",{d:"M18 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 .601.2"}],["path",{d:"M18 22v-3"}],["circle",{cx:"10",cy:"10",r:"3"}]],zn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"M9 10h6"}]],Fn=[["path",{d:"M18.977 14C19.6 12.701 20 11.343 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M16 18h6"}]],Dn=[["path",{d:"M12.75 7.09a3 3 0 0 1 2.16 2.16"}],["path",{d:"M17.072 17.072c-1.634 2.17-3.527 3.912-4.471 4.727a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 1.432-4.568"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.475 2.818A8 8 0 0 1 20 10c0 1.183-.31 2.377-.81 3.533"}],["path",{d:"M9.13 9.13a3 3 0 0 0 3.74 3.74"}]],k2=[["path",{d:"M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"10",r:"3"}]],bn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"M12 7v6"}],["path",{d:"M9 10h6"}]],Rn=[["path",{d:"M19.914 11.105A7.298 7.298 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M16 18h6"}],["path",{d:"M19 15v6"}]],Tn=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["path",{d:"m14.5 7.5-5 5"}],["path",{d:"m9.5 7.5 5 5"}]],qn=[["path",{d:"M19.752 11.901A7.78 7.78 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 19 19 0 0 0 .09-.077"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"m21.5 15.5-5 5"}],["path",{d:"m21.5 20.5-5-5"}]],Un=[["path",{d:"M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"}],["circle",{cx:"12",cy:"10",r:"3"}]],On=[["path",{d:"M18 8c0 3.613-3.869 7.429-5.393 8.795a1 1 0 0 1-1.214 0C9.87 15.429 6 11.613 6 8a6 6 0 0 1 12 0"}],["circle",{cx:"12",cy:"8",r:"2"}],["path",{d:"M8.714 14h-3.71a1 1 0 0 0-.948.683l-2.004 6A1 1 0 0 0 3 22h18a1 1 0 0 0 .948-1.316l-2-6a1 1 0 0 0-.949-.684h-3.712"}]],Zn=[["path",{d:"m11 19-1.106-.552a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0l4.212 2.106a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619V12"}],["path",{d:"M15 5.764V12"}],["path",{d:"M18 15v6"}],["path",{d:"M21 18h-6"}],["path",{d:"M9 3.236v15"}]],Gn=[["path",{d:"M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"}],["path",{d:"M15 5.764v15"}],["path",{d:"M9 3.236v15"}]],Wn=[["path",{d:"m14 6 4 4"}],["path",{d:"M17 3h4v4"}],["path",{d:"m21 3-7.75 7.75"}],["circle",{cx:"9",cy:"15",r:"6"}]],In=[["path",{d:"M16 3h5v5"}],["path",{d:"m21 3-6.75 6.75"}],["circle",{cx:"10",cy:"14",r:"6"}]],En=[["path",{d:"M8 22h8"}],["path",{d:"M12 11v11"}],["path",{d:"m19 3-7 8-7-8Z"}]],Xn=[["path",{d:"M15 3h6v6"}],["path",{d:"m21 3-7 7"}],["path",{d:"m3 21 7-7"}],["path",{d:"M9 21H3v-6"}]],jn=[["path",{d:"M8 3H5a2 2 0 0 0-2 2v3"}],["path",{d:"M21 8V5a2 2 0 0 0-2-2h-3"}],["path",{d:"M3 16v3a2 2 0 0 0 2 2h3"}],["path",{d:"M16 21h3a2 2 0 0 0 2-2v-3"}]],Nn=[["path",{d:"M11.636 6A13 13 0 0 0 19.4 3.2 1 1 0 0 1 21 4v11.344"}],["path",{d:"M14.378 14.357A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h1"}],["path",{d:"m2 2 20 20"}],["path",{d:"M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"}],["path",{d:"M8 8v6"}]],Kn=[["path",{d:"M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"}],["path",{d:"M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"}],["path",{d:"M8 6v8"}]],Qn=[["path",{d:"M7.21 15 2.66 7.14a2 2 0 0 1 .13-2.2L4.4 2.8A2 2 0 0 1 6 2h12a2 2 0 0 1 1.6.8l1.6 2.14a2 2 0 0 1 .14 2.2L16.79 15"}],["path",{d:"M11 12 5.12 2.2"}],["path",{d:"m13 12 5.88-9.8"}],["path",{d:"M8 7h8"}],["circle",{cx:"12",cy:"17",r:"5"}],["path",{d:"M12 18v-2h-.5"}]],Jn=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"8",x2:"16",y1:"15",y2:"15"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],Yn=[["path",{d:"M12 12v-2"}],["path",{d:"M12 18v-2"}],["path",{d:"M16 12v-2"}],["path",{d:"M16 18v-2"}],["path",{d:"M2 11h1.5"}],["path",{d:"M20 18v-2"}],["path",{d:"M20.5 11H22"}],["path",{d:"M4 18v-2"}],["path",{d:"M8 12v-2"}],["path",{d:"M8 18v-2"}],["rect",{x:"2",y:"6",width:"20",height:"10",rx:"2"}]],_n=[["path",{d:"m8 6 4-4 4 4"}],["path",{d:"M12 2v10.3a4 4 0 0 1-1.172 2.872L4 22"}],["path",{d:"m20 22-5-5"}]],xn=[["path",{d:"M4 5h16"}],["path",{d:"M4 12h16"}],["path",{d:"M4 19h16"}]],al=[["path",{d:"m10 9-3 3 3 3"}],["path",{d:"m14 15 3-3-3-3"}],["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}]],tl=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0"}],["path",{d:"M17.609 3.72a10 10 0 0 1 2.69 2.7"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8"}],["path",{d:"M20.28 17.61a10 10 0 0 1-2.7 2.69"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69"}],["path",{d:"m6.163 21.117-2.906.85a1 1 0 0 1-1.236-1.169l.965-2.98"}]],hl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M7.828 13.07A3 3 0 0 1 12 8.764a3 3 0 0 1 5.004 2.224 3 3 0 0 1-.832 2.083l-3.447 3.62a1 1 0 0 1-1.45-.001z"}]],dl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M8 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}]],cl=[["path",{d:"m2 2 20 20"}],["path",{d:"M4.93 4.929a10 10 0 0 0-1.938 11.412 2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 0 0 11.302-1.989"}],["path",{d:"M8.35 2.69A10 10 0 0 1 21.3 15.65"}]],Ml=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],P2=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],pl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"m10 15-3-3 3-3"}],["path",{d:"M7 12h8a2 2 0 0 1 2 2v1"}]],il=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"M12 8v4"}],["path",{d:"M12 16h.01"}]],nl=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],ll=[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"}]],el=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m10 8-3 3 3 3"}],["path",{d:"m14 14 3-3-3-3"}]],rl=[["path",{d:"M12 19h.01"}],["path",{d:"M12 3h.01"}],["path",{d:"M16 19h.01"}],["path",{d:"M16 3h.01"}],["path",{d:"M2 13h.01"}],["path",{d:"M2 17v4.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H8"}],["path",{d:"M2 5a2 2 0 0 1 2-2"}],["path",{d:"M2 9h.01"}],["path",{d:"M20 3a2 2 0 0 1 2 2"}],["path",{d:"M22 13h.01"}],["path",{d:"M22 17a2 2 0 0 1-2 2"}],["path",{d:"M22 9h.01"}],["path",{d:"M8 3h.01"}]],ol=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M10 15h4"}],["path",{d:"M10 9h4"}],["path",{d:"M12 7v4"}]],vl=[["path",{d:"M12.7 3H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H20a2 2 0 0 0 2-2v-4.7"}],["circle",{cx:"19",cy:"6",r:"3"}]],$l=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M7.5 9.5c0 .687.265 1.383.697 1.844l3.009 3.264a1.14 1.14 0 0 0 .407.314 1 1 0 0 0 .783-.004 1.14 1.14 0 0 0 .398-.31l3.008-3.264A2.77 2.77 0 0 0 16.5 9.5 2.5 2.5 0 0 0 12 8a2.5 2.5 0 0 0-4.5 1.5"}]],ml=[["path",{d:"M22 8.5V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H10"}],["path",{d:"M20 15v-2a2 2 0 0 0-4 0v2"}],["rect",{x:"14",y:"15",width:"8",height:"5",rx:"1"}]],yl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 11h.01"}],["path",{d:"M16 11h.01"}],["path",{d:"M8 11h.01"}]],sl=[["path",{d:"M19 19H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.7.7 0 0 1 2 21.286V5a2 2 0 0 1 1.184-1.826"}],["path",{d:"m2 2 20 20"}],["path",{d:"M8.656 3H20a2 2 0 0 1 2 2v11.344"}]],gl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 8v6"}],["path",{d:"M9 11h6"}]],Cl=[["path",{d:"M14 14a2 2 0 0 0 2-2V8h-2"}],["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M8 14a2 2 0 0 0 2-2V8H8"}]],ul=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m10 8-3 3 3 3"}],["path",{d:"M17 14v-1a2 2 0 0 0-2-2H7"}]],Hl=[["path",{d:"M12 3H4a2 2 0 0 0-2 2v16.286a.71.71 0 0 0 1.212.502l2.202-2.202A2 2 0 0 1 6.828 19H20a2 2 0 0 0 2-2v-4"}],["path",{d:"M16 3h6v6"}],["path",{d:"m16 9 6-6"}]],Al=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M7 11h10"}],["path",{d:"M7 15h6"}],["path",{d:"M7 7h8"}]],wl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"M12 15h.01"}],["path",{d:"M12 7v4"}]],Vl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}],["path",{d:"m14.5 8.5-5 5"}],["path",{d:"m9.5 8.5 5 5"}]],Sl=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"}]],Ll=[["path",{d:"M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"}],["path",{d:"M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1"}]],fl=[["path",{d:"M12 19v3"}],["path",{d:"M15 9.34V5a3 3 0 0 0-5.68-1.33"}],["path",{d:"M16.95 16.95A7 7 0 0 1 5 12v-2"}],["path",{d:"M18.89 13.23A7 7 0 0 0 19 12v-2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M9 9v3a3 3 0 0 0 5.12 2.12"}]],B2=[["path",{d:"m11 7.601-5.994 8.19a1 1 0 0 0 .1 1.298l.817.818a1 1 0 0 0 1.314.087L15.09 12"}],["path",{d:"M16.5 21.174C15.5 20.5 14.372 20 13 20c-2.058 0-3.928 2.356-6 2-2.072-.356-2.775-3.369-1.5-4.5"}],["circle",{cx:"16",cy:"7",r:"5"}]],kl=[["path",{d:"M12 19v3"}],["path",{d:"M19 10v2a7 7 0 0 1-14 0v-2"}],["rect",{x:"9",y:"2",width:"6",height:"13",rx:"3"}]],Pl=[["path",{d:"M10 12h4"}],["path",{d:"M10 17h4"}],["path",{d:"M10 7h4"}],["path",{d:"M18 12h2"}],["path",{d:"M18 18h2"}],["path",{d:"M18 6h2"}],["path",{d:"M4 12h2"}],["path",{d:"M4 18h2"}],["path",{d:"M4 6h2"}],["rect",{x:"6",y:"2",width:"12",height:"20",rx:"2"}]],Bl=[["path",{d:"M6 18h8"}],["path",{d:"M3 22h18"}],["path",{d:"M14 22a7 7 0 1 0 0-14h-1"}],["path",{d:"M9 14h2"}],["path",{d:"M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z"}],["path",{d:"M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"}]],zl=[["rect",{width:"20",height:"15",x:"2",y:"4",rx:"2"}],["rect",{width:"8",height:"7",x:"6",y:"8",rx:"1"}],["path",{d:"M18 8v7"}],["path",{d:"M6 19v2"}],["path",{d:"M18 19v2"}]],Fl=[["path",{d:"M12 13v8"}],["path",{d:"M12 3v3"}],["path",{d:"M4 6a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h13a2 2 0 0 0 1.152-.365l3.424-2.317a1 1 0 0 0 0-1.635l-3.424-2.318A2 2 0 0 0 17 6z"}]],Dl=[["path",{d:"M8 2h8"}],["path",{d:"M9 2v1.343M15 2v2.789a4 4 0 0 0 .672 2.219l.656.984a4 4 0 0 1 .672 2.22v1.131M7.8 7.8l-.128.192A4 4 0 0 0 7 10.212V20a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-3"}],["path",{d:"M7 15a6.47 6.47 0 0 1 5 0 6.472 6.472 0 0 0 3.435.435"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],bl=[["path",{d:"M8 2h8"}],["path",{d:"M9 2v2.789a4 4 0 0 1-.672 2.219l-.656.984A4 4 0 0 0 7 10.212V20a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-9.789a4 4 0 0 0-.672-2.219l-.656-.984A4 4 0 0 1 15 4.788V2"}],["path",{d:"M7 15a6.472 6.472 0 0 1 5 0 6.47 6.47 0 0 0 5 0"}]],Rl=[["path",{d:"m14 10 7-7"}],["path",{d:"M20 10h-6V4"}],["path",{d:"m3 21 7-7"}],["path",{d:"M4 14h6v6"}]],Tl=[["path",{d:"M8 3v3a2 2 0 0 1-2 2H3"}],["path",{d:"M21 8h-3a2 2 0 0 1-2-2V3"}],["path",{d:"M3 16h3a2 2 0 0 1 2 2v3"}],["path",{d:"M16 21v-3a2 2 0 0 1 2-2h3"}]],ql=[["path",{d:"M5 12h14"}]],Ul=[["path",{d:"m9 10 2 2 4-4"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Ol=[["path",{d:"M11 13a3 3 0 1 1 2.83-4H14a2 2 0 0 1 0 4z"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],Zl=[["path",{d:"M12 17v4"}],["path",{d:"m14.305 7.53.923-.382"}],["path",{d:"m15.228 4.852-.923-.383"}],["path",{d:"m16.852 3.228-.383-.924"}],["path",{d:"m16.852 8.772-.383.923"}],["path",{d:"m19.148 3.228.383-.924"}],["path",{d:"m19.53 9.696-.382-.924"}],["path",{d:"m20.772 4.852.924-.383"}],["path",{d:"m20.772 7.148.924.383"}],["path",{d:"M22 13v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"}],["path",{d:"M8 21h8"}],["circle",{cx:"18",cy:"6",r:"3"}]],Gl=[["path",{d:"M12 17v4"}],["path",{d:"M22 12.307V15a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8.693"}],["path",{d:"M8 21h8"}],["circle",{cx:"19",cy:"6",r:"3"}]],Wl=[["path",{d:"M12 13V7"}],["path",{d:"m15 10-3 3-3-3"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Il=[["path",{d:"M17 17H4a2 2 0 0 1-2-2V5c0-1.5 1-2 1-2"}],["path",{d:"M22 15V5a2 2 0 0 0-2-2H9"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m2 2 20 20"}]],El=[["path",{d:"M10 13V7"}],["path",{d:"M14 13V7"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Xl=[["path",{d:"M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],jl=[["path",{d:"M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8"}],["path",{d:"M10 19v-3.96 3.15"}],["path",{d:"M7 19h5"}],["rect",{width:"6",height:"10",x:"16",y:"12",rx:"2"}]],Nl=[["path",{d:"M5.5 20H8"}],["path",{d:"M17 9h.01"}],["rect",{width:"10",height:"16",x:"12",y:"4",rx:"2"}],["path",{d:"M8 6H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h4"}],["circle",{cx:"17",cy:"15",r:"1"}]],Kl=[["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}],["rect",{x:"9",y:"7",width:"6",height:"6",rx:"1"}]],Ql=[["path",{d:"m9 10 3-3 3 3"}],["path",{d:"M12 13V7"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Jl=[["path",{d:"m14.5 12.5-5-5"}],["path",{d:"m9.5 12.5 5-5"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}]],Yl=[["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}],["line",{x1:"8",x2:"16",y1:"21",y2:"21"}],["line",{x1:"12",x2:"12",y1:"17",y2:"21"}]],_l=[["path",{d:"M18 5h4"}],["path",{d:"M20 3v4"}],["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"}]],xl=[["path",{d:"m18 14-1-3"}],["path",{d:"m3 9 6 2a2 2 0 0 1 2-2h2a2 2 0 0 1 1.99 1.81"}],["path",{d:"M8 17h3a1 1 0 0 0 1-1 6 6 0 0 1 6-6 1 1 0 0 0 1-1v-.75A5 5 0 0 0 17 5"}],["circle",{cx:"19",cy:"17",r:"3"}],["circle",{cx:"5",cy:"17",r:"3"}]],ae=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"}]],te=[["path",{d:"m8 3 4 8 5-5 5 15H2L8 3z"}],["path",{d:"M4.14 15.08c2.62-1.57 5.24-1.43 7.86.42 2.74 1.94 5.49 2 8.23.19"}]],he=[["path",{d:"m8 3 4 8 5-5 5 15H2L8 3z"}]],de=[["path",{d:"M12 6v.343"}],["path",{d:"M18.218 18.218A7 7 0 0 1 5 15V9a7 7 0 0 1 .782-3.218"}],["path",{d:"M19 13.343V9A7 7 0 0 0 8.56 2.902"}],["path",{d:"M22 22 2 2"}]],ce=[["path",{d:"m15.55 8.45 5.138 2.087a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063L8.45 15.551"}],["path",{d:"M22 2 2 22"}],["path",{d:"m6.816 11.528-2.779-6.84a.495.495 0 0 1 .651-.651l6.84 2.779"}]],Me=[["path",{d:"M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z"}]],pe=[["path",{d:"M2.034 2.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.944L8.204 7.545a1 1 0 0 0-.66.66l-1.066 3.443a.5.5 0 0 1-.944.033z"}],["circle",{cx:"16",cy:"16",r:"6"}],["path",{d:"m11.8 11.8 8.4 8.4"}]],ie=[["path",{d:"M14 4.1 12 6"}],["path",{d:"m5.1 8-2.9-.8"}],["path",{d:"m6 12-1.9 2"}],["path",{d:"M7.2 2.2 8 5.1"}],["path",{d:"M9.037 9.69a.498.498 0 0 1 .653-.653l11 4.5a.5.5 0 0 1-.074.949l-4.349 1.041a1 1 0 0 0-.74.739l-1.04 4.35a.5.5 0 0 1-.95.074z"}]],ne=[["path",{d:"M12.586 12.586 19 19"}],["path",{d:"M3.688 3.037a.497.497 0 0 0-.651.651l6.5 15.999a.501.501 0 0 0 .947-.062l1.569-6.083a2 2 0 0 1 1.448-1.479l6.124-1.579a.5.5 0 0 0 .063-.947z"}]],le=[["rect",{x:"5",y:"2",width:"14",height:"20",rx:"7"}],["path",{d:"M12 6v4"}]],z2=[["path",{d:"M5 3v16h16"}],["path",{d:"m5 19 6-6"}],["path",{d:"m2 6 3-3 3 3"}],["path",{d:"m18 16 3 3-3 3"}]],ee=[["path",{d:"M19 13v6h-6"}],["path",{d:"M5 11V5h6"}],["path",{d:"m5 5 14 14"}]],re=[["path",{d:"M11 19H5v-6"}],["path",{d:"M13 5h6v6"}],["path",{d:"M19 5 5 19"}]],oe=[["path",{d:"M11 19H5V13"}],["path",{d:"M19 5L5 19"}]],ve=[["path",{d:"M19 13V19H13"}],["path",{d:"M5 5L19 19"}]],$e=[["path",{d:"M8 18L12 22L16 18"}],["path",{d:"M12 2V22"}]],me=[["path",{d:"M6 8L2 12L6 16"}],["path",{d:"M2 12H22"}]],ye=[["path",{d:"m18 8 4 4-4 4"}],["path",{d:"M2 12h20"}],["path",{d:"m6 8-4 4 4 4"}]],se=[["path",{d:"M18 8L22 12L18 16"}],["path",{d:"M2 12H22"}]],ge=[["path",{d:"M5 11V5H11"}],["path",{d:"M5 5L19 19"}]],Ce=[["path",{d:"M13 5H19V11"}],["path",{d:"M19 5L5 19"}]],ue=[["path",{d:"M8 6L12 2L16 6"}],["path",{d:"M12 2V22"}]],He=[["path",{d:"M12 2v20"}],["path",{d:"m8 18 4 4 4-4"}],["path",{d:"m8 6 4-4 4 4"}]],Ae=[["path",{d:"M12 2v20"}],["path",{d:"m15 19-3 3-3-3"}],["path",{d:"m19 9 3 3-3 3"}],["path",{d:"M2 12h20"}],["path",{d:"m5 9-3 3 3 3"}],["path",{d:"m9 5 3-3 3 3"}]],we=[["circle",{cx:"8",cy:"18",r:"4"}],["path",{d:"M12 18V2l7 4"}]],Ve=[["circle",{cx:"12",cy:"18",r:"4"}],["path",{d:"M16 18V2"}]],Se=[["path",{d:"M9 18V5l12-2v13"}],["path",{d:"m9 9 12-2"}],["circle",{cx:"6",cy:"18",r:"3"}],["circle",{cx:"18",cy:"16",r:"3"}]],Le=[["path",{d:"M9 18V5l12-2v13"}],["circle",{cx:"6",cy:"18",r:"3"}],["circle",{cx:"18",cy:"16",r:"3"}]],fe=[["path",{d:"M9.31 9.31 5 21l7-4 7 4-1.17-3.17"}],["path",{d:"M14.53 8.88 12 2l-1.17 3.17"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],ke=[["polygon",{points:"12 2 19 21 12 17 5 21 12 2"}]],Pe=[["path",{d:"M8.43 8.43 3 11l8 2 2 8 2.57-5.43"}],["path",{d:"M17.39 11.73 22 2l-9.73 4.61"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],Be=[["polygon",{points:"3 11 22 2 13 21 11 13 3 11"}]],ze=[["path",{d:"M15 18h-5"}],["path",{d:"M18 14h-8"}],["path",{d:"M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"}],["rect",{width:"8",height:"4",x:"10",y:"6",rx:"1"}]],Fe=[["rect",{x:"16",y:"16",width:"6",height:"6",rx:"1"}],["rect",{x:"2",y:"16",width:"6",height:"6",rx:"1"}],["rect",{x:"9",y:"2",width:"6",height:"6",rx:"1"}],["path",{d:"M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"}],["path",{d:"M12 12V8"}]],De=[["path",{d:"M6 8.32a7.43 7.43 0 0 1 0 7.36"}],["path",{d:"M9.46 6.21a11.76 11.76 0 0 1 0 11.58"}],["path",{d:"M12.91 4.1a15.91 15.91 0 0 1 .01 15.8"}],["path",{d:"M16.37 2a20.16 20.16 0 0 1 0 20"}]],be=[["path",{d:"M12 2v10"}],["path",{d:"m8.5 4 7 4"}],["path",{d:"m8.5 8 7-4"}],["circle",{cx:"12",cy:"17",r:"5"}]],Re=[["path",{d:"M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"}],["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["path",{d:"M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}]],Te=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M15 2v20"}],["path",{d:"M15 7h5"}],["path",{d:"M15 12h5"}],["path",{d:"M15 17h5"}]],qe=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M9.5 8h5"}],["path",{d:"M9.5 12H16"}],["path",{d:"M9.5 16H14"}]],Ue=[["path",{d:"M2 6h4"}],["path",{d:"M2 10h4"}],["path",{d:"M2 14h4"}],["path",{d:"M2 18h4"}],["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M16 2v20"}]],Oe=[["path",{d:"M8 2v4"}],["path",{d:"M12 2v4"}],["path",{d:"M16 2v4"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v2"}],["path",{d:"M20 12v2"}],["path",{d:"M20 18v2a2 2 0 0 1-2 2h-1"}],["path",{d:"M13 22h-2"}],["path",{d:"M7 22H6a2 2 0 0 1-2-2v-2"}],["path",{d:"M4 14v-2"}],["path",{d:"M4 8V6a2 2 0 0 1 2-2h2"}],["path",{d:"M8 10h6"}],["path",{d:"M8 14h8"}],["path",{d:"M8 18h5"}]],Ze=[["path",{d:"M8 2v4"}],["path",{d:"M12 2v4"}],["path",{d:"M16 2v4"}],["rect",{width:"16",height:"18",x:"4",y:"4",rx:"2"}],["path",{d:"M8 10h6"}],["path",{d:"M8 14h8"}],["path",{d:"M8 18h5"}]],Ge=[["path",{d:"M12 4V2"}],["path",{d:"M5 10v4a7.004 7.004 0 0 0 5.277 6.787c.412.104.802.292 1.102.592L12 22l.621-.621c.3-.3.69-.488 1.102-.592a7.01 7.01 0 0 0 4.125-2.939"}],["path",{d:"M19 10v3.343"}],["path",{d:"M12 12c-1.349-.573-1.905-1.005-2.5-2-.546.902-1.048 1.353-2.5 2-1.018-.644-1.46-1.08-2-2-1.028.71-1.69.918-3 1 1.081-1.048 1.757-2.03 2-3 .194-.776.84-1.551 1.79-2.21m11.654 5.997c.887-.457 1.28-.891 1.556-1.787 1.032.916 1.683 1.157 3 1-1.297-1.036-1.758-2.03-2-3-.5-2-4-4-8-4-.74 0-1.461.068-2.15.192"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],We=[["path",{d:"M12 4V2"}],["path",{d:"M5 10v4a7.004 7.004 0 0 0 5.277 6.787c.412.104.802.292 1.102.592L12 22l.621-.621c.3-.3.69-.488 1.102-.592A7.003 7.003 0 0 0 19 14v-4"}],["path",{d:"M12 4C8 4 4.5 6 4 8c-.243.97-.919 1.952-2 3 1.31-.082 1.972-.29 3-1 .54.92.982 1.356 2 2 1.452-.647 1.954-1.098 2.5-2 .595.995 1.151 1.427 2.5 2 1.31-.621 1.862-1.058 2.5-2 .629.977 1.162 1.423 2.5 2 1.209-.548 1.68-.967 2-2 1.032.916 1.683 1.157 3 1-1.297-1.036-1.758-2.03-2-3-.5-2-4-4-8-4Z"}]],F2=[["path",{d:"M12 16h.01"}],["path",{d:"M12 8v4"}],["path",{d:"M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"}]],Ie=[["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}],["path",{d:"M8 12h8"}]],D2=[["path",{d:"M10 15V9"}],["path",{d:"M14 15V9"}],["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}]],b2=[["path",{d:"m15 9-6 6"}],["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}],["path",{d:"m9 9 6 6"}]],Ee=[["path",{d:"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"}]],Xe=[["path",{d:"M3 20h4.5a.5.5 0 0 0 .5-.5v-.282a.52.52 0 0 0-.247-.437 8 8 0 1 1 8.494-.001.52.52 0 0 0-.247.438v.282a.5.5 0 0 0 .5.5H21"}]],je=[["path",{d:"M3 3h6l6 18h6"}],["path",{d:"M14 3h7"}]],Ne=[["path",{d:"M20.341 6.484A10 10 0 0 1 10.266 21.85"}],["path",{d:"M3.659 17.516A10 10 0 0 1 13.74 2.152"}],["circle",{cx:"12",cy:"12",r:"3"}],["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}]],Ke=[["path",{d:"M12 12V4a1 1 0 0 1 1-1h6.297a1 1 0 0 1 .651 1.759l-4.696 4.025"}],["path",{d:"m12 21-7.414-7.414A2 2 0 0 1 4 12.172V6.415a1.002 1.002 0 0 1 1.707-.707L20 20.009"}],["path",{d:"m12.214 3.381 8.414 14.966a1 1 0 0 1-.167 1.199l-1.168 1.163a1 1 0 0 1-.706.291H6.351a1 1 0 0 1-.625-.219L3.25 18.8a1 1 0 0 1 .631-1.781l4.165.027"}]],Qe=[["path",{d:"M12 3v6"}],["path",{d:"M16.76 3a2 2 0 0 1 1.8 1.1l2.23 4.479a2 2 0 0 1 .21.891V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9.472a2 2 0 0 1 .211-.894L5.45 4.1A2 2 0 0 1 7.24 3z"}],["path",{d:"M3.054 9.013h17.893"}]],Je=[["path",{d:"m16 16 2 2 4-4"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],Ye=[["path",{d:"M16 16h6"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],_e=[["path",{d:"M12 22v-9"}],["path",{d:"M15.17 2.21a1.67 1.67 0 0 1 1.63 0L21 4.57a1.93 1.93 0 0 1 0 3.36L8.82 14.79a1.655 1.655 0 0 1-1.64 0L3 12.43a1.93 1.93 0 0 1 0-3.36z"}],["path",{d:"M20 13v3.87a2.06 2.06 0 0 1-1.11 1.83l-6 3.08a1.93 1.93 0 0 1-1.78 0l-6-3.08A2.06 2.06 0 0 1 4 16.87V13"}],["path",{d:"M21 12.43a1.93 1.93 0 0 0 0-3.36L8.83 2.2a1.64 1.64 0 0 0-1.63 0L3 4.57a1.93 1.93 0 0 0 0 3.36l12.18 6.86a1.636 1.636 0 0 0 1.63 0z"}]],xe=[["path",{d:"M16 16h6"}],["path",{d:"M19 13v6"}],["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}]],ar=[["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}],["circle",{cx:"18.5",cy:"15.5",r:"2.5"}],["path",{d:"M20.27 17.27 22 19"}]],tr=[["path",{d:"M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"}],["path",{d:"m7.5 4.27 9 5.15"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["line",{x1:"12",x2:"12",y1:"22",y2:"12"}],["path",{d:"m17 13 5 5m-5 0 5-5"}]],hr=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"}],["path",{d:"M12 22V12"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["path",{d:"m7.5 4.27 9 5.15"}]],dr=[["path",{d:"M11 7 6 2"}],["path",{d:"M18.992 12H2.041"}],["path",{d:"M21.145 18.38A3.34 3.34 0 0 1 20 16.5a3.3 3.3 0 0 1-1.145 1.88c-.575.46-.855 1.02-.855 1.595A2 2 0 0 0 20 22a2 2 0 0 0 2-2.025c0-.58-.285-1.13-.855-1.595"}],["path",{d:"m8.5 4.5 2.148-2.148a1.205 1.205 0 0 1 1.704 0l7.296 7.296a1.205 1.205 0 0 1 0 1.704l-7.592 7.592a3.615 3.615 0 0 1-5.112 0l-3.888-3.888a3.615 3.615 0 0 1 0-5.112L5.67 7.33"}]],cr=[["rect",{width:"16",height:"6",x:"2",y:"2",rx:"2"}],["path",{d:"M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"}],["rect",{width:"4",height:"6",x:"8",y:"16",rx:"1"}]],R2=[["path",{d:"M10 2v2"}],["path",{d:"M14 2v4"}],["path",{d:"M17 2a1 1 0 0 1 1 1v9H6V3a1 1 0 0 1 1-1z"}],["path",{d:"M6 12a1 1 0 0 0-1 1v1a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v2.9a2 2 0 1 0 4 0V17a1 1 0 0 1 1-1h2a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1"}]],Mr=[["path",{d:"m14.622 17.897-10.68-2.913"}],["path",{d:"M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z"}],["path",{d:"M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15"}]],pr=[["path",{d:"M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z"}],["circle",{cx:"13.5",cy:"6.5",r:".5",fill:"currentColor"}],["circle",{cx:"17.5",cy:"10.5",r:".5",fill:"currentColor"}],["circle",{cx:"6.5",cy:"12.5",r:".5",fill:"currentColor"}],["circle",{cx:"8.5",cy:"7.5",r:".5",fill:"currentColor"}]],ir=[["path",{d:"M11.25 17.25h1.5L12 18z"}],["path",{d:"m15 12 2 2"}],["path",{d:"M18 6.5a.5.5 0 0 0-.5-.5"}],["path",{d:"M20.69 9.67a4.5 4.5 0 1 0-7.04-5.5 8.35 8.35 0 0 0-3.3 0 4.5 4.5 0 1 0-7.04 5.5C2.49 11.2 2 12.88 2 14.5 2 19.47 6.48 22 12 22s10-2.53 10-7.5c0-1.62-.48-3.3-1.3-4.83"}],["path",{d:"M6 6.5a.495.495 0 0 1 .5-.5"}],["path",{d:"m9 12-2 2"}]],nr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}],["path",{d:"m15 8-3 3-3-3"}]],T2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M14 15h1"}],["path",{d:"M19 15h2"}],["path",{d:"M3 15h2"}],["path",{d:"M9 15h1"}]],lr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}],["path",{d:"m9 10 3-3 3 3"}]],er=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h18"}]],q2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"m16 15-3-3 3-3"}]],U2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 14v1"}],["path",{d:"M9 19v2"}],["path",{d:"M9 3v2"}],["path",{d:"M9 9v1"}]],O2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"m14 9 3 3-3 3"}]],rr=[["path",{d:"M15 10V9"}],["path",{d:"M15 15v-1"}],["path",{d:"M15 21v-2"}],["path",{d:"M15 5V3"}],["path",{d:"M9 10V9"}],["path",{d:"M9 15v-1"}],["path",{d:"M9 21v-2"}],["path",{d:"M9 5V3"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Z2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}]],or=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}],["path",{d:"m8 9 3 3-3 3"}]],G2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 14v1"}],["path",{d:"M15 19v2"}],["path",{d:"M15 3v2"}],["path",{d:"M15 9v1"}]],vr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}],["path",{d:"m10 15-3-3 3-3"}]],$r=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M15 3v18"}]],mr=[["path",{d:"M14 15h1"}],["path",{d:"M14 9h1"}],["path",{d:"M19 15h2"}],["path",{d:"M19 9h2"}],["path",{d:"M3 15h2"}],["path",{d:"M3 9h2"}],["path",{d:"M9 15h1"}],["path",{d:"M9 9h1"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],yr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"m9 16 3-3 3 3"}]],W2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M14 9h1"}],["path",{d:"M19 9h2"}],["path",{d:"M3 9h2"}],["path",{d:"M9 9h1"}]],sr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"m15 14-3 3-3-3"}]],gr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 3v18"}],["path",{d:"M9 15h12"}]],Cr=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}]],ur=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 15h12"}],["path",{d:"M15 3v18"}]],I2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M9 21V9"}]],Hr=[["path",{d:"m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"}]],Ar=[["path",{d:"M11 15h2"}],["path",{d:"M12 12v3"}],["path",{d:"M12 19v3"}],["path",{d:"M15.282 19a1 1 0 0 0 .948-.68l2.37-6.988a7 7 0 1 0-13.2 0l2.37 6.988a1 1 0 0 0 .948.68z"}],["path",{d:"M9 9a3 3 0 1 1 6 0"}]],wr=[["path",{d:"M8 21s-4-3-4-9 4-9 4-9"}],["path",{d:"M16 3s4 3 4 9-4 9-4 9"}]],Vr=[["path",{d:"M5.8 11.3 2 22l10.7-3.79"}],["path",{d:"M4 3h.01"}],["path",{d:"M22 8h.01"}],["path",{d:"M15 2h.01"}],["path",{d:"M22 20h.01"}],["path",{d:"m22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10"}],["path",{d:"m22 13-.82-.33c-.86-.34-1.82.2-1.98 1.11c-.11.7-.72 1.22-1.43 1.22H17"}],["path",{d:"m11 2 .33.82c.34.86-.2 1.82-1.11 1.98C9.52 4.9 9 5.52 9 6.23V7"}],["path",{d:"M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z"}]],Sr=[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1"}]],Lr=[["circle",{cx:"11",cy:"4",r:"2"}],["circle",{cx:"18",cy:"8",r:"2"}],["circle",{cx:"20",cy:"16",r:"2"}],["path",{d:"M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z"}]],fr=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2"}],["path",{d:"M15 14h.01"}],["path",{d:"M9 6h6"}],["path",{d:"M9 10h6"}]],E2=[["path",{d:"M13 21h8"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],kr=[["path",{d:"m10 10-6.157 6.162a2 2 0 0 0-.5.833l-1.322 4.36a.5.5 0 0 0 .622.624l4.358-1.323a2 2 0 0 0 .83-.5L14 13.982"}],["path",{d:"m12.829 7.172 4.359-4.346a1 1 0 1 1 3.986 3.986l-4.353 4.353"}],["path",{d:"m2 2 20 20"}]],Pr=[["path",{d:"M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z"}],["path",{d:"m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18"}],["path",{d:"m2.3 2.3 7.286 7.286"}],["circle",{cx:"11",cy:"11",r:"2"}]],X2=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],Br=[["path",{d:"M13 21h8"}],["path",{d:"m15 5 4 4"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}]],zr=[["path",{d:"m10 10-6.157 6.162a2 2 0 0 0-.5.833l-1.322 4.36a.5.5 0 0 0 .622.624l4.358-1.323a2 2 0 0 0 .83-.5L14 13.982"}],["path",{d:"m12.829 7.172 4.359-4.346a1 1 0 1 1 3.986 3.986l-4.353 4.353"}],["path",{d:"m15 5 4 4"}],["path",{d:"m2 2 20 20"}]],Fr=[["path",{d:"M13 7 8.7 2.7a2.41 2.41 0 0 0-3.4 0L2.7 5.3a2.41 2.41 0 0 0 0 3.4L7 13"}],["path",{d:"m8 6 2-2"}],["path",{d:"m18 16 2-2"}],["path",{d:"m17 11 4.3 4.3c.94.94.94 2.46 0 3.4l-2.6 2.6c-.94.94-2.46.94-3.4 0L11 17"}],["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]],Dr=[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"}],["path",{d:"m15 5 4 4"}]],br=[["path",{d:"M10.83 2.38a2 2 0 0 1 2.34 0l8 5.74a2 2 0 0 1 .73 2.25l-3.04 9.26a2 2 0 0 1-1.9 1.37H7.04a2 2 0 0 1-1.9-1.37L2.1 10.37a2 2 0 0 1 .73-2.25z"}]],Rr=[["line",{x1:"19",x2:"5",y1:"5",y2:"19"}],["circle",{cx:"6.5",cy:"6.5",r:"2.5"}],["circle",{cx:"17.5",cy:"17.5",r:"2.5"}]],Tr=[["circle",{cx:"12",cy:"5",r:"1"}],["path",{d:"m9 20 3-6 3 6"}],["path",{d:"m6 8 6 2 6-2"}],["path",{d:"M12 10v4"}]],qr=[["path",{d:"M20 11H4"}],["path",{d:"M20 7H4"}],["path",{d:"M7 21V4a1 1 0 0 1 1-1h4a1 1 0 0 1 0 12H7"}]],Ur=[["path",{d:"M14 6h8"}],["path",{d:"m18 2 4 4-4 4"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Or=[["path",{d:"M13 2a9 9 0 0 1 9 9"}],["path",{d:"M13 6a5 5 0 0 1 5 5"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Zr=[["path",{d:"M16 2v6h6"}],["path",{d:"m22 2-6 6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Gr=[["path",{d:"m16 2 6 6"}],["path",{d:"m22 2-6 6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Wr=[["path",{d:"M10.1 13.9a14 14 0 0 0 3.732 2.668 1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2 18 18 0 0 1-12.728-5.272"}],["path",{d:"M22 2 2 22"}],["path",{d:"M4.76 13.582A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 .244.473"}]],Ir=[["path",{d:"m16 8 6-6"}],["path",{d:"M22 8V2h-6"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Er=[["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"}]],Xr=[["line",{x1:"9",x2:"9",y1:"4",y2:"20"}],["path",{d:"M4 7c0-1.7 1.3-3 3-3h13"}],["path",{d:"M18 20c-1.7 0-3-1.3-3-3V4"}]],jr=[["path",{d:"M18.5 8c-1.4 0-2.6-.8-3.2-2A6.87 6.87 0 0 0 2 9v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-8.5C22 9.6 20.4 8 18.5 8"}],["path",{d:"M2 14h20"}],["path",{d:"M6 14v4"}],["path",{d:"M10 14v4"}],["path",{d:"M14 14v4"}],["path",{d:"M18 14v4"}]],Nr=[["path",{d:"m14 13-8.381 8.38a1 1 0 0 1-3.001-3L11 9.999"}],["path",{d:"M15.973 4.027A13 13 0 0 0 5.902 2.373c-1.398.342-1.092 2.158.277 2.601a19.9 19.9 0 0 1 5.822 3.024"}],["path",{d:"M16.001 11.999a19.9 19.9 0 0 1 3.024 5.824c.444 1.369 2.26 1.676 2.603.278A13 13 0 0 0 20 8.069"}],["path",{d:"M18.352 3.352a1.205 1.205 0 0 0-1.704 0l-5.296 5.296a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l5.296-5.296a1.205 1.205 0 0 0 0-1.704z"}]],Kr=[["path",{d:"M21 9V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h4"}],["rect",{width:"10",height:"7",x:"12",y:"13",rx:"2"}]],Qr=[["path",{d:"M2 10h6V4"}],["path",{d:"m2 4 6 6"}],["path",{d:"M21 10V7a2 2 0 0 0-2-2h-7"}],["path",{d:"M3 14v2a2 2 0 0 0 2 2h3"}],["rect",{x:"12",y:"14",width:"10",height:"7",rx:"1"}]],Jr=[["path",{d:"M11 17h3v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a3.16 3.16 0 0 0 2-2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1h-1a5 5 0 0 0-2-4V3a4 4 0 0 0-3.2 1.6l-.3.4H11a6 6 0 0 0-6 6v1a5 5 0 0 0 2 4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z"}],["path",{d:"M16 10h.01"}],["path",{d:"M2 8v1a2 2 0 0 0 2 2h1"}]],Yr=[["path",{d:"M14 3v11"}],["path",{d:"M14 9h-3a3 3 0 0 1 0-6h9"}],["path",{d:"M18 3v11"}],["path",{d:"M22 18H2l4-4"}],["path",{d:"m6 22-4-4"}]],_r=[["path",{d:"M10 3v11"}],["path",{d:"M10 9H7a1 1 0 0 1 0-6h8"}],["path",{d:"M14 3v11"}],["path",{d:"m18 14 4 4H2"}],["path",{d:"m22 18-4 4"}]],xr=[["path",{d:"M13 4v16"}],["path",{d:"M17 4v16"}],["path",{d:"M19 4H9.5a4.5 4.5 0 0 0 0 9H13"}]],ao=[["path",{d:"M18 11h-4a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h4"}],["path",{d:"M6 7v13a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7"}],["rect",{width:"16",height:"5",x:"4",y:"2",rx:"1"}]],to=[["path",{d:"m10.5 20.5 10-10a4.95 4.95 0 1 0-7-7l-10 10a4.95 4.95 0 1 0 7 7Z"}],["path",{d:"m8.5 8.5 7 7"}]],ho=[["path",{d:"M12 17v5"}],["path",{d:"M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89"}],["path",{d:"m2 2 20 20"}],["path",{d:"M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11"}]],co=[["path",{d:"M12 17v5"}],["path",{d:"M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"}]],Mo=[["path",{d:"m12 9-8.414 8.414A2 2 0 0 0 3 18.828v1.344a2 2 0 0 1-.586 1.414A2 2 0 0 1 3.828 21h1.344a2 2 0 0 0 1.414-.586L15 12"}],["path",{d:"m18 9 .4.4a1 1 0 1 1-3 3l-3.8-3.8a1 1 0 1 1 3-3l.4.4 3.4-3.4a1 1 0 1 1 3 3z"}],["path",{d:"m2 22 .414-.414"}]],po=[["path",{d:"m12 14-1 1"}],["path",{d:"m13.75 18.25-1.25 1.42"}],["path",{d:"M17.775 5.654a15.68 15.68 0 0 0-12.121 12.12"}],["path",{d:"M18.8 9.3a1 1 0 0 0 2.1 7.7"}],["path",{d:"M21.964 20.732a1 1 0 0 1-1.232 1.232l-18-5a1 1 0 0 1-.695-1.232A19.68 19.68 0 0 1 15.732 2.037a1 1 0 0 1 1.232.695z"}]],io=[["path",{d:"M2 22h20"}],["path",{d:"M3.77 10.77 2 9l2-4.5 1.1.55c.55.28.9.84.9 1.45s.35 1.17.9 1.45L8 8.5l3-6 1.05.53a2 2 0 0 1 1.09 1.52l.72 5.4a2 2 0 0 0 1.09 1.52l4.4 2.2c.42.22.78.55 1.01.96l.6 1.03c.49.88-.06 1.98-1.06 2.1l-1.18.15c-.47.06-.95-.02-1.37-.24L4.29 11.15a2 2 0 0 1-.52-.38Z"}]],no=[["path",{d:"M2 22h20"}],["path",{d:"M6.36 17.4 4 17l-2-4 1.1-.55a2 2 0 0 1 1.8 0l.17.1a2 2 0 0 0 1.8 0L8 12 5 6l.9-.45a2 2 0 0 1 2.09.2l4.02 3a2 2 0 0 0 2.1.2l4.19-2.06a2.41 2.41 0 0 1 1.73-.17L21 7a1.4 1.4 0 0 1 .87 1.99l-.38.76c-.23.46-.6.84-1.07 1.08L7.58 17.2a2 2 0 0 1-1.22.18Z"}]],lo=[["path",{d:"M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z"}]],eo=[["path",{d:"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"}]],ro=[["path",{d:"M9 2v6"}],["path",{d:"M15 2v6"}],["path",{d:"M12 17v5"}],["path",{d:"M5 8h14"}],["path",{d:"M6 11V8h12v3a6 6 0 1 1-12 0Z"}]],j2=[["path",{d:"M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"}],["path",{d:"m2 22 3-3"}],["path",{d:"M7.5 13.5 10 11"}],["path",{d:"M10.5 16.5 13 14"}],["path",{d:"m18 3-4 4h6l-4 4"}]],oo=[["path",{d:"M12 22v-5"}],["path",{d:"M15 8V2"}],["path",{d:"M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"}],["path",{d:"M9 8V2"}]],vo=[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]],$o=[["path",{d:"M3 2v1c0 1 2 1 2 2S3 6 3 7s2 1 2 2-2 1-2 2 2 1 2 2"}],["path",{d:"M18 6h.01"}],["path",{d:"M6 18h.01"}],["path",{d:"M20.83 8.83a4 4 0 0 0-5.66-5.66l-12 12a4 4 0 1 0 5.66 5.66Z"}],["path",{d:"M18 11.66V22a4 4 0 0 0 4-4V6"}]],mo=[["path",{d:"M20 3a2 2 0 0 1 2 2v6a1 1 0 0 1-20 0V5a2 2 0 0 1 2-2z"}],["path",{d:"m8 10 4 4 4-4"}]],yo=[["path",{d:"M13 17a1 1 0 1 0-2 0l.5 4.5a0.5 0.5 0 0 0 1 0z",fill:"currentColor"}],["path",{d:"M16.85 18.58a9 9 0 1 0-9.7 0"}],["path",{d:"M8 14a5 5 0 1 1 8 0"}],["circle",{cx:"12",cy:"11",r:"1",fill:"currentColor"}]],so=[["path",{d:"M10 4.5V4a2 2 0 0 0-2.41-1.957"}],["path",{d:"M13.9 8.4a2 2 0 0 0-1.26-1.295"}],["path",{d:"M21.7 16.2A8 8 0 0 0 22 14v-3a2 2 0 1 0-4 0v-1a2 2 0 0 0-3.63-1.158"}],["path",{d:"m7 15-1.8-1.8a2 2 0 0 0-2.79 2.86L6 19.7a7.74 7.74 0 0 0 6 2.3h2a8 8 0 0 0 5.657-2.343"}],["path",{d:"M6 6v8"}],["path",{d:"m2 2 20 20"}]],go=[["path",{d:"M22 14a8 8 0 0 1-8 8"}],["path",{d:"M18 11v-1a2 2 0 0 0-2-2a2 2 0 0 0-2 2"}],["path",{d:"M14 10V9a2 2 0 0 0-2-2a2 2 0 0 0-2 2v1"}],["path",{d:"M10 9.5V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v10"}],["path",{d:"M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"}]],Co=[["path",{d:"M18 8a2 2 0 0 0 0-4 2 2 0 0 0-4 0 2 2 0 0 0-4 0 2 2 0 0 0-4 0 2 2 0 0 0 0 4"}],["path",{d:"M10 22 9 8"}],["path",{d:"m14 22 1-14"}],["path",{d:"M20 8c.5 0 .9.4.8 1l-2.6 12c-.1.5-.7 1-1.2 1H7c-.6 0-1.1-.4-1.2-1L3.2 9c-.1-.6.3-1 .8-1Z"}]],uo=[["path",{d:"M18.6 14.4c.8-.8.8-2 0-2.8l-8.1-8.1a4.95 4.95 0 1 0-7.1 7.1l8.1 8.1c.9.7 2.1.7 2.9-.1Z"}],["path",{d:"m22 22-5.5-5.5"}]],Ho=[["path",{d:"M18 7c0-5.333-8-5.333-8 0"}],["path",{d:"M10 7v14"}],["path",{d:"M6 21h12"}],["path",{d:"M6 13h10"}]],Ao=[["path",{d:"M18.36 6.64A9 9 0 0 1 20.77 15"}],["path",{d:"M6.16 6.16a9 9 0 1 0 12.68 12.68"}],["path",{d:"M12 2v4"}],["path",{d:"m2 2 20 20"}]],wo=[["path",{d:"M12 2v10"}],["path",{d:"M18.4 6.6a9 9 0 1 1-12.77.04"}]],Vo=[["path",{d:"M2 3h20"}],["path",{d:"M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"}],["path",{d:"m7 21 5-5 5 5"}]],So=[["path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"}],["rect",{x:"6",y:"14",width:"12",height:"8",rx:"1"}]],Lo=[["path",{d:"M13.5 22H7a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v.5"}],["path",{d:"m16 19 2 2 4-4"}],["path",{d:"M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v2"}],["path",{d:"M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"}]],fo=[["path",{d:"M5 7 3 5"}],["path",{d:"M9 6V3"}],["path",{d:"m13 7 2-2"}],["circle",{cx:"9",cy:"13",r:"3"}],["path",{d:"M11.83 12H20a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h2.17"}],["path",{d:"M16 16h2"}]],ko=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M12 9v11"}],["path",{d:"M2 9h13a2 2 0 0 1 2 2v9"}]],Po=[["path",{d:"M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z"}]],Bo=[["path",{d:"M2.5 16.88a1 1 0 0 1-.32-1.43l9-13.02a1 1 0 0 1 1.64 0l9 13.01a1 1 0 0 1-.32 1.44l-8.51 4.86a2 2 0 0 1-1.98 0Z"}],["path",{d:"M12 2v20"}]],zo=[["rect",{width:"5",height:"5",x:"3",y:"3",rx:"1"}],["rect",{width:"5",height:"5",x:"16",y:"3",rx:"1"}],["rect",{width:"5",height:"5",x:"3",y:"16",rx:"1"}],["path",{d:"M21 16h-3a2 2 0 0 0-2 2v3"}],["path",{d:"M21 21v.01"}],["path",{d:"M12 7v3a2 2 0 0 1-2 2H7"}],["path",{d:"M3 12h.01"}],["path",{d:"M12 3h.01"}],["path",{d:"M12 16v.01"}],["path",{d:"M16 12h1"}],["path",{d:"M21 12v.01"}],["path",{d:"M12 21v-1"}]],Fo=[["path",{d:"M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"}],["path",{d:"M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"}]],Do=[["path",{d:"M13 16a3 3 0 0 1 2.24 5"}],["path",{d:"M18 12h.01"}],["path",{d:"M18 21h-8a4 4 0 0 1-4-4 7 7 0 0 1 7-7h.2L9.6 6.4a1 1 0 1 1 2.8-2.8L15.8 7h.2c3.3 0 6 2.7 6 6v1a2 2 0 0 1-2 2h-1a3 3 0 0 0-3 3"}],["path",{d:"M20 8.54V4a2 2 0 1 0-4 0v3"}],["path",{d:"M7.612 12.524a3 3 0 1 0-1.6 4.3"}]],bo=[["path",{d:"M19.07 4.93A10 10 0 0 0 6.99 3.34"}],["path",{d:"M4 6h.01"}],["path",{d:"M2.29 9.62A10 10 0 1 0 21.31 8.35"}],["path",{d:"M16.24 7.76A6 6 0 1 0 8.23 16.67"}],["path",{d:"M12 18h.01"}],["path",{d:"M17.99 11.66A6 6 0 0 1 15.77 16.67"}],["circle",{cx:"12",cy:"12",r:"2"}],["path",{d:"m13.41 10.59 5.66-5.66"}]],Ro=[["path",{d:"M12 12h.01"}],["path",{d:"M14 15.4641a4 4 0 0 1-4 0L7.52786 19.74597 A 1 1 0 0 0 7.99303 21.16211 10 10 0 0 0 16.00697 21.16211 1 1 0 0 0 16.47214 19.74597z"}],["path",{d:"M16 12a4 4 0 0 0-2-3.464l2.472-4.282a1 1 0 0 1 1.46-.305 10 10 0 0 1 4.006 6.94A1 1 0 0 1 21 12z"}],["path",{d:"M8 12a4 4 0 0 1 2-3.464L7.528 4.254a1 1 0 0 0-1.46-.305 10 10 0 0 0-4.006 6.94A1 1 0 0 0 3 12z"}]],To=[["path",{d:"M3 12h3.28a1 1 0 0 1 .948.684l2.298 7.934a.5.5 0 0 0 .96-.044L13.82 4.771A1 1 0 0 1 14.792 4H21"}]],qo=[["path",{d:"M5 16v2"}],["path",{d:"M19 16v2"}],["rect",{width:"20",height:"8",x:"2",y:"8",rx:"2"}],["path",{d:"M18 12h.01"}]],Uo=[["path",{d:"M4.9 16.1C1 12.2 1 5.8 4.9 1.9"}],["path",{d:"M7.8 4.7a6.14 6.14 0 0 0-.8 7.5"}],["circle",{cx:"12",cy:"9",r:"2"}],["path",{d:"M16.2 4.8c2 2 2.26 5.11.8 7.47"}],["path",{d:"M19.1 1.9a9.96 9.96 0 0 1 0 14.1"}],["path",{d:"M9.5 18h5"}],["path",{d:"m8 22 4-11 4 11"}]],Oo=[["path",{d:"M16.247 7.761a6 6 0 0 1 0 8.478"}],["path",{d:"M19.075 4.933a10 10 0 0 1 0 14.134"}],["path",{d:"M4.925 19.067a10 10 0 0 1 0-14.134"}],["path",{d:"M7.753 16.239a6 6 0 0 1 0-8.478"}],["circle",{cx:"12",cy:"12",r:"2"}]],Zo=[["path",{d:"M20.34 17.52a10 10 0 1 0-2.82 2.82"}],["circle",{cx:"19",cy:"19",r:"2"}],["path",{d:"m13.41 13.41 4.18 4.18"}],["circle",{cx:"12",cy:"12",r:"2"}]],Go=[["path",{d:"M5 15h14"}],["path",{d:"M5 9h14"}],["path",{d:"m14 20-5-5 6-6-5-5"}]],Wo=[["path",{d:"M22 17a10 10 0 0 0-20 0"}],["path",{d:"M6 17a6 6 0 0 1 12 0"}],["path",{d:"M10 17a2 2 0 0 1 4 0"}]],Io=[["path",{d:"M13 22H4a2 2 0 0 1 0-4h12"}],["path",{d:"M13.236 18a3 3 0 0 0-2.2-5"}],["path",{d:"M16 9h.01"}],["path",{d:"M16.82 3.94a3 3 0 1 1 3.237 4.868l1.815 2.587a1.5 1.5 0 0 1-1.5 2.1l-2.872-.453a3 3 0 0 0-3.5 3"}],["path",{d:"M17 4.988a3 3 0 1 0-5.2 2.052A7 7 0 0 0 4 14.015 4 4 0 0 0 8 18"}]],Eo=[["rect",{width:"12",height:"20",x:"6",y:"2",rx:"2"}],["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],Xo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M12 6.5v11"}],["path",{d:"M15 9.4a4 4 0 1 0 0 5.2"}]],jo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 12h5"}],["path",{d:"M16 9.5a4 4 0 1 0 0 5.2"}]],No=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 7h8"}],["path",{d:"M12 17.5 8 15h1a4 4 0 0 0 0-8"}],["path",{d:"M8 11h8"}]],Ko=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"m12 10 3-3"}],["path",{d:"m9 7 3 3v7.5"}],["path",{d:"M9 11h6"}],["path",{d:"M9 15h6"}]],Qo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 13h5"}],["path",{d:"M10 17V9.5a2.5 2.5 0 0 1 5 0"}],["path",{d:"M8 17h7"}]],Jo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M8 15h5"}],["path",{d:"M8 11h5a2 2 0 1 0 0-4h-3v10"}]],Yo=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M10 17V7h5"}],["path",{d:"M10 11h4"}],["path",{d:"M8 15h5"}]],_o=[["path",{d:"M13 16H8"}],["path",{d:"M14 8H8"}],["path",{d:"M16 12H8"}],["path",{d:"M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z"}]],xo=[["path",{d:"M10 6.5v11a5.5 5.5 0 0 0 5.5-5.5"}],["path",{d:"m14 8-6 3"}],["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1z"}]],av=[["path",{d:"M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"}],["path",{d:"M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"}],["path",{d:"M12 17.5v-11"}]],tv=[["path",{d:"M14 4v16H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"}],["circle",{cx:"14",cy:"12",r:"8"}]],N2=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}],["path",{d:"M12 12h.01"}],["path",{d:"M17 12h.01"}],["path",{d:"M7 12h.01"}]],hv=[["path",{d:"M20 6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-4a2 2 0 0 1-1.6-.8l-1.6-2.13a1 1 0 0 0-1.6 0L9.6 17.2A2 2 0 0 1 8 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"}]],dv=[["rect",{width:"20",height:"12",x:"2",y:"6",rx:"2"}]],cv=[["rect",{width:"12",height:"20",x:"6",y:"2",rx:"2"}]],Mv=[["path",{d:"M7 19H4.815a1.83 1.83 0 0 1-1.57-.881 1.785 1.785 0 0 1-.004-1.784L7.196 9.5"}],["path",{d:"M11 19h8.203a1.83 1.83 0 0 0 1.556-.89 1.784 1.784 0 0 0 0-1.775l-1.226-2.12"}],["path",{d:"m14 16-3 3 3 3"}],["path",{d:"M8.293 13.596 7.196 9.5 3.1 10.598"}],["path",{d:"m9.344 5.811 1.093-1.892A1.83 1.83 0 0 1 11.985 3a1.784 1.784 0 0 1 1.546.888l3.943 6.843"}],["path",{d:"m13.378 9.633 4.096 1.098 1.097-4.096"}]],pv=[["path",{d:"m15 14 5-5-5-5"}],["path",{d:"M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13"}]],iv=[["circle",{cx:"12",cy:"17",r:"1"}],["path",{d:"M21 7v6h-6"}],["path",{d:"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"}]],nv=[["path",{d:"M21 7v6h-6"}],["path",{d:"M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"}]],lv=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"}],["path",{d:"M16 16h5v5"}],["circle",{cx:"12",cy:"12",r:"1"}]],ev=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"}],["path",{d:"M16 16h5v5"}]],rv=[["path",{d:"M21 8L18.74 5.74A9.75 9.75 0 0 0 12 3C11 3 10.03 3.16 9.13 3.47"}],["path",{d:"M8 16H3v5"}],["path",{d:"M3 12C3 9.51 4 7.26 5.64 5.64"}],["path",{d:"m3 16 2.26 2.26A9.75 9.75 0 0 0 12 21c2.49 0 4.74-1 6.36-2.64"}],["path",{d:"M21 12c0 1-.16 1.97-.47 2.87"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M22 22 2 2"}]],ov=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"}],["path",{d:"M8 16H3v5"}]],vv=[["path",{d:"M5 6a4 4 0 0 1 4-4h6a4 4 0 0 1 4 4v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6Z"}],["path",{d:"M5 10h14"}],["path",{d:"M15 7v6"}]],$v=[["path",{d:"M17 3v10"}],["path",{d:"m12.67 5.5 8.66 5"}],["path",{d:"m12.67 10.5 8.66-5"}],["path",{d:"M9 17a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2z"}]],mv=[["path",{d:"M4 7V4h16v3"}],["path",{d:"M5 20h6"}],["path",{d:"M13 4 8 20"}],["path",{d:"m15 15 5 5"}],["path",{d:"m20 15-5 5"}]],yv=[["path",{d:"m17 2 4 4-4 4"}],["path",{d:"M3 11v-1a4 4 0 0 1 4-4h14"}],["path",{d:"m7 22-4-4 4-4"}],["path",{d:"M21 13v1a4 4 0 0 1-4 4H3"}],["path",{d:"M11 10h1v4"}]],sv=[["path",{d:"m2 9 3-3 3 3"}],["path",{d:"M13 18H7a2 2 0 0 1-2-2V6"}],["path",{d:"m22 15-3 3-3-3"}],["path",{d:"M11 6h6a2 2 0 0 1 2 2v10"}]],gv=[["path",{d:"m17 2 4 4-4 4"}],["path",{d:"M3 11v-1a4 4 0 0 1 4-4h14"}],["path",{d:"m7 22-4-4 4-4"}],["path",{d:"M21 13v1a4 4 0 0 1-4 4H3"}]],Cv=[["path",{d:"M14 14a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M14 4a1 1 0 0 1 1-1"}],["path",{d:"M15 10a1 1 0 0 1-1-1"}],["path",{d:"M19 14a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1"}],["path",{d:"M21 4a1 1 0 0 0-1-1"}],["path",{d:"M21 9a1 1 0 0 1-1 1"}],["path",{d:"m3 7 3 3 3-3"}],["path",{d:"M6 10V5a2 2 0 0 1 2-2h2"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}]],uv=[["path",{d:"M14 4a1 1 0 0 1 1-1"}],["path",{d:"M15 10a1 1 0 0 1-1-1"}],["path",{d:"M21 4a1 1 0 0 0-1-1"}],["path",{d:"M21 9a1 1 0 0 1-1 1"}],["path",{d:"m3 7 3 3 3-3"}],["path",{d:"M6 10V5a2 2 0 0 1 2-2h2"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}]],Hv=[["path",{d:"m12 17-5-5 5-5"}],["path",{d:"M22 18v-2a4 4 0 0 0-4-4H7"}],["path",{d:"m7 17-5-5 5-5"}]],Av=[["path",{d:"M20 18v-2a4 4 0 0 0-4-4H4"}],["path",{d:"m9 17-5-5 5-5"}]],wv=[["path",{d:"M12 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 12 18z"}],["path",{d:"M22 6a2 2 0 0 0-3.414-1.414l-6 6a2 2 0 0 0 0 2.828l6 6A2 2 0 0 0 22 18z"}]],Vv=[["path",{d:"M12 11.22C11 9.997 10 9 10 8a2 2 0 0 1 4 0c0 1-.998 2.002-2.01 3.22"}],["path",{d:"m12 18 2.57-3.5"}],["path",{d:"M6.243 9.016a7 7 0 0 1 11.507-.009"}],["path",{d:"M9.35 14.53 12 11.22"}],["path",{d:"M9.35 14.53C7.728 12.246 6 10.221 6 7a6 5 0 0 1 12 0c-.005 3.22-1.778 5.235-3.43 7.5l3.557 4.527a1 1 0 0 1-.203 1.43l-1.894 1.36a1 1 0 0 1-1.384-.215L12 18l-2.679 3.593a1 1 0 0 1-1.39.213l-1.865-1.353a1 1 0 0 1-.203-1.422z"}]],Sv=[["path",{d:"M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"}],["path",{d:"m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"}],["path",{d:"M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"}],["path",{d:"M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"}]],Lv=[["polyline",{points:"3.5 2 6.5 12.5 18 12.5"}],["line",{x1:"9.5",x2:"5.5",y1:"12.5",y2:"20"}],["line",{x1:"15",x2:"18.5",y1:"12.5",y2:"20"}],["path",{d:"M2.75 18a13 13 0 0 0 18.5 0"}]],fv=[["path",{d:"M6 19V5"}],["path",{d:"M10 19V6.8"}],["path",{d:"M14 19v-7.8"}],["path",{d:"M18 5v4"}],["path",{d:"M18 19v-6"}],["path",{d:"M22 19V9"}],["path",{d:"M2 19V9a4 4 0 0 1 4-4c2 0 4 1.33 6 4s4 4 6 4a4 4 0 1 0-3-6.65"}]],kv=[["path",{d:"M17 10h-1a4 4 0 1 1 4-4v.534"}],["path",{d:"M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"}],["path",{d:"M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"}],["path",{d:"M9.77 12C4 15 2 22 2 22"}],["circle",{cx:"17",cy:"8",r:"2"}]],K2=[["path",{d:"M16.466 7.5C15.643 4.237 13.952 2 12 2 9.239 2 7 6.477 7 12s2.239 10 5 10c.342 0 .677-.069 1-.2"}],["path",{d:"m15.194 13.707 3.814 1.86-1.86 3.814"}],["path",{d:"M19 15.57c-1.804.885-4.274 1.43-7 1.43-5.523 0-10-2.239-10-5s4.477-5 10-5c4.838 0 8.873 1.718 9.8 4"}]],Pv=[["path",{d:"m14.5 9.5 1 1"}],["path",{d:"m15.5 8.5-4 4"}],["path",{d:"M3 12a9 9 0 1 0 9-9 9.74 9.74 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["circle",{cx:"10",cy:"14",r:"2"}]],Bv=[["path",{d:"M20 9V7a2 2 0 0 0-2-2h-6"}],["path",{d:"m15 2-3 3 3 3"}],["path",{d:"M20 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2"}]],zv=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}]],Fv=[["path",{d:"M12 5H6a2 2 0 0 0-2 2v3"}],["path",{d:"m9 8 3-3-3-3"}],["path",{d:"M4 14v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"}]],Dv=[["path",{d:"M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"}],["path",{d:"M21 3v5h-5"}]],bv=[["circle",{cx:"6",cy:"19",r:"3"}],["path",{d:"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15"}],["circle",{cx:"18",cy:"5",r:"3"}]],Rv=[["circle",{cx:"6",cy:"19",r:"3"}],["path",{d:"M9 19h8.5c.4 0 .9-.1 1.3-.2"}],["path",{d:"M5.2 5.2A3.5 3.53 0 0 0 6.5 12H12"}],["path",{d:"m2 2 20 20"}],["path",{d:"M21 15.3a3.5 3.5 0 0 0-3.3-3.3"}],["path",{d:"M15 5h-4.3"}],["circle",{cx:"18",cy:"5",r:"3"}]],Tv=[["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2"}],["path",{d:"M6.01 18H6"}],["path",{d:"M10.01 18H10"}],["path",{d:"M15 10v4"}],["path",{d:"M17.84 7.17a4 4 0 0 0-5.66 0"}],["path",{d:"M20.66 4.34a8 8 0 0 0-11.31 0"}]],Q2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 12h18"}]],J2=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 9H3"}],["path",{d:"M21 15H3"}]],qv=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 7.5H3"}],["path",{d:"M21 12H3"}],["path",{d:"M21 16.5H3"}]],Uv=[["path",{d:"M4 11a9 9 0 0 1 9 9"}],["path",{d:"M4 4a16 16 0 0 1 16 16"}],["circle",{cx:"5",cy:"19",r:"1"}]],Ov=[["path",{d:"M10 15v-3"}],["path",{d:"M14 15v-3"}],["path",{d:"M18 15v-3"}],["path",{d:"M2 8V4"}],["path",{d:"M22 6H2"}],["path",{d:"M22 8V4"}],["path",{d:"M6 15v-3"}],["rect",{x:"2",y:"12",width:"20",height:"8",rx:"2"}]],Zv=[["path",{d:"M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"}],["path",{d:"m14.5 12.5 2-2"}],["path",{d:"m11.5 9.5 2-2"}],["path",{d:"m8.5 6.5 2-2"}],["path",{d:"m17.5 15.5 2-2"}]],Gv=[["path",{d:"M6 11h8a4 4 0 0 0 0-8H9v18"}],["path",{d:"M6 15h8"}]],Wv=[["path",{d:"M10 2v15"}],["path",{d:"M7 22a4 4 0 0 1-4-4 1 1 0 0 1 1-1h16a1 1 0 0 1 1 1 4 4 0 0 1-4 4z"}],["path",{d:"M9.159 2.46a1 1 0 0 1 1.521-.193l9.977 8.98A1 1 0 0 1 20 13H4a1 1 0 0 1-.824-1.567z"}]],Iv=[["path",{d:"M7 21h10"}],["path",{d:"M12 21a9 9 0 0 0 9-9H3a9 9 0 0 0 9 9Z"}],["path",{d:"M11.38 12a2.4 2.4 0 0 1-.4-4.77 2.4 2.4 0 0 1 3.2-2.77 2.4 2.4 0 0 1 3.47-.63 2.4 2.4 0 0 1 3.37 3.37 2.4 2.4 0 0 1-1.1 3.7 2.51 2.51 0 0 1 .03 1.1"}],["path",{d:"m13 12 4-4"}],["path",{d:"M10.9 7.25A3.99 3.99 0 0 0 4 10c0 .73.2 1.41.54 2"}]],Ev=[["path",{d:"m2.37 11.223 8.372-6.777a2 2 0 0 1 2.516 0l8.371 6.777"}],["path",{d:"M21 15a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-5.25"}],["path",{d:"M3 15a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h9"}],["path",{d:"m6.67 15 6.13 4.6a2 2 0 0 0 2.8-.4l3.15-4.2"}],["rect",{width:"20",height:"4",x:"2",y:"11",rx:"1"}]],Xv=[["path",{d:"M4 10a7.31 7.31 0 0 0 10 10Z"}],["path",{d:"m9 15 3-3"}],["path",{d:"M17 13a6 6 0 0 0-6-6"}],["path",{d:"M21 13A10 10 0 0 0 11 3"}]],jv=[["path",{d:"m13.5 6.5-3.148-3.148a1.205 1.205 0 0 0-1.704 0L6.352 5.648a1.205 1.205 0 0 0 0 1.704L9.5 10.5"}],["path",{d:"M16.5 7.5 19 5"}],["path",{d:"m17.5 10.5 3.148 3.148a1.205 1.205 0 0 1 0 1.704l-2.296 2.296a1.205 1.205 0 0 1-1.704 0L13.5 14.5"}],["path",{d:"M9 21a6 6 0 0 0-6-6"}],["path",{d:"M9.352 10.648a1.205 1.205 0 0 0 0 1.704l2.296 2.296a1.205 1.205 0 0 0 1.704 0l4.296-4.296a1.205 1.205 0 0 0 0-1.704l-2.296-2.296a1.205 1.205 0 0 0-1.704 0z"}]],Nv=[["path",{d:"m20 19.5-5.5 1.2"}],["path",{d:"M14.5 4v11.22a1 1 0 0 0 1.242.97L20 15.2"}],["path",{d:"m2.978 19.351 5.549-1.363A2 2 0 0 0 10 16V2"}],["path",{d:"M20 10 4 13.5"}]],Kv=[["path",{d:"M10 2v3a1 1 0 0 0 1 1h5"}],["path",{d:"M18 18v-6a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6"}],["path",{d:"M18 22H4a2 2 0 0 1-2-2V6"}],["path",{d:"M8 18a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9.172a2 2 0 0 1 1.414.586l2.828 2.828A2 2 0 0 1 22 6.828V16a2 2 0 0 1-2.01 2z"}]],Qv=[["path",{d:"M13 13H8a1 1 0 0 0-1 1v7"}],["path",{d:"M14 8h1"}],["path",{d:"M17 21v-4"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20.41 20.41A2 2 0 0 1 19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 .59-1.41"}],["path",{d:"M29.5 11.5s5 5 4 5"}],["path",{d:"M9 3h6.2a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V15"}]],Jv=[["path",{d:"M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"}],["path",{d:"M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"}],["path",{d:"M7 3v4a1 1 0 0 0 1 1h7"}]],Y2=[["path",{d:"M5 7v11a1 1 0 0 0 1 1h11"}],["path",{d:"M5.293 18.707 11 13"}],["circle",{cx:"19",cy:"19",r:"2"}],["circle",{cx:"5",cy:"5",r:"2"}]],Yv=[["path",{d:"M12 3v18"}],["path",{d:"m19 8 3 8a5 5 0 0 1-6 0zV7"}],["path",{d:"M3 7h1a17 17 0 0 0 8-2 17 17 0 0 0 8 2h1"}],["path",{d:"m5 8 3 8a5 5 0 0 1-6 0zV7"}],["path",{d:"M7 21h10"}]],_v=[["path",{d:"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}],["path",{d:"M14 15H9v-5"}],["path",{d:"M16 3h5v5"}],["path",{d:"M21 3 9 15"}]],xv=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"}]],a$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M8 7v10"}],["path",{d:"M12 7v10"}],["path",{d:"M17 7v10"}]],t$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 9h.01"}]],h$=[["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7.828 13.07A3 3 0 0 1 12 8.764a3 3 0 0 1 4.172 4.306l-3.447 3.62a1 1 0 0 1-1.449 0z"}]],d$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7 12h10"}]],c$=[["path",{d:"M17 12v4a1 1 0 0 1-1 1h-4"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M17 8V7"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M7 17h.01"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["rect",{x:"7",y:"7",width:"5",height:"5",rx:"1"}]],M$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"m16 16-1.9-1.9"}]],p$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M7 8h8"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h6"}]],i$=[["path",{d:"M3 7V5a2 2 0 0 1 2-2h2"}],["path",{d:"M17 3h2a2 2 0 0 1 2 2v2"}],["path",{d:"M21 17v2a2 2 0 0 1-2 2h-2"}],["path",{d:"M7 21H5a2 2 0 0 1-2-2v-2"}]],n$=[["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M18 5v16"}],["path",{d:"m4 6 7.106-3.79a2 2 0 0 1 1.788 0L20 6"}],["path",{d:"m6 11-3.52 2.147a1 1 0 0 0-.48.854V19a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5a1 1 0 0 0-.48-.853L18 11"}],["path",{d:"M6 5v16"}],["circle",{cx:"12",cy:"9",r:"2"}]],l$=[["path",{d:"M5.42 9.42 8 12"}],["circle",{cx:"4",cy:"8",r:"2"}],["path",{d:"m14 6-8.58 8.58"}],["circle",{cx:"4",cy:"16",r:"2"}],["path",{d:"M10.8 14.8 14 18"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}]],e$=[["circle",{cx:"6",cy:"6",r:"3"}],["path",{d:"M8.12 8.12 12 12"}],["path",{d:"M20 4 8.12 15.88"}],["circle",{cx:"6",cy:"18",r:"3"}],["path",{d:"M14.8 14.8 20 20"}]],r$=[["path",{d:"M21 4h-3.5l2 11.05"}],["path",{d:"M6.95 17h5.142c.523 0 .95-.406 1.063-.916a6.5 6.5 0 0 1 5.345-5.009"}],["circle",{cx:"19.5",cy:"17.5",r:"2.5"}],["circle",{cx:"4.5",cy:"17.5",r:"2.5"}]],o$=[["path",{d:"M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m22 3-5 5"}],["path",{d:"m17 3 5 5"}]],v$=[["path",{d:"M13 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-3"}],["path",{d:"M8 21h8"}],["path",{d:"M12 17v4"}],["path",{d:"m17 8 5-5"}],["path",{d:"M17 3h5v5"}]],$$=[["path",{d:"M15 12h-5"}],["path",{d:"M15 8h-5"}],["path",{d:"M19 17V5a2 2 0 0 0-2-2H4"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"}]],m$=[["path",{d:"M19 17V5a2 2 0 0 0-2-2H4"}],["path",{d:"M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"}]],y$=[["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M11 7v4"}],["path",{d:"M11 15h.01"}]],s$=[["path",{d:"m13 13.5 2-2.5-2-2.5"}],["path",{d:"m21 21-4.3-4.3"}],["path",{d:"M9 8.5 7 11l2 2.5"}],["circle",{cx:"11",cy:"11",r:"8"}]],g$=[["path",{d:"m8 11 2 2 4-4"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],C$=[["path",{d:"m13.5 8.5-5 5"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],u$=[["path",{d:"m13.5 8.5-5 5"}],["path",{d:"m8.5 8.5 5 5"}],["circle",{cx:"11",cy:"11",r:"8"}],["path",{d:"m21 21-4.3-4.3"}]],H$=[["path",{d:"m21 21-4.34-4.34"}],["circle",{cx:"11",cy:"11",r:"8"}]],_2=[["path",{d:"M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"}],["path",{d:"M6 12h16"}]],A$=[["path",{d:"M16 5a4 3 0 0 0-8 0c0 4 8 3 8 7a4 3 0 0 1-8 0"}],["path",{d:"M8 19a4 3 0 0 0 8 0c0-4-8-3-8-7a4 3 0 0 1 8 0"}]],w$=[["rect",{x:"14",y:"14",width:"8",height:"8",rx:"2"}],["rect",{x:"2",y:"2",width:"8",height:"8",rx:"2"}],["path",{d:"M7 14v1a2 2 0 0 0 2 2h1"}],["path",{d:"M14 7h1a2 2 0 0 1 2 2v1"}]],V$=[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"}],["path",{d:"m21.854 2.147-10.94 10.939"}]],S$=[["path",{d:"m16 16-4 4-4-4"}],["path",{d:"M3 12h18"}],["path",{d:"m8 8 4-4 4 4"}]],L$=[["path",{d:"M12 3v18"}],["path",{d:"m16 16 4-4-4-4"}],["path",{d:"m8 8-4 4 4 4"}]],f$=[["path",{d:"m10.852 14.772-.383.923"}],["path",{d:"M13.148 14.772a3 3 0 1 0-2.296-5.544l-.383-.923"}],["path",{d:"m13.148 9.228.383-.923"}],["path",{d:"m13.53 15.696-.382-.924a3 3 0 1 1-2.296-5.544"}],["path",{d:"m14.772 10.852.923-.383"}],["path",{d:"m14.772 13.148.923.383"}],["path",{d:"M4.5 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-.5"}],["path",{d:"M4.5 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-.5"}],["path",{d:"M6 18h.01"}],["path",{d:"M6 6h.01"}],["path",{d:"m9.228 10.852-.923-.383"}],["path",{d:"m9.228 13.148-.923.383"}]],k$=[["path",{d:"M6 10H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2"}],["path",{d:"M6 14H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2h-2"}],["path",{d:"M6 6h.01"}],["path",{d:"M6 18h.01"}],["path",{d:"m13 6-4 6h6l-4 6"}]],P$=[["path",{d:"M7 2h13a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-5"}],["path",{d:"M10 10 2.5 2.5C2 2 2 2.5 2 5v3a2 2 0 0 0 2 2h6z"}],["path",{d:"M22 17v-1a2 2 0 0 0-2-2h-1"}],["path",{d:"M4 14a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h16.5l1-.5.5.5-8-8H4z"}],["path",{d:"M6 18h.01"}],["path",{d:"m2 2 20 20"}]],B$=[["rect",{width:"20",height:"8",x:"2",y:"2",rx:"2",ry:"2"}],["rect",{width:"20",height:"8",x:"2",y:"14",rx:"2",ry:"2"}],["line",{x1:"6",x2:"6.01",y1:"6",y2:"6"}],["line",{x1:"6",x2:"6.01",y1:"18",y2:"18"}]],z$=[["path",{d:"M14 17H5"}],["path",{d:"M19 7h-9"}],["circle",{cx:"17",cy:"17",r:"3"}],["circle",{cx:"7",cy:"7",r:"3"}]],F$=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"}],["circle",{cx:"12",cy:"12",r:"3"}]],D$=[["path",{d:"M8.3 10a.7.7 0 0 1-.626-1.079L11.4 3a.7.7 0 0 1 1.198-.043L16.3 8.9a.7.7 0 0 1-.572 1.1Z"}],["rect",{x:"3",y:"14",width:"7",height:"7",rx:"1"}],["circle",{cx:"17.5",cy:"17.5",r:"3.5"}]],b$=[["circle",{cx:"18",cy:"5",r:"3"}],["circle",{cx:"6",cy:"12",r:"3"}],["circle",{cx:"18",cy:"19",r:"3"}],["line",{x1:"8.59",x2:"15.42",y1:"13.51",y2:"17.49"}],["line",{x1:"15.41",x2:"8.59",y1:"6.51",y2:"10.49"}]],R$=[["path",{d:"M12 2v13"}],["path",{d:"m16 6-4-4-4 4"}],["path",{d:"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"}]],T$=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["line",{x1:"3",x2:"21",y1:"9",y2:"9"}],["line",{x1:"3",x2:"21",y1:"15",y2:"15"}],["line",{x1:"9",x2:"9",y1:"9",y2:"21"}],["line",{x1:"15",x2:"15",y1:"9",y2:"21"}]],q$=[["path",{d:"M14 11a2 2 0 1 1-4 0 4 4 0 0 1 8 0 6 6 0 0 1-12 0 8 8 0 0 1 16 0 10 10 0 1 1-20 0 11.93 11.93 0 0 1 2.42-7.22 2 2 0 1 1 3.16 2.44"}]],U$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M12 8v4"}],["path",{d:"M12 16h.01"}]],O$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m4.243 5.21 14.39 12.472"}]],Z$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m9 12 2 2 4-4"}]],G$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M8 12h.01"}],["path",{d:"M12 12h.01"}],["path",{d:"M16 12h.01"}]],W$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M12 22V2"}]],I$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9 12h6"}]],E$=[["path",{d:"m2 2 20 20"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264"}]],X$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9 12h6"}],["path",{d:"M12 9v6"}]],x2=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3"}],["path",{d:"M12 17h.01"}]],j$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"M6.376 18.91a6 6 0 0 1 11.249.003"}],["circle",{cx:"12",cy:"11",r:"4"}]],a0=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m14.5 9.5-5 5"}],["path",{d:"m9.5 9.5 5 5"}]],N$=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}]],K$=[["circle",{cx:"12",cy:"12",r:"8"}],["path",{d:"M12 2v7.5"}],["path",{d:"m19 5-5.23 5.23"}],["path",{d:"M22 12h-7.5"}],["path",{d:"m19 19-5.23-5.23"}],["path",{d:"M12 14.5V22"}],["path",{d:"M10.23 13.77 5 19"}],["path",{d:"M9.5 12H2"}],["path",{d:"M10.23 10.23 5 5"}],["circle",{cx:"12",cy:"12",r:"2.5"}]],Q$=[["path",{d:"M12 10.189V14"}],["path",{d:"M12 2v3"}],["path",{d:"M19 13V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v6"}],["path",{d:"M19.38 20A11.6 11.6 0 0 0 21 14l-8.188-3.639a2 2 0 0 0-1.624 0L3 14a11.6 11.6 0 0 0 2.81 7.76"}],["path",{d:"M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1s1.2 1 2.5 1c2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}]],J$=[["path",{d:"M20.38 3.46 16 2a4 4 0 0 1-8 0L3.62 3.46a2 2 0 0 0-1.34 2.23l.58 3.47a1 1 0 0 0 .99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 0 0 2-2V10h2.15a1 1 0 0 0 .99-.84l.58-3.47a2 2 0 0 0-1.34-2.23z"}]],Y$=[["path",{d:"M16 10a4 4 0 0 1-8 0"}],["path",{d:"M3.103 6.034h17.794"}],["path",{d:"M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z"}]],_$=[["path",{d:"m15 11-1 9"}],["path",{d:"m19 11-4-7"}],["path",{d:"M2 11h20"}],["path",{d:"m3.5 11 1.6 7.4a2 2 0 0 0 2 1.6h9.8a2 2 0 0 0 2-1.6l1.7-7.4"}],["path",{d:"M4.5 15.5h15"}],["path",{d:"m5 11 4-7"}],["path",{d:"m9 11 1 9"}]],x$=[["circle",{cx:"8",cy:"21",r:"1"}],["circle",{cx:"19",cy:"21",r:"1"}],["path",{d:"M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"}]],am=[["path",{d:"M21.56 4.56a1.5 1.5 0 0 1 0 2.122l-.47.47a3 3 0 0 1-4.212-.03 3 3 0 0 1 0-4.243l.44-.44a1.5 1.5 0 0 1 2.121 0z"}],["path",{d:"M3 22a1 1 0 0 1-1-1v-3.586a1 1 0 0 1 .293-.707l3.355-3.355a1.205 1.205 0 0 1 1.704 0l3.296 3.296a1.205 1.205 0 0 1 0 1.704l-3.355 3.355a1 1 0 0 1-.707.293z"}],["path",{d:"m9 15 7.879-7.878"}]],tm=[["path",{d:"m4 4 2.5 2.5"}],["path",{d:"M13.5 6.5a4.95 4.95 0 0 0-7 7"}],["path",{d:"M15 5 5 15"}],["path",{d:"M14 17v.01"}],["path",{d:"M10 16v.01"}],["path",{d:"M13 13v.01"}],["path",{d:"M16 10v.01"}],["path",{d:"M11 20v.01"}],["path",{d:"M17 14v.01"}],["path",{d:"M20 11v.01"}]],hm=[["path",{d:"M4 13V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v5"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5"}],["path",{d:"M10 22v-5"}],["path",{d:"M14 19v-2"}],["path",{d:"M18 20v-3"}],["path",{d:"M2 13h20"}],["path",{d:"M6 20v-3"}]],dm=[["path",{d:"M11 12h.01"}],["path",{d:"M13 22c.5-.5 1.12-1 2.5-1-1.38 0-2-.5-2.5-1"}],["path",{d:"M14 2a3.28 3.28 0 0 1-3.227 1.798l-6.17-.561A2.387 2.387 0 1 0 4.387 8H15.5a1 1 0 0 1 0 13 1 1 0 0 0 0-5H12a7 7 0 0 1-7-7V8"}],["path",{d:"M14 8a8.5 8.5 0 0 1 0 8"}],["path",{d:"M16 16c2 0 4.5-4 4-6"}]],cm=[["path",{d:"m15 15 6 6m-6-6v4.8m0-4.8h4.8"}],["path",{d:"M9 19.8V15m0 0H4.2M9 15l-6 6"}],["path",{d:"M15 4.2V9m0 0h4.8M15 9l6-6"}],["path",{d:"M9 4.2V9m0 0H4.2M9 9 3 3"}]],Mm=[["path",{d:"M12 22v-5.172a2 2 0 0 0-.586-1.414L9.5 13.5"}],["path",{d:"M14.5 14.5 12 17"}],["path",{d:"M17 8.8A6 6 0 0 1 13.8 20H10A6.5 6.5 0 0 1 7 8a5 5 0 0 1 10 0z"}]],pm=[["path",{d:"m18 14 4 4-4 4"}],["path",{d:"m18 2 4 4-4 4"}],["path",{d:"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22"}],["path",{d:"M2 6h1.972a4 4 0 0 1 3.6 2.2"}],["path",{d:"M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45"}]],im=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}],["path",{d:"M17 20V8"}]],nm=[["path",{d:"M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2"}]],lm=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}]],em=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}]],rm=[["path",{d:"M2 20h.01"}]],om=[["path",{d:"M2 20h.01"}],["path",{d:"M7 20v-4"}],["path",{d:"M12 20v-8"}],["path",{d:"M17 20V8"}],["path",{d:"M22 4v16"}]],vm=[["path",{d:"m21 17-2.156-1.868A.5.5 0 0 0 18 15.5v.5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1c0-2.545-3.991-3.97-8.5-4a1 1 0 0 0 0 5c4.153 0 4.745-11.295 5.708-13.5a2.5 2.5 0 1 1 3.31 3.284"}],["path",{d:"M3 21h18"}]],$m=[["path",{d:"M10 9H4L2 7l2-2h6"}],["path",{d:"M14 5h6l2 2-2 2h-6"}],["path",{d:"M10 22V4a2 2 0 1 1 4 0v18"}],["path",{d:"M8 22h8"}]],mm=[["path",{d:"M12 13v8"}],["path",{d:"M12 3v3"}],["path",{d:"M18 6a2 2 0 0 1 1.387.56l2.307 2.22a1 1 0 0 1 0 1.44l-2.307 2.22A2 2 0 0 1 18 13H6a2 2 0 0 1-1.387-.56l-2.306-2.22a1 1 0 0 1 0-1.44l2.306-2.22A2 2 0 0 1 6 6z"}]],ym=[["path",{d:"M7 18v-6a5 5 0 1 1 10 0v6"}],["path",{d:"M5 21a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2z"}],["path",{d:"M21 12h1"}],["path",{d:"M18.5 4.5 18 5"}],["path",{d:"M2 12h1"}],["path",{d:"M12 2v1"}],["path",{d:"m4.929 4.929.707.707"}],["path",{d:"M12 12v6"}]],sm=[["path",{d:"M17.971 4.285A2 2 0 0 1 21 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z"}],["path",{d:"M3 20V4"}]],gm=[["path",{d:"m12.5 17-.5-1-.5 1h1z"}],["path",{d:"M15 22a1 1 0 0 0 1-1v-1a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20v1a1 1 0 0 0 1 1z"}],["circle",{cx:"15",cy:"12",r:"1"}],["circle",{cx:"9",cy:"12",r:"1"}]],Cm=[["path",{d:"M21 4v16"}],["path",{d:"M6.029 4.285A2 2 0 0 0 3 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"}]],um=[["rect",{width:"3",height:"8",x:"13",y:"2",rx:"1.5"}],["path",{d:"M19 8.5V10h1.5A1.5 1.5 0 1 0 19 8.5"}],["rect",{width:"3",height:"8",x:"8",y:"14",rx:"1.5"}],["path",{d:"M5 15.5V14H3.5A1.5 1.5 0 1 0 5 15.5"}],["rect",{width:"8",height:"3",x:"14",y:"13",rx:"1.5"}],["path",{d:"M15.5 19H14v1.5a1.5 1.5 0 1 0 1.5-1.5"}],["rect",{width:"8",height:"3",x:"2",y:"8",rx:"1.5"}],["path",{d:"M8.5 5H10V3.5A1.5 1.5 0 1 0 8.5 5"}]],Hm=[["path",{d:"M22 2 2 22"}]],Am=[["path",{d:"M11 16.586V19a1 1 0 0 1-1 1H2L18.37 3.63a1 1 0 1 1 3 3l-9.663 9.663a1 1 0 0 1-1.414 0L8 14"}]],wm=[["path",{d:"M10 5H3"}],["path",{d:"M12 19H3"}],["path",{d:"M14 3v4"}],["path",{d:"M16 17v4"}],["path",{d:"M21 12h-9"}],["path",{d:"M21 19h-5"}],["path",{d:"M21 5h-7"}],["path",{d:"M8 10v4"}],["path",{d:"M8 12H3"}]],t0=[["path",{d:"M10 8h4"}],["path",{d:"M12 21v-9"}],["path",{d:"M12 8V3"}],["path",{d:"M17 16h4"}],["path",{d:"M19 12V3"}],["path",{d:"M19 21v-5"}],["path",{d:"M3 14h4"}],["path",{d:"M5 10V3"}],["path",{d:"M5 21v-7"}]],Vm=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2"}],["path",{d:"M12.667 8 10 12h4l-2.667 4"}]],Sm=[["rect",{width:"7",height:"12",x:"2",y:"6",rx:"1"}],["path",{d:"M13 8.32a7.43 7.43 0 0 1 0 7.36"}],["path",{d:"M16.46 6.21a11.76 11.76 0 0 1 0 11.58"}],["path",{d:"M19.91 4.1a15.91 15.91 0 0 1 .01 15.8"}]],Lm=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2"}],["path",{d:"M12 18h.01"}]],fm=[["path",{d:"M22 11v1a10 10 0 1 1-9-10"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}],["path",{d:"M16 5h6"}],["path",{d:"M19 2v6"}]],km=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M8 14s1.5 2 4 2 4-2 4-2"}],["line",{x1:"9",x2:"9.01",y1:"9",y2:"9"}],["line",{x1:"15",x2:"15.01",y1:"9",y2:"9"}]],Pm=[["path",{d:"M2 13a6 6 0 1 0 12 0 4 4 0 1 0-8 0 2 2 0 0 0 4 0"}],["circle",{cx:"10",cy:"13",r:"8"}],["path",{d:"M2 21h12c4.4 0 8-3.6 8-8V7a2 2 0 1 0-4 0v6"}],["path",{d:"M18 3 19.1 5.2"}],["path",{d:"M22 3 20.9 5.2"}]],Bm=[["path",{d:"m10 20-1.25-2.5L6 18"}],["path",{d:"M10 4 8.75 6.5 6 6"}],["path",{d:"m14 20 1.25-2.5L18 18"}],["path",{d:"m14 4 1.25 2.5L18 6"}],["path",{d:"m17 21-3-6h-4"}],["path",{d:"m17 3-3 6 1.5 3"}],["path",{d:"M2 12h6.5L10 9"}],["path",{d:"m20 10-1.5 2 1.5 2"}],["path",{d:"M22 12h-6.5L14 15"}],["path",{d:"m4 10 1.5 2L4 14"}],["path",{d:"m7 21 3-6-1.5-3"}],["path",{d:"m7 3 3 6h4"}]],zm=[["path",{d:"M10.5 2v4"}],["path",{d:"M14 2H7a2 2 0 0 0-2 2"}],["path",{d:"M19.29 14.76A6.67 6.67 0 0 1 17 11a6.6 6.6 0 0 1-2.29 3.76c-1.15.92-1.71 2.04-1.71 3.19 0 2.22 1.8 4.05 4 4.05s4-1.83 4-4.05c0-1.16-.57-2.26-1.71-3.19"}],["path",{d:"M9.607 21H6a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h7V7a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3"}]],Fm=[["path",{d:"M20 9V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3"}],["path",{d:"M2 16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5a2 2 0 0 0-4 0v1.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V11a2 2 0 0 0-4 0z"}],["path",{d:"M4 18v2"}],["path",{d:"M20 18v2"}],["path",{d:"M12 4v9"}]],Dm=[["path",{d:"M11 2h2"}],["path",{d:"m14.28 14-4.56 8"}],["path",{d:"m21 22-1.558-4H4.558"}],["path",{d:"M3 10v2"}],["path",{d:"M6.245 15.04A2 2 0 0 1 8 14h12a1 1 0 0 1 .864 1.505l-3.11 5.457A2 2 0 0 1 16 22H4a1 1 0 0 1-.863-1.506z"}],["path",{d:"M7 2a4 4 0 0 1-4 4"}],["path",{d:"m8.66 7.66 1.41 1.41"}]],bm=[["path",{d:"M12 21a9 9 0 0 0 9-9H3a9 9 0 0 0 9 9Z"}],["path",{d:"M7 21h10"}],["path",{d:"M19.5 12 22 6"}],["path",{d:"M16.25 3c.27.1.8.53.75 1.36-.06.83-.93 1.2-1 2.02-.05.78.34 1.24.73 1.62"}],["path",{d:"M11.25 3c.27.1.8.53.74 1.36-.05.83-.93 1.2-.98 2.02-.06.78.33 1.24.72 1.62"}],["path",{d:"M6.25 3c.27.1.8.53.75 1.36-.06.83-.93 1.2-1 2.02-.05.78.34 1.24.74 1.62"}]],Rm=[["path",{d:"M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1"}]],Tm=[["path",{d:"M12 18v4"}],["path",{d:"M2 14.499a5.5 5.5 0 0 0 9.591 3.675.6.6 0 0 1 .818.001A5.5 5.5 0 0 0 22 14.5c0-2.29-1.5-4-3-5.5l-5.492-5.312a2 2 0 0 0-3-.02L5 8.999c-1.5 1.5-3 3.2-3 5.5"}]],qm=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"}]],h0=[["path",{d:"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"}],["path",{d:"M20 2v4"}],["path",{d:"M22 4h-4"}],["circle",{cx:"4",cy:"20",r:"2"}]],Um=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2"}],["path",{d:"M12 6h.01"}],["circle",{cx:"12",cy:"14",r:"4"}],["path",{d:"M12 14h.01"}]],Om=[["path",{d:"M8.8 20v-4.1l1.9.2a2.3 2.3 0 0 0 2.164-2.1V8.3A5.37 5.37 0 0 0 2 8.25c0 2.8.656 3.054 1 4.55a5.77 5.77 0 0 1 .029 2.758L2 20"}],["path",{d:"M19.8 17.8a7.5 7.5 0 0 0 .003-10.603"}],["path",{d:"M17 15a3.5 3.5 0 0 0-.025-4.975"}]],Zm=[["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}],["path",{d:"M4 21c1.1 0 1.1-1 2.3-1s1.1 1 2.3 1c1.1 0 1.1-1 2.3-1 1.1 0 1.1 1 2.3 1 1.1 0 1.1-1 2.3-1 1.1 0 1.1 1 2.3 1 1.1 0 1.1-1 2.3-1"}]],Gm=[["path",{d:"m6 16 6-12 6 12"}],["path",{d:"M8 12h8"}],["path",{d:"m16 20 2 2 4-4"}]],Wm=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M5 17A12 12 0 0 1 17 5"}],["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}]],Im=[["circle",{cx:"19",cy:"5",r:"2"}],["circle",{cx:"5",cy:"19",r:"2"}],["path",{d:"M5 17A12 12 0 0 1 17 5"}]],Em=[["path",{d:"M16 3h5v5"}],["path",{d:"M8 3H3v5"}],["path",{d:"M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"}],["path",{d:"m15 9 6-6"}]],Xm=[["path",{d:"M17 13.44 4.442 17.082A2 2 0 0 0 4.982 21H19a2 2 0 0 0 .558-3.921l-1.115-.32A2 2 0 0 1 17 14.837V7.66"}],["path",{d:"m7 10.56 12.558-3.642A2 2 0 0 0 19.018 3H5a2 2 0 0 0-.558 3.921l1.115.32A2 2 0 0 1 7 9.163v7.178"}]],jm=[["path",{d:"M15.295 19.562 16 22"}],["path",{d:"m17 16 3.758 2.098"}],["path",{d:"m19 12.5 3.026-.598"}],["path",{d:"M7.61 6.3a3 3 0 0 0-3.92 1.3l-1.38 2.79a3 3 0 0 0 1.3 3.91l6.89 3.597a1 1 0 0 0 1.342-.447l3.106-6.211a1 1 0 0 0-.447-1.341z"}],["path",{d:"M8 9V2"}]],Nm=[["path",{d:"M3 3h.01"}],["path",{d:"M7 5h.01"}],["path",{d:"M11 7h.01"}],["path",{d:"M3 7h.01"}],["path",{d:"M7 9h.01"}],["path",{d:"M3 11h.01"}],["rect",{width:"4",height:"4",x:"15",y:"5"}],["path",{d:"m19 9 2 2v10c0 .6-.4 1-1 1h-6c-.6 0-1-.4-1-1V11l2-2"}],["path",{d:"m13 14 8-2"}],["path",{d:"m13 19 8-2"}]],Km=[["path",{d:"M14 9.536V7a4 4 0 0 1 4-4h1.5a.5.5 0 0 1 .5.5V5a4 4 0 0 1-4 4 4 4 0 0 0-4 4c0 2 1 3 1 5a5 5 0 0 1-1 3"}],["path",{d:"M4 9a5 5 0 0 1 8 4 5 5 0 0 1-8-4"}],["path",{d:"M5 21h14"}]],d0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M17 12h-2l-2 5-2-10-2 5H7"}]],c0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 8-8 8"}],["path",{d:"M16 16H8V8"}]],M0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m8 8 8 8"}],["path",{d:"M16 8v8H8"}]],p0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 8v8"}],["path",{d:"m8 12 4 4 4-4"}]],i0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m12 8-4 4 4 4"}],["path",{d:"M16 12H8"}]],n0=[["path",{d:"M13 21h6a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6"}],["path",{d:"m3 21 9-9"}],["path",{d:"M9 21H3v-6"}]],l0=[["path",{d:"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}],["path",{d:"m21 21-9-9"}],["path",{d:"M21 15v6h-6"}]],e0=[["path",{d:"M13 3h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6"}],["path",{d:"m3 3 9 9"}],["path",{d:"M3 9V3h6"}]],r0=[["path",{d:"M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"}],["path",{d:"m21 3-9 9"}],["path",{d:"M15 3h6v6"}]],o0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}],["path",{d:"m12 16 4-4-4-4"}]],v0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 16V8h8"}],["path",{d:"M16 16 8 8"}]],$0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 8h8v8"}],["path",{d:"m8 16 8-8"}]],m0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 12-4-4-4 4"}],["path",{d:"M12 16V8"}]],y0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 8v8"}],["path",{d:"m8.5 14 7-4"}],["path",{d:"m8.5 10 7 4"}]],s0=[["line",{x1:"5",y1:"3",x2:"19",y2:"3"}],["line",{x1:"3",y1:"5",x2:"3",y2:"19"}],["line",{x1:"21",y1:"5",x2:"21",y2:"19"}],["line",{x1:"9",y1:"21",x2:"10",y2:"21"}],["line",{x1:"14",y1:"21",x2:"15",y2:"21"}],["path",{d:"M 3 5 A2 2 0 0 1 5 3"}],["path",{d:"M 19 3 A2 2 0 0 1 21 5"}],["path",{d:"M 5 21 A2 2 0 0 1 3 19"}],["path",{d:"M 21 19 A2 2 0 0 1 19 21"}],["circle",{cx:"8.5",cy:"8.5",r:"1.5"}],["line",{x1:"9.56066",y1:"9.56066",x2:"12",y2:"12"}],["line",{x1:"17",y1:"17",x2:"14.82",y2:"14.82"}],["circle",{cx:"8.5",cy:"15.5",r:"1.5"}],["line",{x1:"9.56066",y1:"14.43934",x2:"17",y2:"7"}]],m=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 8h7"}],["path",{d:"M8 12h6"}],["path",{d:"M11 16h5"}]],g0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m9 12 2 2 4-4"}]],C0=[["path",{d:"M21 10.656V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h12.344"}],["path",{d:"m9 11 3 3L22 4"}]],u0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m16 10-4 4-4-4"}]],H0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m14 16-4-4 4-4"}]],A0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m10 8 4 4-4 4"}]],w0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m8 14 4-4 4 4"}]],V0=[["path",{d:"m10 9-3 3 3 3"}],["path",{d:"m14 15 3-3-3-3"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],Qm=[["path",{d:"M10 9.5 8 12l2 2.5"}],["path",{d:"M14 21h1"}],["path",{d:"m14 9.5 2 2.5-2 2.5"}],["path",{d:"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2"}],["path",{d:"M9 21h1"}]],Jm=[["path",{d:"M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2"}],["path",{d:"M9 21h1"}],["path",{d:"M14 21h1"}]],S0=[["path",{d:"M8 7v7"}],["path",{d:"M12 7v4"}],["path",{d:"M16 7v9"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M9 3h1"}],["path",{d:"M14 3h1"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M14 21h1"}],["path",{d:"M9 21h1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M3 14v1"}],["path",{d:"M3 9v1"}]],L0=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 3h1"}],["path",{d:"M9 21h2"}],["path",{d:"M14 3h1"}],["path",{d:"M3 9v1"}],["path",{d:"M21 9v2"}],["path",{d:"M3 14v1"}]],Ym=[["path",{d:"M14 21h1"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"}],["path",{d:"M3 9v1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 21h1"}]],f0=[["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M9 3h1"}],["path",{d:"M9 21h1"}],["path",{d:"M14 3h1"}],["path",{d:"M14 21h1"}],["path",{d:"M3 9v1"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M21 14v1"}]],k0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["line",{x1:"8",x2:"16",y1:"12",y2:"12"}],["line",{x1:"12",x2:"12",y1:"16",y2:"16"}],["line",{x1:"12",x2:"12",y1:"8",y2:"8"}]],P0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"12",r:"1"}]],B0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 10h10"}],["path",{d:"M7 14h10"}]],z0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"M9 17c2 0 2.8-1 2.8-2.8V10c0-2 1-3.3 3.2-3"}],["path",{d:"M9 11.2h5.7"}]],F0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 7v7"}],["path",{d:"M12 7v4"}],["path",{d:"M16 7v9"}]],D0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7v10"}],["path",{d:"M11 7v10"}],["path",{d:"m15 7 2 10"}]],b0=[["path",{d:"M8 16V8.5a.5.5 0 0 1 .9-.3l2.7 3.599a.5.5 0 0 0 .8 0l2.7-3.6a.5.5 0 0 1 .9.3V16"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],R0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 8h10"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h10"}]],T0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}]],q0=[["path",{d:"M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"}],["path",{d:"M21 11V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h6"}]],U0=[["path",{d:"M3.6 3.6A2 2 0 0 1 5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-.59 1.41"}],["path",{d:"M3 8.7V19a2 2 0 0 0 2 2h10.3"}],["path",{d:"m2 2 20 20"}],["path",{d:"M13 13a3 3 0 1 0 0-6H9v2"}],["path",{d:"M9 17v-2.3"}]],O0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M9 17V7h4a3 3 0 0 1 0 6H9"}]],i=[["path",{d:"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"}],["path",{d:"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"}]],_m=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["line",{x1:"10",x2:"10",y1:"15",y2:"9"}],["line",{x1:"14",x2:"14",y1:"15",y2:"9"}]],Z0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"m15 9-6 6"}],["path",{d:"M9 9h.01"}],["path",{d:"M15 15h.01"}]],G0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M7 7h10"}],["path",{d:"M10 7v10"}],["path",{d:"M16 17a2 2 0 0 1-2-2V7"}]],W0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M12 12H9.5a2.5 2.5 0 0 1 0-5H17"}],["path",{d:"M12 7v10"}],["path",{d:"M16 7v10"}]],I0=[["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}],["path",{d:"M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z"}]],E0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M8 12h8"}],["path",{d:"M12 8v8"}]],X0=[["path",{d:"M12 7v4"}],["path",{d:"M7.998 9.003a5 5 0 1 0 8-.005"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],xm=[["path",{d:"M7 12h2l2 5 2-10h4"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],j0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"8.5",cy:"8.5",r:"1.5"}],["line",{x1:"9.56066",y1:"9.56066",x2:"12",y2:"12"}],["line",{x1:"17",y1:"17",x2:"14.82",y2:"14.82"}],["circle",{cx:"8.5",cy:"15.5",r:"1.5"}],["line",{x1:"9.56066",y1:"14.43934",x2:"17",y2:"7"}]],ay=[["path",{d:"M21 11a8 8 0 0 0-8-8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"}]],N0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M16 8.9V7H8l4 5-4 5h8v-1.9"}]],K0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["line",{x1:"9",x2:"15",y1:"15",y2:"9"}]],Q0=[["path",{d:"M8 19H5c-1 0-2-1-2-2V7c0-1 1-2 2-2h3"}],["path",{d:"M16 5h3c1 0 2 1 2 2v10c0 1-1 2-2 2h-3"}],["line",{x1:"12",x2:"12",y1:"4",y2:"20"}]],J0=[["path",{d:"M5 8V5c0-1 1-2 2-2h10c1 0 2 1 2 2v3"}],["path",{d:"M19 16v3c0 1-1 2-2 2H7c-1 0-2-1-2-2v-3"}],["line",{x1:"4",x2:"20",y1:"12",y2:"12"}]],ty=[["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1"}]],hy=[["path",{d:"M4 10c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"}],["path",{d:"M10 16c-1.1 0-2-.9-2-2v-4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"}],["rect",{width:"8",height:"8",x:"14",y:"14",rx:"2"}]],dy=[["path",{d:"M11.035 7.69a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.866l-1.156-1.153a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["rect",{x:"3",y:"3",width:"18",height:"18",rx:"2"}]],cy=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["rect",{x:"9",y:"9",width:"6",height:"6",rx:"1"}]],Y0=[["path",{d:"m7 11 2-2-2-2"}],["path",{d:"M11 13h4"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}]],_0=[["path",{d:"M18 21a6 6 0 0 0-12 0"}],["circle",{cx:"12",cy:"11",r:"4"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],x0=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 21v-2a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2"}]],aa=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]],My=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],py=[["path",{d:"M16 12v2a2 2 0 0 1-2 2H9a1 1 0 0 0-1 1v3a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2h0"}],["path",{d:"M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v3a1 1 0 0 1-1 1h-5a2 2 0 0 0-2 2v2"}]],iy=[["path",{d:"M10 22a2 2 0 0 1-2-2"}],["path",{d:"M14 2a2 2 0 0 1 2 2"}],["path",{d:"M16 22h-2"}],["path",{d:"M2 10V8"}],["path",{d:"M2 4a2 2 0 0 1 2-2"}],["path",{d:"M20 8a2 2 0 0 1 2 2"}],["path",{d:"M22 14v2"}],["path",{d:"M22 20a2 2 0 0 1-2 2"}],["path",{d:"M4 16a2 2 0 0 1-2-2"}],["path",{d:"M8 10a2 2 0 0 1 2-2h5a1 1 0 0 1 1 1v5a2 2 0 0 1-2 2H9a1 1 0 0 1-1-1z"}],["path",{d:"M8 2h2"}]],ny=[["path",{d:"M10 22a2 2 0 0 1-2-2"}],["path",{d:"M16 22h-2"}],["path",{d:"M16 4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-5a2 2 0 0 1 2-2h5a1 1 0 0 0 1-1z"}],["path",{d:"M20 8a2 2 0 0 1 2 2"}],["path",{d:"M22 14v2"}],["path",{d:"M22 20a2 2 0 0 1-2 2"}]],ly=[["path",{d:"M13.77 3.043a34 34 0 0 0-3.54 0"}],["path",{d:"M13.771 20.956a33 33 0 0 1-3.541.001"}],["path",{d:"M20.18 17.74c-.51 1.15-1.29 1.93-2.439 2.44"}],["path",{d:"M20.18 6.259c-.51-1.148-1.291-1.929-2.44-2.438"}],["path",{d:"M20.957 10.23a33 33 0 0 1 0 3.54"}],["path",{d:"M3.043 10.23a34 34 0 0 0 .001 3.541"}],["path",{d:"M6.26 20.179c-1.15-.508-1.93-1.29-2.44-2.438"}],["path",{d:"M6.26 3.82c-1.149.51-1.93 1.291-2.44 2.44"}]],ey=[["path",{d:"M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v3a1 1 0 0 0 1 1h3a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2v-3a1 1 0 0 0-1-1z"}]],ry=[["path",{d:"M12 3c7.2 0 9 1.8 9 9s-1.8 9-9 9-9-1.8-9-9 1.8-9 9-9"}]],oy=[["path",{d:"M15.236 22a3 3 0 0 0-2.2-5"}],["path",{d:"M16 20a3 3 0 0 1 3-3h1a2 2 0 0 0 2-2v-2a4 4 0 0 0-4-4V4"}],["path",{d:"M18 13h.01"}],["path",{d:"M18 6a4 4 0 0 0-4 4 7 7 0 0 0-7 7c0-5 4-5 4-10.5a4.5 4.5 0 1 0-9 0 2.5 2.5 0 0 0 5 0C7 10 3 11 3 17c0 2.8 2.2 5 5 5h10"}]],vy=[["path",{d:"M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-6 0c0 2 1 2 1 3.5V13"}],["path",{d:"M20 15.5a2.5 2.5 0 0 0-2.5-2.5h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1z"}],["path",{d:"M5 22h14"}]],$y=[["path",{d:"M8.34 8.34 2 9.27l5 4.87L5.82 21 12 17.77 18.18 21l-.59-3.43"}],["path",{d:"M18.42 12.76 22 9.27l-6.91-1L12 2l-1.44 2.91"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],my=[["path",{d:"M12 18.338a2.1 2.1 0 0 0-.987.244L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.12 2.12 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.12 2.12 0 0 0 1.597-1.16l2.309-4.679A.53.53 0 0 1 12 2"}]],yy=[["path",{d:"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"}]],sy=[["path",{d:"M13.971 4.285A2 2 0 0 1 17 6v12a2 2 0 0 1-3.029 1.715l-9.997-5.998a2 2 0 0 1-.003-3.432z"}],["path",{d:"M21 20V4"}]],gy=[["path",{d:"M10.029 4.285A2 2 0 0 0 7 6v12a2 2 0 0 0 3.029 1.715l9.997-5.998a2 2 0 0 0 .003-3.432z"}],["path",{d:"M3 4v16"}]],Cy=[["path",{d:"M11 2v2"}],["path",{d:"M5 2v2"}],["path",{d:"M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1"}],["path",{d:"M8 15a6 6 0 0 0 12 0v-3"}],["circle",{cx:"20",cy:"10",r:"2"}]],uy=[["path",{d:"M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z"}],["path",{d:"M15 3v5a1 1 0 0 0 1 1h5"}],["path",{d:"M8 13h.01"}],["path",{d:"M16 13h.01"}],["path",{d:"M10 16s.8 1 2 1c1.3 0 2-1 2-1"}]],Hy=[["path",{d:"M11.264 2.205A4 4 0 0 0 6.42 4.211l-4 8a4 4 0 0 0 1.359 5.117l6 4a4 4 0 0 0 4.438 0l6-4a4 4 0 0 0 1.576-4.592l-2-6a4 4 0 0 0-2.53-2.53z"}],["path",{d:"M11.99 22 14 12l7.822 3.184"}],["path",{d:"M14 12 8.47 2.302"}]],Ay=[["path",{d:"M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z"}],["path",{d:"M15 3v5a1 1 0 0 0 1 1h5"}]],wy=[["path",{d:"M15 21v-5a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v5"}],["path",{d:"M17.774 10.31a1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.451 0 1.12 1.12 0 0 0-1.548 0 2.5 2.5 0 0 1-3.452 0 1.12 1.12 0 0 0-1.549 0 2.5 2.5 0 0 1-3.77-3.248l2.889-4.184A2 2 0 0 1 7 2h10a2 2 0 0 1 1.653.873l2.895 4.192a2.5 2.5 0 0 1-3.774 3.244"}],["path",{d:"M4 10.95V19a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8.05"}]],Vy=[["rect",{width:"20",height:"6",x:"2",y:"4",rx:"2"}],["rect",{width:"20",height:"6",x:"2",y:"14",rx:"2"}]],Sy=[["rect",{width:"6",height:"20",x:"4",y:"2",rx:"2"}],["rect",{width:"6",height:"20",x:"14",y:"2",rx:"2"}]],Ly=[["path",{d:"M16 4H9a3 3 0 0 0-2.83 4"}],["path",{d:"M14 12a4 4 0 0 1 0 8H6"}],["line",{x1:"4",x2:"20",y1:"12",y2:"12"}]],fy=[["path",{d:"m4 5 8 8"}],["path",{d:"m12 5-8 8"}],["path",{d:"M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07"}]],ky=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 4h.01"}],["path",{d:"M20 12h.01"}],["path",{d:"M12 20h.01"}],["path",{d:"M4 12h.01"}],["path",{d:"M17.657 6.343h.01"}],["path",{d:"M17.657 17.657h.01"}],["path",{d:"M6.343 17.657h.01"}],["path",{d:"M6.343 6.343h.01"}]],Py=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 3v1"}],["path",{d:"M12 20v1"}],["path",{d:"M3 12h1"}],["path",{d:"M20 12h1"}],["path",{d:"m18.364 5.636-.707.707"}],["path",{d:"m6.343 17.657-.707.707"}],["path",{d:"m5.636 5.636.707.707"}],["path",{d:"m17.657 17.657.707.707"}]],By=[["path",{d:"M12 2v2"}],["path",{d:"M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715"}],["path",{d:"M16 12a4 4 0 0 0-4-4"}],["path",{d:"m19 5-1.256 1.256"}],["path",{d:"M20 12h2"}]],zy=[["path",{d:"M10 21v-1"}],["path",{d:"M10 4V3"}],["path",{d:"M10 9a3 3 0 0 0 0 6"}],["path",{d:"m14 20 1.25-2.5L18 18"}],["path",{d:"m14 4 1.25 2.5L18 6"}],["path",{d:"m17 21-3-6 1.5-3H22"}],["path",{d:"m17 3-3 6 1.5 3"}],["path",{d:"M2 12h1"}],["path",{d:"m20 10-1.5 2 1.5 2"}],["path",{d:"m3.64 18.36.7-.7"}],["path",{d:"m4.34 6.34-.7-.7"}]],Fy=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 2v2"}],["path",{d:"M12 20v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"m17.66 17.66 1.41 1.41"}],["path",{d:"M2 12h2"}],["path",{d:"M20 12h2"}],["path",{d:"m6.34 17.66-1.41 1.41"}],["path",{d:"m19.07 4.93-1.41 1.41"}]],Dy=[["path",{d:"M12 2v8"}],["path",{d:"m4.93 10.93 1.41 1.41"}],["path",{d:"M2 18h2"}],["path",{d:"M20 18h2"}],["path",{d:"m19.07 10.93-1.41 1.41"}],["path",{d:"M22 22H2"}],["path",{d:"m8 6 4-4 4 4"}],["path",{d:"M16 18a4 4 0 0 0-8 0"}]],by=[["path",{d:"M12 10V2"}],["path",{d:"m4.93 10.93 1.41 1.41"}],["path",{d:"M2 18h2"}],["path",{d:"M20 18h2"}],["path",{d:"m19.07 10.93-1.41 1.41"}],["path",{d:"M22 22H2"}],["path",{d:"m16 6-4 4-4-4"}],["path",{d:"M16 18a4 4 0 0 0-8 0"}]],Ry=[["path",{d:"m4 19 8-8"}],["path",{d:"m12 19-8-8"}],["path",{d:"M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 0 0-2.617-.436c-.42.239-.738.614-.899 1.06"}]],Ty=[["path",{d:"M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z"}],["path",{d:"M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7"}],["path",{d:"M 7 17h.01"}],["path",{d:"m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8"}]],qy=[["path",{d:"M10 21V3h8"}],["path",{d:"M6 16h9"}],["path",{d:"M10 9.5h7"}]],Uy=[["path",{d:"M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"}],["path",{d:"M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5"}],["circle",{cx:"12",cy:"12",r:"3"}],["path",{d:"m18 22-3-3 3-3"}],["path",{d:"m6 2 3 3-3 3"}]],Oy=[["path",{d:"m11 19-6-6"}],["path",{d:"m5 21-2-2"}],["path",{d:"m8 16-4 4"}],["path",{d:"M9.5 17.5 21 6V3h-3L6.5 14.5"}]],Zy=[["polyline",{points:"14.5 17.5 3 6 3 3 6 3 17.5 14.5"}],["line",{x1:"13",x2:"19",y1:"19",y2:"13"}],["line",{x1:"16",x2:"20",y1:"16",y2:"20"}],["line",{x1:"19",x2:"21",y1:"21",y2:"19"}],["polyline",{points:"14.5 6.5 18 3 21 3 21 6 17.5 9.5"}],["line",{x1:"5",x2:"9",y1:"14",y2:"18"}],["line",{x1:"7",x2:"4",y1:"17",y2:"20"}],["line",{x1:"3",x2:"5",y1:"19",y2:"21"}]],Gy=[["path",{d:"m18 2 4 4"}],["path",{d:"m17 7 3-3"}],["path",{d:"M19 9 8.7 19.3c-1 1-2.5 1-3.4 0l-.6-.6c-1-1-1-2.5 0-3.4L15 5"}],["path",{d:"m9 11 4 4"}],["path",{d:"m5 19-3 3"}],["path",{d:"m14 4 6 6"}]],Wy=[["path",{d:"M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"}]],Iy=[["path",{d:"M12 21v-6"}],["path",{d:"M12 9V3"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],Ey=[["path",{d:"M12 15V9"}],["path",{d:"M3 15h18"}],["path",{d:"M3 9h18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}]],Xy=[["path",{d:"M16 5H3"}],["path",{d:"M16 12H3"}],["path",{d:"M16 19H3"}],["path",{d:"M21 5h.01"}],["path",{d:"M21 12h.01"}],["path",{d:"M21 19h.01"}]],jy=[["path",{d:"M14 14v2"}],["path",{d:"M14 20v2"}],["path",{d:"M14 2v2"}],["path",{d:"M14 8v2"}],["path",{d:"M2 15h8"}],["path",{d:"M2 3h6a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H2"}],["path",{d:"M2 9h8"}],["path",{d:"M22 15h-4"}],["path",{d:"M22 3h-2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h2"}],["path",{d:"M22 9h-4"}],["path",{d:"M5 3v18"}]],Ny=[["path",{d:"M15 3v18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M21 9H3"}],["path",{d:"M21 15H3"}]],Ky=[["path",{d:"M14 10h2"}],["path",{d:"M15 22v-8"}],["path",{d:"M15 2v4"}],["path",{d:"M2 10h2"}],["path",{d:"M20 10h2"}],["path",{d:"M3 19h18"}],["path",{d:"M3 22v-6a2 2 135 0 1 2-2h14a2 2 45 0 1 2 2v6"}],["path",{d:"M3 2v2a2 2 45 0 0 2 2h14a2 2 135 0 0 2-2V2"}],["path",{d:"M8 10h2"}],["path",{d:"M9 22v-8"}],["path",{d:"M9 2v4"}]],Qy=[["path",{d:"M12 3v18"}],["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9h18"}],["path",{d:"M3 15h18"}]],Jy=[["rect",{width:"10",height:"14",x:"3",y:"8",rx:"2"}],["path",{d:"M5 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2h-2.4"}],["path",{d:"M8 18h.01"}]],Yy=[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2",ry:"2"}],["line",{x1:"12",x2:"12.01",y1:"18",y2:"18"}]],_y=[["circle",{cx:"7",cy:"7",r:"5"}],["circle",{cx:"17",cy:"17",r:"5"}],["path",{d:"M12 17h10"}],["path",{d:"m3.46 10.54 7.08-7.08"}]],xy=[["path",{d:"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}]],as=[["path",{d:"M13.172 2a2 2 0 0 1 1.414.586l6.71 6.71a2.4 2.4 0 0 1 0 3.408l-4.592 4.592a2.4 2.4 0 0 1-3.408 0l-6.71-6.71A2 2 0 0 1 6 9.172V3a1 1 0 0 1 1-1z"}],["path",{d:"M2 7v6.172a2 2 0 0 0 .586 1.414l6.71 6.71a2.4 2.4 0 0 0 3.191.193"}],["circle",{cx:"10.5",cy:"6.5",r:".5",fill:"currentColor"}]],ts=[["path",{d:"M4 4v16"}]],hs=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}]],ds=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}]],cs=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}],["path",{d:"M19 4v16"}]],Ms=[["path",{d:"M4 4v16"}],["path",{d:"M9 4v16"}],["path",{d:"M14 4v16"}],["path",{d:"M19 4v16"}],["path",{d:"M22 6 2 18"}]],ps=[["circle",{cx:"17",cy:"4",r:"2"}],["path",{d:"M15.59 5.41 5.41 15.59"}],["circle",{cx:"4",cy:"17",r:"2"}],["path",{d:"M12 22s-4-9-1.5-11.5S22 12 22 12"}]],is=[["circle",{cx:"12",cy:"12",r:"10"}],["circle",{cx:"12",cy:"12",r:"6"}],["circle",{cx:"12",cy:"12",r:"2"}]],ns=[["path",{d:"m10.065 12.493-6.18 1.318a.934.934 0 0 1-1.108-.702l-.537-2.15a1.07 1.07 0 0 1 .691-1.265l13.504-4.44"}],["path",{d:"m13.56 11.747 4.332-.924"}],["path",{d:"m16 21-3.105-6.21"}],["path",{d:"M16.485 5.94a2 2 0 0 1 1.455-2.425l1.09-.272a1 1 0 0 1 1.212.727l1.515 6.06a1 1 0 0 1-.727 1.213l-1.09.272a2 2 0 0 1-2.425-1.455z"}],["path",{d:"m6.158 8.633 1.114 4.456"}],["path",{d:"m8 21 3.105-6.21"}],["circle",{cx:"12",cy:"13",r:"2"}]],ls=[["circle",{cx:"4",cy:"4",r:"2"}],["path",{d:"m14 5 3-3 3 3"}],["path",{d:"m14 10 3-3 3 3"}],["path",{d:"M17 14V2"}],["path",{d:"M17 14H7l-5 8h20Z"}],["path",{d:"M8 14v8"}],["path",{d:"m9 14 5 8"}]],es=[["path",{d:"M3.5 21 14 3"}],["path",{d:"M20.5 21 10 3"}],["path",{d:"M15.5 21 12 15l-3.5 6"}],["path",{d:"M2 21h20"}]],ta=[["path",{d:"M21 7 6.82 21.18a2.83 2.83 0 0 1-3.99-.01a2.83 2.83 0 0 1 0-4L17 3"}],["path",{d:"m16 2 6 6"}],["path",{d:"M12 16H4"}]],rs=[["path",{d:"M12 19h8"}],["path",{d:"m4 17 6-6-6-6"}]],os=[["path",{d:"M14.5 2v17.5c0 1.4-1.1 2.5-2.5 2.5c-1.4 0-2.5-1.1-2.5-2.5V2"}],["path",{d:"M8.5 2h7"}],["path",{d:"M14.5 16h-5"}]],vs=[["path",{d:"M9 2v17.5A2.5 2.5 0 0 1 6.5 22A2.5 2.5 0 0 1 4 19.5V2"}],["path",{d:"M20 2v17.5a2.5 2.5 0 0 1-2.5 2.5a2.5 2.5 0 0 1-2.5-2.5V2"}],["path",{d:"M3 2h7"}],["path",{d:"M14 2h7"}],["path",{d:"M9 16H4"}],["path",{d:"M20 16h-5"}]],ha=[["path",{d:"M21 5H3"}],["path",{d:"M17 12H7"}],["path",{d:"M19 19H5"}]],da=[["path",{d:"M21 5H3"}],["path",{d:"M21 12H9"}],["path",{d:"M21 19H7"}]],ca=[["path",{d:"M3 5h18"}],["path",{d:"M3 12h18"}],["path",{d:"M3 19h18"}]],y=[["path",{d:"M21 5H3"}],["path",{d:"M15 12H3"}],["path",{d:"M17 19H3"}]],$s=[["path",{d:"M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6"}],["path",{d:"M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7"}],["path",{d:"M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1"}],["path",{d:"M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1"}],["path",{d:"M9 6v12"}]],ms=[["path",{d:"M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"}],["path",{d:"M7 22h1a4 4 0 0 0 4-4v-1"}],["path",{d:"M7 2h1a4 4 0 0 1 4 4v1"}]],ys=[["path",{d:"M17 5H3"}],["path",{d:"M21 12H8"}],["path",{d:"M21 19H8"}],["path",{d:"M3 12v7"}]],Ma=[["path",{d:"M15 5h6"}],["path",{d:"M15 12h6"}],["path",{d:"M3 19h18"}],["path",{d:"m3 12 3.553-7.724a.5.5 0 0 1 .894 0L11 12"}],["path",{d:"M3.92 10h6.16"}]],ss=[["path",{d:"M21 5H3"}],["path",{d:"M10 12H3"}],["path",{d:"M10 19H3"}],["circle",{cx:"17",cy:"15",r:"3"}],["path",{d:"m21 19-1.9-1.9"}]],pa=[["path",{d:"M14 21h1"}],["path",{d:"M14 3h1"}],["path",{d:"M19 3a2 2 0 0 1 2 2"}],["path",{d:"M21 14v1"}],["path",{d:"M21 19a2 2 0 0 1-2 2"}],["path",{d:"M21 9v1"}],["path",{d:"M3 14v1"}],["path",{d:"M3 9v1"}],["path",{d:"M5 21a2 2 0 0 1-2-2"}],["path",{d:"M5 3a2 2 0 0 0-2 2"}],["path",{d:"M7 12h10"}],["path",{d:"M7 16h6"}],["path",{d:"M7 8h8"}],["path",{d:"M9 21h1"}],["path",{d:"M9 3h1"}]],gs=[["path",{d:"M2 10s3-3 3-8"}],["path",{d:"M22 10s-3-3-3-8"}],["path",{d:"M10 2c0 4.4-3.6 8-8 8"}],["path",{d:"M14 2c0 4.4 3.6 8 8 8"}],["path",{d:"M2 10s2 2 2 5"}],["path",{d:"M22 10s-2 2-2 5"}],["path",{d:"M8 15h8"}],["path",{d:"M2 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1"}],["path",{d:"M14 22v-1a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1"}]],ia=[["path",{d:"m16 16-3 3 3 3"}],["path",{d:"M3 12h14.5a1 1 0 0 1 0 7H13"}],["path",{d:"M3 19h6"}],["path",{d:"M3 5h18"}]],Cs=[["path",{d:"m10 20-1.25-2.5L6 18"}],["path",{d:"M10 4 8.75 6.5 6 6"}],["path",{d:"M10.585 15H10"}],["path",{d:"M2 12h6.5L10 9"}],["path",{d:"M20 14.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0z"}],["path",{d:"m4 10 1.5 2L4 14"}],["path",{d:"m7 21 3-6-1.5-3"}],["path",{d:"m7 3 3 6h2"}]],us=[["path",{d:"M12 2v2"}],["path",{d:"M12 8a4 4 0 0 0-1.645 7.647"}],["path",{d:"M2 12h2"}],["path",{d:"M20 14.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0z"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"m6.34 17.66-1.41 1.41"}]],Hs=[["path",{d:"M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"}]],As=[["path",{d:"M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"}],["path",{d:"M17 14V2"}]],ws=[["path",{d:"M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"}],["path",{d:"M7 10v12"}]],Vs=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9 12 2 2 4-4"}]],Ss=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 12h6"}]],Ls=[["path",{d:"M2 9a3 3 0 1 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 1 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 9h.01"}],["path",{d:"m15 9-6 6"}],["path",{d:"M15 15h.01"}]],fs=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M9 12h6"}],["path",{d:"M12 9v6"}]],ks=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9.5 14.5 5-5"}]],Ps=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"m9.5 14.5 5-5"}],["path",{d:"m9.5 9.5 5 5"}]],Bs=[["path",{d:"M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"}],["path",{d:"M13 5v2"}],["path",{d:"M13 17v2"}],["path",{d:"M13 11v2"}]],zs=[["path",{d:"M10.5 17h1.227a2 2 0 0 0 1.345-.52L18 12"}],["path",{d:"m12 13.5 3.75.5"}],["path",{d:"m3.173 8.18 11-5a2 2 0 0 1 2.647.993L18.56 8"}],["path",{d:"M6 10V8"}],["path",{d:"M6 14v1"}],["path",{d:"M6 19v2"}],["rect",{x:"2",y:"8",width:"20",height:"13",rx:"2"}]],Fs=[["path",{d:"m3.173 8.18 11-5a2 2 0 0 1 2.647.993L18.56 8"}],["path",{d:"M6 10V8"}],["path",{d:"M6 14v1"}],["path",{d:"M6 19v2"}],["rect",{x:"2",y:"8",width:"20",height:"13",rx:"2"}]],Ds=[["path",{d:"M10 2h4"}],["path",{d:"M12 14v-4"}],["path",{d:"M4 13a8 8 0 0 1 8-7 8 8 0 1 1-5.3 14L4 17.6"}],["path",{d:"M9 17H4v5"}]],bs=[["path",{d:"M10 2h4"}],["path",{d:"M4.6 11a8 8 0 0 0 1.7 8.7 8 8 0 0 0 8.7 1.7"}],["path",{d:"M7.4 7.4a8 8 0 0 1 10.3 1 8 8 0 0 1 .9 10.2"}],["path",{d:"m2 2 20 20"}],["path",{d:"M12 12v-2"}]],Rs=[["line",{x1:"10",x2:"14",y1:"2",y2:"2"}],["line",{x1:"12",x2:"15",y1:"14",y2:"11"}],["circle",{cx:"12",cy:"14",r:"8"}]],Ts=[["circle",{cx:"9",cy:"12",r:"3"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7"}]],qs=[["circle",{cx:"15",cy:"12",r:"3"}],["rect",{width:"20",height:"14",x:"2",y:"5",rx:"7"}]],Us=[["path",{d:"M7 12h13a1 1 0 0 1 1 1 5 5 0 0 1-5 5h-.598a.5.5 0 0 0-.424.765l1.544 2.47a.5.5 0 0 1-.424.765H5.402a.5.5 0 0 1-.424-.765L7 18"}],["path",{d:"M8 18a5 5 0 0 1-5-5V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8"}]],Os=[["path",{d:"M10 15h4"}],["path",{d:"m14.817 10.995-.971-1.45 1.034-1.232a2 2 0 0 0-2.025-3.238l-1.82.364L9.91 3.885a2 2 0 0 0-3.625.748L6.141 6.55l-1.725.426a2 2 0 0 0-.19 3.756l.657.27"}],["path",{d:"m18.822 10.995 2.26-5.38a1 1 0 0 0-.557-1.318L16.954 2.9a1 1 0 0 0-1.281.533l-.924 2.122"}],["path",{d:"M4 12.006A1 1 0 0 1 4.994 11H19a1 1 0 0 1 1 1v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z"}]],Zs=[["path",{d:"M16 12v4"}],["path",{d:"M16 6a2 2 0 0 1 1.414.586l4 4A2 2 0 0 1 22 12v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 .586-1.414l4-4A2 2 0 0 1 8 6z"}],["path",{d:"M16 6V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"}],["path",{d:"M2 14h20"}],["path",{d:"M8 12v4"}]],Gs=[["path",{d:"M21 4H3"}],["path",{d:"M18 8H6"}],["path",{d:"M19 12H9"}],["path",{d:"M16 16h-6"}],["path",{d:"M11 20H9"}]],Ws=[["ellipse",{cx:"12",cy:"11",rx:"3",ry:"2"}],["ellipse",{cx:"12",cy:"12.5",rx:"10",ry:"8.5"}]],Is=[["path",{d:"M12 20v-6"}],["path",{d:"M19.656 14H22"}],["path",{d:"M2 14h12"}],["path",{d:"m2 2 20 20"}],["path",{d:"M20 20H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"}],["path",{d:"M9.656 4H20a2 2 0 0 1 2 2v10.344"}]],Es=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M2 14h20"}],["path",{d:"M12 20v-6"}]],Xs=[["path",{d:"M18.2 12.27 20 6H4l1.8 6.27a1 1 0 0 0 .95.73h10.5a1 1 0 0 0 .96-.73Z"}],["path",{d:"M8 13v9"}],["path",{d:"M16 22v-9"}],["path",{d:"m9 6 1 7"}],["path",{d:"m15 6-1 7"}],["path",{d:"M12 6V2"}],["path",{d:"M13 2h-2"}]],js=[["rect",{width:"18",height:"12",x:"3",y:"8",rx:"1"}],["path",{d:"M10 8V5c0-.6-.4-1-1-1H6a1 1 0 0 0-1 1v3"}],["path",{d:"M19 8V5c0-.6-.4-1-1-1h-3a1 1 0 0 0-1 1v3"}]],Ns=[["path",{d:"M16.05 10.966a5 2.5 0 0 1-8.1 0"}],["path",{d:"m16.923 14.049 4.48 2.04a1 1 0 0 1 .001 1.831l-8.574 3.9a2 2 0 0 1-1.66 0l-8.574-3.91a1 1 0 0 1 0-1.83l4.484-2.04"}],["path",{d:"M16.949 14.14a5 2.5 0 1 1-9.9 0L10.063 3.5a2 2 0 0 1 3.874 0z"}],["path",{d:"M9.194 6.57a5 2.5 0 0 0 5.61 0"}]],Ks=[["path",{d:"m10 11 11 .9a1 1 0 0 1 .8 1.1l-.665 4.158a1 1 0 0 1-.988.842H20"}],["path",{d:"M16 18h-5"}],["path",{d:"M18 5a1 1 0 0 0-1 1v5.573"}],["path",{d:"M3 4h8.129a1 1 0 0 1 .99.863L13 11.246"}],["path",{d:"M4 11V4"}],["path",{d:"M7 15h.01"}],["path",{d:"M8 10.1V4"}],["circle",{cx:"18",cy:"18",r:"2"}],["circle",{cx:"7",cy:"15",r:"5"}]],Qs=[["path",{d:"M2 22V12a10 10 0 1 1 20 0v10"}],["path",{d:"M15 6.8v1.4a3 2.8 0 1 1-6 0V6.8"}],["path",{d:"M10 15h.01"}],["path",{d:"M14 15h.01"}],["path",{d:"M10 19a4 4 0 0 1-4-4v-3a6 6 0 1 1 12 0v3a4 4 0 0 1-4 4Z"}],["path",{d:"m9 19-2 3"}],["path",{d:"m15 19 2 3"}]],Js=[["path",{d:"M8 3.1V7a4 4 0 0 0 8 0V3.1"}],["path",{d:"m9 15-1-1"}],["path",{d:"m15 15 1-1"}],["path",{d:"M9 19c-2.8 0-5-2.2-5-5v-4a8 8 0 0 1 16 0v4c0 2.8-2.2 5-5 5Z"}],["path",{d:"m8 19-2 3"}],["path",{d:"m16 19 2 3"}]],Ys=[["path",{d:"M2 17 17 2"}],["path",{d:"m2 14 8 8"}],["path",{d:"m5 11 8 8"}],["path",{d:"m8 8 8 8"}],["path",{d:"m11 5 8 8"}],["path",{d:"m14 2 8 8"}],["path",{d:"M7 22 22 7"}]],na=[["rect",{width:"16",height:"16",x:"4",y:"3",rx:"2"}],["path",{d:"M4 11h16"}],["path",{d:"M12 3v8"}],["path",{d:"m8 19-2 3"}],["path",{d:"m18 22-2-3"}],["path",{d:"M8 15h.01"}],["path",{d:"M16 15h.01"}]],_s=[["path",{d:"M12 16v6"}],["path",{d:"M14 20h-4"}],["path",{d:"M18 2h4v4"}],["path",{d:"m2 2 7.17 7.17"}],["path",{d:"M2 5.355V2h3.357"}],["path",{d:"m22 2-7.17 7.17"}],["path",{d:"M8 5 5 8"}],["circle",{cx:"12",cy:"12",r:"4"}]],xs=[["path",{d:"M10 11v6"}],["path",{d:"M14 11v6"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]],ag=[["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]],tg=[["path",{d:"M8 19a4 4 0 0 1-2.24-7.32A3.5 3.5 0 0 1 9 6.03V6a3 3 0 1 1 6 0v.04a3.5 3.5 0 0 1 3.24 5.65A4 4 0 0 1 16 19Z"}],["path",{d:"M12 19v3"}]],la=[["path",{d:"M13 8c0-2.76-2.46-5-5.5-5S2 5.24 2 8h2l1-1 1 1h4"}],["path",{d:"M13 7.14A5.82 5.82 0 0 1 16.5 6c3.04 0 5.5 2.24 5.5 5h-3l-1-1-1 1h-3"}],["path",{d:"M5.89 9.71c-2.15 2.15-2.3 5.47-.35 7.43l4.24-4.25.7-.7.71-.71 2.12-2.12c-1.95-1.96-5.27-1.8-7.42.35"}],["path",{d:"M11 15.5c.5 2.5-.17 4.5-1 6.5h4c2-5.5-.5-12-1-14"}]],hg=[["path",{d:"m17 14 3 3.3a1 1 0 0 1-.7 1.7H4.7a1 1 0 0 1-.7-1.7L7 14h-.3a1 1 0 0 1-.7-1.7L9 9h-.2A1 1 0 0 1 8 7.3L12 3l4 4.3a1 1 0 0 1-.8 1.7H15l3 3.3a1 1 0 0 1-.7 1.7H17Z"}],["path",{d:"M12 22v-3"}]],dg=[["path",{d:"M10 10v.2A3 3 0 0 1 8.9 16H5a3 3 0 0 1-1-5.8V10a3 3 0 0 1 6 0Z"}],["path",{d:"M7 16v6"}],["path",{d:"M13 19v3"}],["path",{d:"M12 19h8.3a1 1 0 0 0 .7-1.7L18 14h.3a1 1 0 0 0 .7-1.7L16 9h.2a1 1 0 0 0 .8-1.7L13 3l-1.4 1.5"}]],cg=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",ry:"2"}],["rect",{width:"3",height:"9",x:"7",y:"7"}],["rect",{width:"3",height:"5",x:"14",y:"7"}]],Mg=[["path",{d:"M16 17h6v-6"}],["path",{d:"m22 17-8.5-8.5-5 5L2 7"}]],pg=[["path",{d:"M14.828 14.828 21 21"}],["path",{d:"M21 16v5h-5"}],["path",{d:"m21 3-9 9-4-4-6 6"}],["path",{d:"M21 8V3h-5"}]],ig=[["path",{d:"M16 7h6v6"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17"}]],ea=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"}],["path",{d:"M12 9v4"}],["path",{d:"M12 17h.01"}]],ng=[["path",{d:"M10.17 4.193a2 2 0 0 1 3.666.013"}],["path",{d:"M14 21h2"}],["path",{d:"m15.874 7.743 1 1.732"}],["path",{d:"m18.849 12.952 1 1.732"}],["path",{d:"M21.824 18.18a2 2 0 0 1-1.835 2.824"}],["path",{d:"M4.024 21a2 2 0 0 1-1.839-2.839"}],["path",{d:"m5.136 12.952-1 1.732"}],["path",{d:"M8 21h2"}],["path",{d:"m8.102 7.743-1 1.732"}]],lg=[["path",{d:"M22 18a2 2 0 0 1-2 2H3c-1.1 0-1.3-.6-.4-1.3L20.4 4.3c.9-.7 1.6-.4 1.6.7Z"}]],eg=[["path",{d:"M13.73 4a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"}]],rg=[["path",{d:"M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978"}],["path",{d:"M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978"}],["path",{d:"M18 9h1.5a1 1 0 0 0 0-5H18"}],["path",{d:"M4 22h16"}],["path",{d:"M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z"}],["path",{d:"M6 9H4.5a1 1 0 0 1 0-5H6"}]],og=[["path",{d:"M14 19V7a2 2 0 0 0-2-2H9"}],["path",{d:"M15 19H9"}],["path",{d:"M19 19h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62L18.3 9.38a1 1 0 0 0-.78-.38H14"}],["path",{d:"M2 13v5a1 1 0 0 0 1 1h2"}],["path",{d:"M4 3 2.15 5.15a.495.495 0 0 0 .35.86h2.15a.47.47 0 0 1 .35.86L3 9.02"}],["circle",{cx:"17",cy:"19",r:"2"}],["circle",{cx:"7",cy:"19",r:"2"}]],vg=[["path",{d:"M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"}],["path",{d:"M15 18H9"}],["path",{d:"M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14"}],["circle",{cx:"17",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],$g=[["path",{d:"M15 4 5 9"}],["path",{d:"m15 8.5-10 5"}],["path",{d:"M18 12a9 9 0 0 1-9 9V3"}]],mg=[["path",{d:"M10 12.01h.01"}],["path",{d:"M18 8v4a8 8 0 0 1-1.07 4"}],["circle",{cx:"10",cy:"12",r:"4"}],["rect",{x:"2",y:"4",width:"20",height:"16",rx:"2"}]],yg=[["path",{d:"m12 10 2 4v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a8 8 0 1 0-16 0v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3l2-4h4Z"}],["path",{d:"M4.82 7.9 8 10"}],["path",{d:"M15.18 7.9 12 10"}],["path",{d:"M16.93 10H20a2 2 0 0 1 0 4H2"}]],sg=[["path",{d:"M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"}],["path",{d:"M7 21h10"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}]],ra=[["path",{d:"M7 21h10"}],["rect",{width:"20",height:"14",x:"2",y:"3",rx:"2"}]],gg=[["path",{d:"m17 2-5 5-5-5"}],["rect",{width:"20",height:"15",x:"2",y:"7",rx:"2"}]],Cg=[["path",{d:"M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"}]],ug=[["path",{d:"M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"}]],Hg=[["path",{d:"M14 16.5a.5.5 0 0 0 .5.5h.5a2 2 0 0 1 0 4H9a2 2 0 0 1 0-4h.5a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5V8a2 2 0 0 1-4 0V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v3a2 2 0 0 1-4 0v-.5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5Z"}]],Ag=[["path",{d:"M12 4v16"}],["path",{d:"M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2"}],["path",{d:"M9 20h6"}]],wg=[["path",{d:"M12 13v7a2 2 0 0 0 4 0"}],["path",{d:"M12 2v2"}],["path",{d:"M18.656 13h2.336a1 1 0 0 0 .97-1.274 10.284 10.284 0 0 0-12.07-7.51"}],["path",{d:"m2 2 20 20"}],["path",{d:"M5.961 5.957a10.28 10.28 0 0 0-3.922 5.769A1 1 0 0 0 3 13h10"}]],Vg=[["path",{d:"M12 13v7a2 2 0 0 0 4 0"}],["path",{d:"M12 2v2"}],["path",{d:"M20.992 13a1 1 0 0 0 .97-1.274 10.284 10.284 0 0 0-19.923 0A1 1 0 0 0 3 13z"}]],Sg=[["path",{d:"M6 4v6a6 6 0 0 0 12 0V4"}],["line",{x1:"4",x2:"20",y1:"20",y2:"20"}]],Lg=[["path",{d:"M9 14 4 9l5-5"}],["path",{d:"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"}]],fg=[["path",{d:"M21 17a9 9 0 0 0-15-6.7L3 13"}],["path",{d:"M3 7v6h6"}],["circle",{cx:"12",cy:"17",r:"1"}]],kg=[["path",{d:"M3 7v6h6"}],["path",{d:"M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"}]],Pg=[["path",{d:"M16 12h6"}],["path",{d:"M8 12H2"}],["path",{d:"M12 2v2"}],["path",{d:"M12 8v2"}],["path",{d:"M12 14v2"}],["path",{d:"M12 20v2"}],["path",{d:"m19 15 3-3-3-3"}],["path",{d:"m5 9-3 3 3 3"}]],Bg=[["path",{d:"M12 22v-6"}],["path",{d:"M12 8V2"}],["path",{d:"M4 12H2"}],["path",{d:"M10 12H8"}],["path",{d:"M16 12h-2"}],["path",{d:"M22 12h-2"}],["path",{d:"m15 19-3 3-3-3"}],["path",{d:"m15 5-3-3-3 3"}]],zg=[["rect",{width:"8",height:"6",x:"5",y:"4",rx:"1"}],["rect",{width:"8",height:"6",x:"11",y:"14",rx:"1"}]],oa=[["path",{d:"M14 21v-3a2 2 0 0 0-4 0v3"}],["path",{d:"M18 12h.01"}],["path",{d:"M18 16h.01"}],["path",{d:"M22 7a1 1 0 0 0-1-1h-2a2 2 0 0 1-1.143-.359L13.143 2.36a2 2 0 0 0-2.286-.001L6.143 5.64A2 2 0 0 1 5 6H3a1 1 0 0 0-1 1v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2z"}],["path",{d:"M6 12h.01"}],["path",{d:"M6 16h.01"}],["circle",{cx:"12",cy:"10",r:"2"}]],Fg=[["path",{d:"M15 7h2a5 5 0 0 1 0 10h-2m-6 0H7A5 5 0 0 1 7 7h2"}]],Dg=[["path",{d:"m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71"}],["path",{d:"m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71"}],["line",{x1:"8",x2:"8",y1:"2",y2:"5"}],["line",{x1:"2",x2:"5",y1:"8",y2:"8"}],["line",{x1:"16",x2:"16",y1:"19",y2:"22"}],["line",{x1:"19",x2:"22",y1:"16",y2:"16"}]],bg=[["path",{d:"m19 5 3-3"}],["path",{d:"m2 22 3-3"}],["path",{d:"M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"}],["path",{d:"M7.5 13.5 10 11"}],["path",{d:"M10.5 16.5 13 14"}],["path",{d:"m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"}]],Rg=[["circle",{cx:"10",cy:"7",r:"1"}],["circle",{cx:"4",cy:"20",r:"1"}],["path",{d:"M4.7 19.3 19 5"}],["path",{d:"m21 3-3 1 2 2Z"}],["path",{d:"M9.26 7.68 5 12l2 5"}],["path",{d:"m10 14 5 2 3.5-3.5"}],["path",{d:"m18 12 1-1 1 1-1 1Z"}]],Tg=[["path",{d:"M12 3v12"}],["path",{d:"m17 8-5-5-5 5"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}]],qg=[["path",{d:"m16 11 2 2 4-4"}],["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}]],Ug=[["path",{d:"M10 15H6a4 4 0 0 0-4 4v2"}],["path",{d:"m14.305 16.53.923-.382"}],["path",{d:"m15.228 13.852-.923-.383"}],["path",{d:"m16.852 12.228-.383-.923"}],["path",{d:"m16.852 17.772-.383.924"}],["path",{d:"m19.148 12.228.383-.923"}],["path",{d:"m19.53 18.696-.382-.924"}],["path",{d:"m20.772 13.852.924-.383"}],["path",{d:"m20.772 16.148.924.383"}],["circle",{cx:"18",cy:"15",r:"3"}],["circle",{cx:"9",cy:"7",r:"4"}]],Og=[["circle",{cx:"10",cy:"7",r:"4"}],["path",{d:"M10.3 15H7a4 4 0 0 0-4 4v2"}],["path",{d:"M15 15.5V14a2 2 0 0 1 4 0v1.5"}],["rect",{width:"8",height:"5",x:"13",y:"16",rx:".899"}]],Zg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11"}]],Gg=[["path",{d:"M11.5 15H7a4 4 0 0 0-4 4v2"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"7",r:"4"}]],Wg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"19",x2:"19",y1:"8",y2:"14"}],["line",{x1:"22",x2:"16",y1:"11",y2:"11"}]],va=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"m16 19 2 2 4-4"}]],$a=[["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"M2 21a8 8 0 0 1 10.434-7.62"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["circle",{cx:"10",cy:"8",r:"5"}],["circle",{cx:"18",cy:"18",r:"3"}]],ma=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M22 19h-6"}]],Ig=[["path",{d:"M2 21a8 8 0 0 1 10.821-7.487"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["circle",{cx:"10",cy:"8",r:"5"}]],ya=[["path",{d:"M2 21a8 8 0 0 1 13.292-6"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M19 16v6"}],["path",{d:"M22 19h-6"}]],Eg=[["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M2 21a8 8 0 0 1 10.434-7.62"}],["circle",{cx:"18",cy:"18",r:"3"}],["path",{d:"m22 22-1.9-1.9"}]],sa=[["circle",{cx:"12",cy:"8",r:"5"}],["path",{d:"M20 21a8 8 0 0 0-16 0"}]],ga=[["path",{d:"M2 21a8 8 0 0 1 11.873-7"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"m17 17 5 5"}],["path",{d:"m22 17-5 5"}]],Xg=[["circle",{cx:"10",cy:"7",r:"4"}],["path",{d:"M10.3 15H7a4 4 0 0 0-4 4v2"}],["circle",{cx:"17",cy:"17",r:"3"}],["path",{d:"m21 21-1.9-1.9"}]],jg=[["path",{d:"M16.051 12.616a1 1 0 0 1 1.909.024l.737 1.452a1 1 0 0 0 .737.535l1.634.256a1 1 0 0 1 .588 1.806l-1.172 1.168a1 1 0 0 0-.282.866l.259 1.613a1 1 0 0 1-1.541 1.134l-1.465-.75a1 1 0 0 0-.912 0l-1.465.75a1 1 0 0 1-1.539-1.133l.258-1.613a1 1 0 0 0-.282-.866l-1.156-1.153a1 1 0 0 1 .572-1.822l1.633-.256a1 1 0 0 0 .737-.535z"}],["path",{d:"M8 15H7a4 4 0 0 0-4 4v2"}],["circle",{cx:"10",cy:"7",r:"4"}]],Ng=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["circle",{cx:"9",cy:"7",r:"4"}],["line",{x1:"17",x2:"22",y1:"8",y2:"13"}],["line",{x1:"22",x2:"17",y1:"8",y2:"13"}]],Kg=[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"}],["circle",{cx:"12",cy:"7",r:"4"}]],Ca=[["path",{d:"M18 21a8 8 0 0 0-16 0"}],["circle",{cx:"10",cy:"8",r:"5"}],["path",{d:"M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"}]],Qg=[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87"}],["circle",{cx:"9",cy:"7",r:"4"}]],ua=[["path",{d:"m16 2-2.3 2.3a3 3 0 0 0 0 4.2l1.8 1.8a3 3 0 0 0 4.2 0L22 8"}],["path",{d:"M15 15 3.3 3.3a4.2 4.2 0 0 0 0 6l7.3 7.3c.7.7 2 .7 2.8 0L15 15Zm0 0 7 7"}],["path",{d:"m2.1 21.8 6.4-6.3"}],["path",{d:"m19 5-7 7"}]],Ha=[["path",{d:"M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"}],["path",{d:"M7 2v20"}],["path",{d:"M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3Zm0 0v7"}]],Jg=[["path",{d:"M13 6v5a1 1 0 0 0 1 1h6.102a1 1 0 0 1 .712.298l.898.91a1 1 0 0 1 .288.702V17a1 1 0 0 1-1 1h-3"}],["path",{d:"M5 18H3a1 1 0 0 1-1-1V8a2 2 0 0 1 2-2h12c1.1 0 2.1.8 2.4 1.8l1.176 4.2"}],["path",{d:"M9 18h5"}],["circle",{cx:"16",cy:"18",r:"2"}],["circle",{cx:"7",cy:"18",r:"2"}]],Yg=[["path",{d:"M12 2v20"}],["path",{d:"M2 5h20"}],["path",{d:"M3 3v2"}],["path",{d:"M7 3v2"}],["path",{d:"M17 3v2"}],["path",{d:"M21 3v2"}],["path",{d:"m19 5-7 7-7-7"}]],_g=[["path",{d:"M8 21s-4-3-4-9 4-9 4-9"}],["path",{d:"M16 3s4 3 4 9-4 9-4 9"}],["line",{x1:"15",x2:"9",y1:"9",y2:"15"}],["line",{x1:"9",x2:"15",y1:"9",y2:"15"}]],xg=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["circle",{cx:"7.5",cy:"7.5",r:".5",fill:"currentColor"}],["path",{d:"m7.9 7.9 2.7 2.7"}],["circle",{cx:"16.5",cy:"7.5",r:".5",fill:"currentColor"}],["path",{d:"m13.4 10.6 2.7-2.7"}],["circle",{cx:"7.5",cy:"16.5",r:".5",fill:"currentColor"}],["path",{d:"m7.9 16.1 2.7-2.7"}],["circle",{cx:"16.5",cy:"16.5",r:".5",fill:"currentColor"}],["path",{d:"m13.4 13.4 2.7 2.7"}],["circle",{cx:"12",cy:"12",r:"2"}]],aC=[["path",{d:"M19.5 7a24 24 0 0 1 0 10"}],["path",{d:"M4.5 7a24 24 0 0 0 0 10"}],["path",{d:"M7 19.5a24 24 0 0 0 10 0"}],["path",{d:"M7 4.5a24 24 0 0 1 10 0"}],["rect",{x:"17",y:"17",width:"5",height:"5",rx:"1"}],["rect",{x:"17",y:"2",width:"5",height:"5",rx:"1"}],["rect",{x:"2",y:"17",width:"5",height:"5",rx:"1"}],["rect",{x:"2",y:"2",width:"5",height:"5",rx:"1"}]],tC=[["path",{d:"M16 8q6 0 6-6-6 0-6 6"}],["path",{d:"M17.41 3.59a10 10 0 1 0 3 3"}],["path",{d:"M2 2a26.6 26.6 0 0 1 10 20c.9-6.82 1.5-9.5 4-14"}]],hC=[["path",{d:"M18 11c-1.5 0-2.5.5-3 2"}],["path",{d:"M4 6a2 2 0 0 0-2 2v4a5 5 0 0 0 5 5 8 8 0 0 1 5 2 8 8 0 0 1 5-2 5 5 0 0 0 5-5V8a2 2 0 0 0-2-2h-3a8 8 0 0 0-5 2 8 8 0 0 0-5-2z"}],["path",{d:"M6 11c1.5 0 2.5.5 3 2"}]],dC=[["path",{d:"M12 15v7"}],["path",{d:"M9 19h6"}],["circle",{cx:"12",cy:"9",r:"6"}]],cC=[["path",{d:"M10 20h4"}],["path",{d:"M12 16v6"}],["path",{d:"M17 2h4v4"}],["path",{d:"m21 2-5.46 5.46"}],["circle",{cx:"12",cy:"11",r:"5"}]],MC=[["path",{d:"m2 8 2 2-2 2 2 2-2 2"}],["path",{d:"m22 8-2 2 2 2-2 2 2 2"}],["path",{d:"M8 8v10c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2"}],["path",{d:"M16 10.34V6c0-.55-.45-1-1-1h-4.34"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],pC=[["path",{d:"m2 8 2 2-2 2 2 2-2 2"}],["path",{d:"m22 8-2 2 2 2-2 2 2 2"}],["rect",{width:"8",height:"14",x:"8",y:"5",rx:"1"}]],iC=[["path",{d:"M10.66 6H14a2 2 0 0 1 2 2v2.5l5.248-3.062A.5.5 0 0 1 22 7.87v8.196"}],["path",{d:"M16 16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2"}],["path",{d:"m2 2 20 20"}]],nC=[["path",{d:"m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"}],["rect",{x:"2",y:"6",width:"14",height:"12",rx:"2"}]],lC=[["circle",{cx:"6",cy:"12",r:"4"}],["circle",{cx:"18",cy:"12",r:"4"}],["line",{x1:"6",x2:"18",y1:"16",y2:"16"}]],eC=[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2"}],["path",{d:"M2 8h20"}],["circle",{cx:"8",cy:"14",r:"2"}],["path",{d:"M8 12h8"}],["circle",{cx:"16",cy:"14",r:"2"}]],rC=[["path",{d:"M21 17v2a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-2"}],["path",{d:"M21 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2"}],["circle",{cx:"12",cy:"12",r:"1"}],["path",{d:"M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"}]],oC=[["path",{d:"M11.1 7.1a16.55 16.55 0 0 1 10.9 4"}],["path",{d:"M12 12a12.6 12.6 0 0 1-8.7 5"}],["path",{d:"M16.8 13.6a16.55 16.55 0 0 1-9 7.5"}],["path",{d:"M20.7 17a12.8 12.8 0 0 0-8.7-5 13.3 13.3 0 0 1 0-10"}],["path",{d:"M6.3 3.8a16.55 16.55 0 0 0 1.9 11.5"}],["circle",{cx:"12",cy:"12",r:"10"}]],vC=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["path",{d:"M16 9a5 5 0 0 1 0 6"}]],$C=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["path",{d:"M16 9a5 5 0 0 1 0 6"}],["path",{d:"M19.364 18.364a9 9 0 0 0 0-12.728"}]],mC=[["path",{d:"M16 9a5 5 0 0 1 .95 2.293"}],["path",{d:"M19.364 5.636a9 9 0 0 1 1.889 9.96"}],["path",{d:"m2 2 20 20"}],["path",{d:"m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11"}],["path",{d:"M9.828 4.172A.686.686 0 0 1 11 4.657v.686"}]],yC=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}],["line",{x1:"22",x2:"16",y1:"9",y2:"15"}],["line",{x1:"16",x2:"22",y1:"9",y2:"15"}]],sC=[["path",{d:"M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"}]],gC=[["path",{d:"m9 12 2 2 4-4"}],["path",{d:"M5 7c0-1.1.9-2 2-2h10a2 2 0 0 1 2 2v12H5V7Z"}],["path",{d:"M22 19H2"}]],Aa=[["path",{d:"M17 14h.01"}],["path",{d:"M7 7h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14"}]],CC=[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2"}],["path",{d:"M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2"}],["path",{d:"M3 11h3c.8 0 1.6.3 2.1.9l1.1.9c1.6 1.6 4.1 1.6 5.7 0l1.1-.9c.5-.5 1.3-.9 2.1-.9H21"}]],uC=[["path",{d:"M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"}],["path",{d:"M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"}]],HC=[["path",{d:"M12 17v4"}],["path",{d:"M8 21h8"}],["path",{d:"m9 17 6.1-6.1a2 2 0 0 1 2.81.01L22 15"}],["circle",{cx:"8",cy:"9",r:"2"}],["rect",{x:"2",y:"3",width:"20",height:"14",rx:"2"}]],wa=[["path",{d:"m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"}],["path",{d:"m14 7 3 3"}],["path",{d:"M5 6v4"}],["path",{d:"M19 14v4"}],["path",{d:"M10 2v2"}],["path",{d:"M7 8H3"}],["path",{d:"M21 16h-4"}],["path",{d:"M11 3H9"}]],AC=[["path",{d:"M15 4V2"}],["path",{d:"M15 16v-2"}],["path",{d:"M8 9h2"}],["path",{d:"M20 9h2"}],["path",{d:"M17.8 11.8 19 13"}],["path",{d:"M15 9h.01"}],["path",{d:"M17.8 6.2 19 5"}],["path",{d:"m3 21 9-9"}],["path",{d:"M12.2 6.2 11 5"}]],wC=[["path",{d:"M18 21V10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v11"}],["path",{d:"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 1.132-1.803l7.95-3.974a2 2 0 0 1 1.837 0l7.948 3.974A2 2 0 0 1 22 8z"}],["path",{d:"M6 13h12"}],["path",{d:"M6 17h12"}]],VC=[["path",{d:"M3 6h3"}],["path",{d:"M17 6h.01"}],["rect",{width:"18",height:"20",x:"3",y:"2",rx:"2"}],["circle",{cx:"12",cy:"13",r:"5"}],["path",{d:"M12 18a2.5 2.5 0 0 0 0-5 2.5 2.5 0 0 1 0-5"}]],SC=[["path",{d:"M12 10v2.2l1.6 1"}],["path",{d:"m16.13 7.66-.81-4.05a2 2 0 0 0-2-1.61h-2.68a2 2 0 0 0-2 1.61l-.78 4.05"}],["path",{d:"m7.88 16.36.8 4a2 2 0 0 0 2 1.61h2.72a2 2 0 0 0 2-1.61l.81-4.05"}],["circle",{cx:"12",cy:"12",r:"6"}]],LC=[["path",{d:"M12 10L12 2"}],["path",{d:"M16 6L12 10L8 6"}],["path",{d:"M2 15C2.6 15.5 3.2 16 4.5 16C7 16 7 14 9.5 14C12.1 14 11.9 16 14.5 16C17 16 17 14 19.5 14C20.8 14 21.4 14.5 22 15"}],["path",{d:"M2 21C2.6 21.5 3.2 22 4.5 22C7 22 7 20 9.5 20C12.1 20 11.9 22 14.5 22C17 22 17 20 19.5 20C20.8 20 21.4 20.5 22 21"}]],fC=[["path",{d:"M12 2v8"}],["path",{d:"M2 15c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"m8 6 4-4 4 4"}]],kC=[["path",{d:"M19 5a2 2 0 0 0-2 2v11"}],["path",{d:"M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M7 13h10"}],["path",{d:"M7 9h10"}],["path",{d:"M9 5a2 2 0 0 0-2 2v11"}]],PC=[["path",{d:"M2 6c.6.5 1.2 1 2.5 1C7 7 7 5 9.5 5c2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 12c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}],["path",{d:"M2 18c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 2.6 0 2.4 2 5 2 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}]],BC=[["circle",{cx:"12",cy:"4.5",r:"2.5"}],["path",{d:"m10.2 6.3-3.9 3.9"}],["circle",{cx:"4.5",cy:"12",r:"2.5"}],["path",{d:"M7 12h10"}],["circle",{cx:"19.5",cy:"12",r:"2.5"}],["path",{d:"m13.8 17.7 3.9-3.9"}],["circle",{cx:"12",cy:"19.5",r:"2.5"}]],zC=[["circle",{cx:"12",cy:"10",r:"8"}],["circle",{cx:"12",cy:"10",r:"3"}],["path",{d:"M7 22h10"}],["path",{d:"M12 22v-4"}]],FC=[["path",{d:"M17 17h-5c-1.09-.02-1.94.92-2.5 1.9A3 3 0 1 1 2.57 15"}],["path",{d:"M9 3.4a4 4 0 0 1 6.52.66"}],["path",{d:"m6 17 3.1-5.8a2.5 2.5 0 0 0 .057-2.05"}],["path",{d:"M20.3 20.3a4 4 0 0 1-2.3.7"}],["path",{d:"M18.6 13a4 4 0 0 1 3.357 3.414"}],["path",{d:"m12 6 .6 1"}],["path",{d:"m2 2 20 20"}]],DC=[["path",{d:"M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2"}],["path",{d:"m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06"}],["path",{d:"m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8"}]],bC=[["path",{d:"M6.5 8a2 2 0 0 0-1.906 1.46L2.1 18.5A2 2 0 0 0 4 21h16a2 2 0 0 0 1.925-2.54L19.4 9.5A2 2 0 0 0 17.48 8z"}],["path",{d:"M7.999 15a2.5 2.5 0 0 1 4 0 2.5 2.5 0 0 0 4 0"}],["circle",{cx:"12",cy:"5",r:"3"}]],RC=[["circle",{cx:"12",cy:"5",r:"3"}],["path",{d:"M6.5 8a2 2 0 0 0-1.905 1.46L2.1 18.5A2 2 0 0 0 4 21h16a2 2 0 0 0 1.925-2.54L19.4 9.5A2 2 0 0 0 17.48 8Z"}]],TC=[["path",{d:"m2 22 10-10"}],["path",{d:"m16 8-1.17 1.17"}],["path",{d:"M3.47 12.53 5 11l1.53 1.53a3.5 3.5 0 0 1 0 4.94L5 19l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"m8 8-.53.53a3.5 3.5 0 0 0 0 4.94L9 15l1.53-1.53c.55-.55.88-1.25.98-1.97"}],["path",{d:"M10.91 5.26c.15-.26.34-.51.56-.73L13 3l1.53 1.53a3.5 3.5 0 0 1 .28 4.62"}],["path",{d:"M20 2h2v2a4 4 0 0 1-4 4h-2V6a4 4 0 0 1 4-4Z"}],["path",{d:"M11.47 17.47 13 19l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L5 19l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"m16 16-.53.53a3.5 3.5 0 0 1-4.94 0L9 15l1.53-1.53a3.49 3.49 0 0 1 1.97-.98"}],["path",{d:"M18.74 13.09c.26-.15.51-.34.73-.56L21 11l-1.53-1.53a3.5 3.5 0 0 0-4.62-.28"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],qC=[["path",{d:"M2 22 16 8"}],["path",{d:"M3.47 12.53 5 11l1.53 1.53a3.5 3.5 0 0 1 0 4.94L5 19l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M7.47 8.53 9 7l1.53 1.53a3.5 3.5 0 0 1 0 4.94L9 15l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M11.47 4.53 13 3l1.53 1.53a3.5 3.5 0 0 1 0 4.94L13 11l-1.53-1.53a3.5 3.5 0 0 1 0-4.94Z"}],["path",{d:"M20 2h2v2a4 4 0 0 1-4 4h-2V6a4 4 0 0 1 4-4Z"}],["path",{d:"M11.47 17.47 13 19l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L5 19l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"M15.47 13.47 17 15l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L9 15l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}],["path",{d:"M19.47 9.47 21 11l-1.53 1.53a3.5 3.5 0 0 1-4.94 0L13 11l1.53-1.53a3.5 3.5 0 0 1 4.94 0Z"}]],UC=[["circle",{cx:"7",cy:"12",r:"3"}],["path",{d:"M10 9v6"}],["circle",{cx:"17",cy:"12",r:"3"}],["path",{d:"M14 7v8"}],["path",{d:"M22 17v1c0 .5-.5 1-1 1H3c-.5 0-1-.5-1-1v-1"}]],OC=[["path",{d:"m14.305 19.53.923-.382"}],["path",{d:"m15.228 16.852-.923-.383"}],["path",{d:"m16.852 15.228-.383-.923"}],["path",{d:"m16.852 20.772-.383.924"}],["path",{d:"m19.148 15.228.383-.923"}],["path",{d:"m19.53 21.696-.382-.924"}],["path",{d:"M2 7.82a15 15 0 0 1 20 0"}],["path",{d:"m20.772 16.852.924-.383"}],["path",{d:"m20.772 19.148.924.383"}],["path",{d:"M5 11.858a10 10 0 0 1 11.5-1.785"}],["path",{d:"M8.5 15.429a5 5 0 0 1 2.413-1.31"}],["circle",{cx:"18",cy:"18",r:"3"}]],ZC=[["path",{d:"M12 20h.01"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],GC=[["path",{d:"M12 20h.01"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],WC=[["path",{d:"M12 20h.01"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}],["path",{d:"M5 12.859a10 10 0 0 1 5.17-2.69"}],["path",{d:"M19 12.859a10 10 0 0 0-2.007-1.523"}],["path",{d:"M2 8.82a15 15 0 0 1 4.177-2.643"}],["path",{d:"M22 8.82a15 15 0 0 0-11.288-3.764"}],["path",{d:"m2 2 20 20"}]],IC=[["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"}],["path",{d:"M5 12.859a10 10 0 0 1 10.5-2.222"}],["path",{d:"M8.5 16.429a5 5 0 0 1 3-1.406"}]],EC=[["path",{d:"M11.965 10.105v4L13.5 12.5a5 5 0 0 1 8 1.5"}],["path",{d:"M11.965 14.105h4"}],["path",{d:"M17.965 18.105h4L20.43 19.71a5 5 0 0 1-8-1.5"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M21.965 22.105v-4"}],["path",{d:"M5 12.86a10 10 0 0 1 3-2.032"}],["path",{d:"M8.5 16.429h.01"}]],XC=[["path",{d:"M12 20h.01"}]],jC=[["path",{d:"M12 20h.01"}],["path",{d:"M2 8.82a15 15 0 0 1 20 0"}],["path",{d:"M5 12.859a10 10 0 0 1 14 0"}],["path",{d:"M8.5 16.429a5 5 0 0 1 7 0"}]],NC=[["path",{d:"M10 2v8"}],["path",{d:"M12.8 21.6A2 2 0 1 0 14 18H2"}],["path",{d:"M17.5 10a2.5 2.5 0 1 1 2 4H2"}],["path",{d:"m6 6 4 4 4-4"}]],KC=[["path",{d:"M12.8 19.6A2 2 0 1 0 14 16H2"}],["path",{d:"M17.5 8a2.5 2.5 0 1 1 2 4H2"}],["path",{d:"M9.8 4.4A2 2 0 1 1 11 8H2"}]],QC=[["path",{d:"M8 22h8"}],["path",{d:"M7 10h3m7 0h-1.343"}],["path",{d:"M12 15v7"}],["path",{d:"M7.307 7.307A12.33 12.33 0 0 0 7 10a5 5 0 0 0 7.391 4.391M8.638 2.981C8.75 2.668 8.872 2.34 9 2h6c1.5 4 2 6 2 8 0 .407-.05.809-.145 1.198"}],["line",{x1:"2",x2:"22",y1:"2",y2:"22"}]],JC=[["path",{d:"M8 22h8"}],["path",{d:"M7 10h10"}],["path",{d:"M12 15v7"}],["path",{d:"M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z"}]],YC=[["path",{d:"m19 12-1.5 3"}],["path",{d:"M19.63 18.81 22 20"}],["path",{d:"M6.47 8.23a1.68 1.68 0 0 1 2.44 1.93l-.64 2.08a6.76 6.76 0 0 0 10.16 7.67l.42-.27a1 1 0 1 0-2.73-4.21l-.42.27a1.76 1.76 0 0 1-2.63-1.99l.64-2.08A6.66 6.66 0 0 0 3.94 3.9l-.7.4a1 1 0 1 0 2.55 4.34z"}]],_C=[["rect",{width:"8",height:"8",x:"3",y:"3",rx:"2"}],["path",{d:"M7 11v4a2 2 0 0 0 2 2h4"}],["rect",{width:"8",height:"8",x:"13",y:"13",rx:"2"}]],xC=[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"}]],au=[["path",{d:"M18 6 6 18"}],["path",{d:"m6 6 12 12"}]],tu=[["path",{d:"M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"}],["path",{d:"m10 15 5-3-5-3z"}]],hu=[["path",{d:"M10.513 4.856 13.12 2.17a.5.5 0 0 1 .86.46l-1.377 4.317"}],["path",{d:"M15.656 10H20a1 1 0 0 1 .78 1.63l-1.72 1.773"}],["path",{d:"M16.273 16.273 10.88 21.83a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14H4a1 1 0 0 1-.78-1.63l4.507-4.643"}],["path",{d:"m2 2 20 20"}]],du=[["path",{d:"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"}]],cu=[["circle",{cx:"11",cy:"11",r:"8"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65"}],["line",{x1:"11",x2:"11",y1:"8",y2:"14"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11"}]],Mu=[["circle",{cx:"11",cy:"11",r:"8"}],["line",{x1:"21",x2:"16.65",y1:"21",y2:"16.65"}],["line",{x1:"8",x2:"14",y1:"11",y2:"11"}]];var pu=Object.freeze({__proto__:null,AArrowDown:ka,AArrowUp:Pa,ALargeSmall:Ba,Accessibility:za,Activity:Fa,ActivitySquare:d0,AirVent:Da,Airplay:ba,AlarmCheck:s,AlarmClock:Ta,AlarmClockCheck:s,AlarmClockMinus:g,AlarmClockOff:Ra,AlarmClockPlus:C,AlarmMinus:g,AlarmPlus:C,AlarmSmoke:qa,Album:Ua,AlertCircle:K,AlertOctagon:F2,AlertTriangle:ea,AlignCenter:ha,AlignCenterHorizontal:Oa,AlignCenterVertical:Za,AlignEndHorizontal:Ga,AlignEndVertical:Wa,AlignHorizontalDistributeCenter:Ia,AlignHorizontalDistributeEnd:Xa,AlignHorizontalDistributeStart:Ea,AlignHorizontalJustifyCenter:ja,AlignHorizontalJustifyEnd:Na,AlignHorizontalJustifyStart:Ka,AlignHorizontalSpaceAround:Qa,AlignHorizontalSpaceBetween:Ja,AlignJustify:ca,AlignLeft:y,AlignRight:da,AlignStartHorizontal:Ya,AlignStartVertical:_a,AlignVerticalDistributeCenter:xa,AlignVerticalDistributeEnd:at,AlignVerticalDistributeStart:tt,AlignVerticalJustifyCenter:ht,AlignVerticalJustifyEnd:dt,AlignVerticalJustifyStart:ct,AlignVerticalSpaceAround:Mt,AlignVerticalSpaceBetween:pt,Ambulance:it,Ampersand:nt,Ampersands:lt,Amphora:et,Anchor:rt,Angry:ot,Annoyed:vt,Antenna:$t,Anvil:mt,Aperture:yt,AppWindow:gt,AppWindowMac:st,Apple:Ct,Archive:At,ArchiveRestore:ut,ArchiveX:Ht,AreaChart:b,Armchair:wt,ArrowBigDown:St,ArrowBigDownDash:Vt,ArrowBigLeft:ft,ArrowBigLeftDash:Lt,ArrowBigRight:Pt,ArrowBigRightDash:kt,ArrowBigUp:zt,ArrowBigUpDash:Bt,ArrowDown:Gt,ArrowDown01:Ft,ArrowDown10:Dt,ArrowDownAZ:u,ArrowDownAz:u,ArrowDownCircle:Q,ArrowDownFromLine:bt,ArrowDownLeft:Rt,ArrowDownLeftFromCircle:Y,ArrowDownLeftFromSquare:n0,ArrowDownLeftSquare:c0,ArrowDownNarrowWide:Ut,ArrowDownRight:Tt,ArrowDownRightFromCircle:_,ArrowDownRightFromSquare:l0,ArrowDownRightSquare:M0,ArrowDownSquare:p0,ArrowDownToDot:qt,ArrowDownToLine:Ot,ArrowDownUp:Zt,ArrowDownWideNarrow:H,ArrowDownZA:A,ArrowDownZa:A,ArrowLeft:Xt,ArrowLeftCircle:J,ArrowLeftFromLine:It,ArrowLeftRight:Wt,ArrowLeftSquare:i0,ArrowLeftToLine:Et,ArrowRight:Qt,ArrowRightCircle:t1,ArrowRightFromLine:jt,ArrowRightLeft:Nt,ArrowRightSquare:o0,ArrowRightToLine:Kt,ArrowUp:Mh,ArrowUp01:Jt,ArrowUp10:Yt,ArrowUpAZ:w,ArrowUpAz:w,ArrowUpCircle:h1,ArrowUpDown:_t,ArrowUpFromDot:xt,ArrowUpFromLine:ah,ArrowUpLeft:th,ArrowUpLeftFromCircle:x,ArrowUpLeftFromSquare:e0,ArrowUpLeftSquare:v0,ArrowUpNarrowWide:V,ArrowUpRight:hh,ArrowUpRightFromCircle:a1,ArrowUpRightFromSquare:r0,ArrowUpRightSquare:$0,ArrowUpSquare:m0,ArrowUpToLine:dh,ArrowUpWideNarrow:ch,ArrowUpZA:S,ArrowUpZa:S,ArrowsUpFromLine:ph,Asterisk:ih,AsteriskSquare:y0,AtSign:nh,Atom:lh,AudioLines:eh,AudioWaveform:oh,Award:rh,Axe:vh,Axis3D:L,Axis3d:L,Baby:$h,Backpack:mh,Badge:zh,BadgeAlert:yh,BadgeCent:sh,BadgeCheck:f,BadgeDollarSign:gh,BadgeEuro:Ch,BadgeHelp:k,BadgeIndianRupee:uh,BadgeInfo:Hh,BadgeJapaneseYen:Ah,BadgeMinus:wh,BadgePercent:Vh,BadgePlus:Sh,BadgePoundSterling:Lh,BadgeQuestionMark:k,BadgeRussianRuble:fh,BadgeSwissFranc:kh,BadgeTurkishLira:Ph,BadgeX:Bh,BaggageClaim:Fh,Balloon:Dh,Ban:bh,Banana:Rh,Bandage:Th,Banknote:Zh,BanknoteArrowDown:qh,BanknoteArrowUp:Uh,BanknoteX:Oh,BarChart:W,BarChart2:I,BarChart3:Z,BarChart4:O,BarChartBig:U,BarChartHorizontal:T,BarChartHorizontalBig:R,Barcode:Gh,Barrel:Wh,Baseline:Ih,Bath:Eh,Battery:Yh,BatteryCharging:Xh,BatteryFull:jh,BatteryLow:Nh,BatteryMedium:Qh,BatteryPlus:Kh,BatteryWarning:Jh,Beaker:_h,Bean:a5,BeanOff:xh,Bed:d5,BedDouble:t5,BedSingle:h5,Beef:c5,Beer:p5,BeerOff:M5,Bell:v5,BellDot:i5,BellElectric:n5,BellMinus:l5,BellOff:e5,BellPlus:r5,BellRing:o5,BetweenHorizonalEnd:P,BetweenHorizonalStart:B,BetweenHorizontalEnd:P,BetweenHorizontalStart:B,BetweenVerticalEnd:$5,BetweenVerticalStart:m5,BicepsFlexed:y5,Bike:s5,Binary:g5,Binoculars:u5,Biohazard:C5,Bird:A5,Birdhouse:H5,Bitcoin:w5,Blend:V5,Blinds:S5,Blocks:L5,Bluetooth:B5,BluetoothConnected:f5,BluetoothOff:k5,BluetoothSearching:P5,Bold:z5,Bolt:F5,Bomb:D5,Bone:b5,Book:M4,BookA:R5,BookAlert:T5,BookAudio:q5,BookCheck:U5,BookCopy:O5,BookDashed:z,BookDown:Z5,BookHeadphones:G5,BookHeart:W5,BookImage:E5,BookKey:I5,BookLock:X5,BookMarked:j5,BookMinus:N5,BookOpen:J5,BookOpenCheck:K5,BookOpenText:Q5,BookPlus:Y5,BookSearch:_5,BookTemplate:z,BookText:x5,BookType:t4,BookUp:a4,BookUp2:h4,BookUser:d4,BookX:c4,Bookmark:e4,BookmarkCheck:p4,BookmarkMinus:i4,BookmarkPlus:n4,BookmarkX:l4,BoomBox:r4,Bot:$4,BotMessageSquare:o4,BotOff:v4,BottleWine:m4,BowArrow:y4,Box:s4,BoxSelect:f0,Boxes:g4,Braces:F,Brackets:C4,Brain:A4,BrainCircuit:u4,BrainCog:H4,BrickWall:S4,BrickWallFire:w4,BrickWallShield:V4,Briefcase:P4,BriefcaseBusiness:L4,BriefcaseConveyorBelt:f4,BriefcaseMedical:k4,BringToFront:B4,Brush:F4,BrushCleaning:z4,Bubbles:D4,Bug:T4,BugOff:b4,BugPlay:R4,Building:U4,Building2:q4,Bus:Z4,BusFront:O4,Cable:W4,CableCar:G4,Cake:E4,CakeSlice:I4,Calculator:X4,Calendar:o3,Calendar1:j4,CalendarArrowDown:N4,CalendarArrowUp:K4,CalendarCheck:J4,CalendarCheck2:Q4,CalendarClock:_4,CalendarCog:Y4,CalendarDays:x4,CalendarFold:a3,CalendarHeart:t3,CalendarMinus:d3,CalendarMinus2:h3,CalendarOff:c3,CalendarPlus:p3,CalendarPlus2:M3,CalendarRange:i3,CalendarSearch:n3,CalendarSync:l3,CalendarX:r3,CalendarX2:e3,Calendars:v3,Camera:m3,CameraOff:$3,CandlestickChart:q,Candy:g3,CandyCane:y3,CandyOff:s3,Cannabis:u3,CannabisOff:C3,Captions:D,CaptionsOff:H3,Car:V3,CarFront:A3,CarTaxiFront:w3,Caravan:S3,CardSim:L3,Carrot:f3,CaseLower:k3,CaseSensitive:P3,CaseUpper:B3,CassetteTape:z3,Cast:F3,Castle:D3,Cat:b3,Cctv:R3,ChartArea:b,ChartBar:T,ChartBarBig:R,ChartBarDecreasing:T3,ChartBarIncreasing:q3,ChartBarStacked:U3,ChartCandlestick:q,ChartColumn:Z,ChartColumnBig:U,ChartColumnDecreasing:O3,ChartColumnIncreasing:O,ChartColumnStacked:Z3,ChartGantt:G3,ChartLine:G,ChartNetwork:W3,ChartNoAxesColumn:I,ChartNoAxesColumnDecreasing:I3,ChartNoAxesColumnIncreasing:W,ChartNoAxesCombined:E3,ChartNoAxesGantt:E,ChartPie:X,ChartScatter:j,ChartSpline:X3,Check:K3,CheckCheck:j3,CheckCircle:c1,CheckCircle2:d1,CheckLine:N3,CheckSquare:C0,CheckSquare2:g0,ChefHat:Q3,Cherry:J3,ChessBishop:Y3,ChessKing:_3,ChessKnight:x3,ChessPawn:ad,ChessQueen:td,ChessRook:cd,ChevronDown:hd,ChevronDownCircle:M1,ChevronDownSquare:u0,ChevronFirst:dd,ChevronLast:Md,ChevronLeft:pd,ChevronLeftCircle:p1,ChevronLeftSquare:H0,ChevronRight:id,ChevronRightCircle:i1,ChevronRightSquare:A0,ChevronUp:nd,ChevronUpCircle:n1,ChevronUpSquare:w0,ChevronsDown:ed,ChevronsDownUp:ld,ChevronsLeft:vd,ChevronsLeftRight:od,ChevronsLeftRightEllipsis:rd,ChevronsRight:md,ChevronsRightLeft:$d,ChevronsUp:sd,ChevronsUpDown:yd,Chrome:N,Chromium:N,Church:gd,Cigarette:ud,CigaretteOff:Cd,Circle:Rd,CircleAlert:K,CircleArrowDown:Q,CircleArrowLeft:J,CircleArrowOutDownLeft:Y,CircleArrowOutDownRight:_,CircleArrowOutUpLeft:x,CircleArrowOutUpRight:a1,CircleArrowRight:t1,CircleArrowUp:h1,CircleCheck:d1,CircleCheckBig:c1,CircleChevronDown:M1,CircleChevronLeft:p1,CircleChevronRight:i1,CircleChevronUp:n1,CircleDashed:Hd,CircleDivide:l1,CircleDollarSign:Ad,CircleDot:Vd,CircleDotDashed:wd,CircleEllipsis:Sd,CircleEqual:Ld,CircleFadingArrowUp:fd,CircleFadingPlus:kd,CircleGauge:e1,CircleHelp:l,CircleMinus:r1,CircleOff:Pd,CircleParking:v1,CircleParkingOff:o1,CirclePause:$1,CirclePercent:m1,CirclePile:Bd,CirclePlay:y1,CirclePlus:s1,CirclePoundSterling:zd,CirclePower:g1,CircleQuestionMark:l,CircleSlash:Fd,CircleSlash2:C1,CircleSlashed:C1,CircleSmall:Dd,CircleStar:bd,CircleStop:u1,CircleUser:A1,CircleUserRound:H1,CircleX:w1,CircuitBoard:Td,Citrus:qd,Clapperboard:Ud,Clipboard:Kd,ClipboardCheck:Od,ClipboardClock:Zd,ClipboardCopy:Gd,ClipboardEdit:S1,ClipboardList:Wd,ClipboardMinus:Id,ClipboardPaste:Ed,ClipboardPen:S1,ClipboardPenLine:V1,ClipboardPlus:Xd,ClipboardSignature:V1,ClipboardType:jd,ClipboardX:Nd,Clock:v8,Clock1:Qd,Clock10:Jd,Clock11:Yd,Clock12:_d,Clock2:xd,Clock3:a8,Clock4:t8,Clock5:h8,Clock6:d8,Clock7:c8,Clock8:M8,Clock9:p8,ClockAlert:i8,ClockArrowDown:n8,ClockArrowUp:l8,ClockCheck:e8,ClockFading:r8,ClockPlus:o8,ClosedCaption:$8,Cloud:F8,CloudAlert:m8,CloudBackup:y8,CloudCheck:s8,CloudCog:g8,CloudDownload:L1,CloudDrizzle:C8,CloudFog:u8,CloudHail:H8,CloudLightning:A8,CloudMoon:V8,CloudMoonRain:w8,CloudOff:L8,CloudRain:f8,CloudRainWind:S8,CloudSnow:k8,CloudSun:B8,CloudSunRain:P8,CloudSync:z8,CloudUpload:f1,Cloudy:D8,Clover:b8,Club:R8,Code:T8,Code2:k1,CodeSquare:V0,CodeXml:k1,Codepen:q8,Codesandbox:U8,Coffee:O8,Cog:G8,Coins:Z8,Columns:P1,Columns2:P1,Columns3:B1,Columns3Cog:e,Columns4:W8,ColumnsSettings:e,Combine:I8,Command:E8,Compass:X8,Component:j8,Computer:N8,ConciergeBell:K8,Cone:Q8,Construction:J8,Contact:Y8,Contact2:z1,ContactRound:z1,Container:_8,Contrast:x8,Cookie:a6,CookingPot:t6,Copy:p6,CopyCheck:h6,CopyMinus:d6,CopyPlus:c6,CopySlash:M6,CopyX:i6,Copyleft:n6,Copyright:l6,CornerDownLeft:r6,CornerDownRight:e6,CornerLeftDown:o6,CornerLeftUp:v6,CornerRightDown:$6,CornerRightUp:m6,CornerUpLeft:y6,CornerUpRight:s6,Cpu:g6,CreativeCommons:C6,CreditCard:u6,Croissant:H6,Crop:A6,Cross:w6,Crosshair:V6,Crown:S6,Cuboid:L6,CupSoda:f6,CurlyBraces:F,Currency:k6,Cylinder:P6,Dam:B6,Database:D6,DatabaseBackup:z6,DatabaseZap:F6,DecimalsArrowLeft:b6,DecimalsArrowRight:R6,Delete:T6,Dessert:q6,Diameter:U6,Diamond:W6,DiamondMinus:O6,DiamondPercent:F1,DiamondPlus:Z6,Dice1:G6,Dice2:I6,Dice3:E6,Dice4:N6,Dice5:X6,Dice6:j6,Dices:K6,Diff:Q6,Disc:x6,Disc2:J6,Disc3:Y6,DiscAlbum:_6,Divide:ac,DivideCircle:l1,DivideSquare:k0,Dna:dc,DnaOff:tc,Dock:hc,Dog:cc,DollarSign:Mc,Donut:pc,DoorClosed:nc,DoorClosedLocked:ic,DoorOpen:ec,Dot:lc,DotSquare:P0,Download:rc,DownloadCloud:L1,DraftingCompass:oc,Drama:vc,Dribbble:$c,Drill:mc,Drone:yc,Droplet:gc,DropletOff:sc,Droplets:Cc,Drum:uc,Drumstick:Hc,Dumbbell:Ac,Ear:Vc,EarOff:wc,Earth:D1,EarthLock:Sc,Eclipse:Lc,Edit:i,Edit2:X2,Edit3:E2,Egg:Pc,EggFried:fc,EggOff:kc,Ellipsis:R1,EllipsisVertical:b1,Equal:Dc,EqualApproximately:Bc,EqualNot:zc,EqualSquare:B0,Eraser:Fc,EthernetPort:bc,Euro:Rc,EvCharger:Tc,Expand:Uc,ExternalLink:qc,Eye:Gc,EyeClosed:Oc,EyeOff:Zc,Facebook:Ic,Factory:Wc,Fan:Ec,FastForward:Xc,Feather:jc,Fence:Nc,FerrisWheel:Kc,Figma:Qc,File:V7,FileArchive:Jc,FileAudio:r,FileAudio2:r,FileAxis3D:T1,FileAxis3d:T1,FileBadge:q1,FileBadge2:q1,FileBarChart:Z1,FileBarChart2:G1,FileBox:Yc,FileBraces:O1,FileBracesCorner:U1,FileChartColumn:G1,FileChartColumnIncreasing:Z1,FileChartLine:W1,FileChartPie:I1,FileCheck:_c,FileCheck2:E1,FileCheckCorner:E1,FileClock:xc,FileCode:a7,FileCode2:X1,FileCodeCorner:X1,FileCog:j1,FileCog2:j1,FileDiff:t7,FileDigit:h7,FileDown:d7,FileEdit:_1,FileExclamationPoint:N1,FileHeadphone:r,FileHeart:c7,FileImage:M7,FileInput:p7,FileJson:O1,FileJson2:U1,FileKey:K1,FileKey2:K1,FileLineChart:W1,FileLock:Q1,FileLock2:Q1,FileMinus:n7,FileMinus2:J1,FileMinusCorner:J1,FileMusic:i7,FileOutput:l7,FilePen:_1,FilePenLine:Y1,FilePieChart:I1,FilePlay:x1,FilePlus:e7,FilePlus2:t2,FilePlusCorner:t2,FileQuestion:a2,FileQuestionMark:a2,FileScan:r7,FileSearch:o7,FileSearch2:h2,FileSearchCorner:h2,FileSignal:d2,FileSignature:Y1,FileSliders:$7,FileSpreadsheet:v7,FileStack:m7,FileSymlink:y7,FileTerminal:s7,FileText:g7,FileType:C7,FileType2:c2,FileTypeCorner:c2,FileUp:u7,FileUser:H7,FileVideo:x1,FileVideo2:M2,FileVideoCamera:M2,FileVolume:A7,FileVolume2:d2,FileWarning:N1,FileX:w7,FileX2:p2,FileXCorner:p2,Files:S7,Film:L7,Filter:r2,FilterX:e2,Fingerprint:i2,FingerprintPattern:i2,FireExtinguisher:f7,Fish:B7,FishOff:k7,FishSymbol:P7,FishingHook:z7,Flag:T7,FlagOff:F7,FlagTriangleLeft:D7,FlagTriangleRight:b7,Flame:q7,FlameKindling:R7,Flashlight:O7,FlashlightOff:U7,FlaskConical:W7,FlaskConicalOff:Z7,FlaskRound:G7,FlipHorizontal:I7,FlipHorizontal2:E7,FlipVertical:j7,FlipVertical2:X7,Flower:K7,Flower2:N7,Focus:Q7,FoldHorizontal:J7,FoldVertical:Y7,Folder:SM,FolderArchive:_7,FolderCheck:x7,FolderClock:aM,FolderClosed:tM,FolderCode:hM,FolderCog:n2,FolderCog2:n2,FolderDot:dM,FolderDown:cM,FolderEdit:l2,FolderGit:pM,FolderGit2:MM,FolderHeart:iM,FolderInput:nM,FolderKanban:lM,FolderKey:eM,FolderLock:rM,FolderMinus:oM,FolderOpen:$M,FolderOpenDot:vM,FolderOutput:mM,FolderPen:l2,FolderPlus:yM,FolderRoot:sM,FolderSearch:CM,FolderSearch2:gM,FolderSymlink:uM,FolderSync:HM,FolderTree:AM,FolderUp:wM,FolderX:VM,Folders:LM,Footprints:fM,ForkKnife:Ha,ForkKnifeCrossed:ua,Forklift:kM,Form:PM,FormInput:N2,Forward:BM,Frame:zM,Framer:FM,Frown:DM,Fuel:bM,Fullscreen:RM,FunctionSquare:z0,Funnel:r2,FunnelPlus:TM,FunnelX:e2,GalleryHorizontal:UM,GalleryHorizontalEnd:qM,GalleryThumbnails:OM,GalleryVertical:GM,GalleryVerticalEnd:ZM,Gamepad:EM,Gamepad2:WM,GamepadDirectional:IM,GanttChart:E,GanttChartSquare:m,Gauge:XM,GaugeCircle:e1,Gavel:jM,Gem:NM,GeorgianLari:KM,Ghost:QM,Gift:JM,GitBranch:_M,GitBranchMinus:YM,GitBranchPlus:xM,GitCommit:o2,GitCommitHorizontal:o2,GitCommitVertical:a9,GitCompare:h9,GitCompareArrows:t9,GitFork:d9,GitGraph:c9,GitMerge:M9,GitPullRequest:r9,GitPullRequestArrow:p9,GitPullRequestClosed:i9,GitPullRequestCreate:l9,GitPullRequestCreateArrow:n9,GitPullRequestDraft:e9,Github:o9,Gitlab:v9,GlassWater:$9,Glasses:m9,Globe:s9,Globe2:D1,GlobeLock:y9,Goal:g9,Gpu:C9,Grab:s2,GraduationCap:u9,Grape:H9,Grid:o,Grid2X2:y2,Grid2X2Check:v2,Grid2X2Plus:$2,Grid2X2X:m2,Grid2x2:y2,Grid2x2Check:v2,Grid2x2Plus:$2,Grid2x2X:m2,Grid3X3:o,Grid3x2:A9,Grid3x3:o,Grip:S9,GripHorizontal:w9,GripVertical:V9,Group:L9,Guitar:k9,Ham:f9,Hamburger:P9,Hammer:B9,Hand:T9,HandCoins:z9,HandFist:F9,HandGrab:s2,HandHeart:D9,HandHelping:g2,HandMetal:b9,HandPlatter:R9,Handbag:q9,Handshake:U9,HardDrive:I9,HardDriveDownload:O9,HardDriveUpload:Z9,HardHat:G9,Hash:W9,HatGlasses:E9,Haze:X9,Hd:j9,HdmiPort:N9,Heading:ap,Heading1:K9,Heading2:J9,Heading3:Q9,Heading4:Y9,Heading5:_9,Heading6:x9,HeadphoneOff:tp,Headphones:hp,Headset:dp,Heart:rp,HeartCrack:cp,HeartHandshake:Mp,HeartMinus:pp,HeartOff:ip,HeartPlus:np,HeartPulse:lp,Heater:ep,Helicopter:op,HelpCircle:l,HelpingHand:g2,Hexagon:vp,Highlighter:$p,History:mp,Home:C2,Hop:yp,HopOff:sp,Hospital:gp,Hotel:Cp,Hourglass:up,House:C2,HouseHeart:Hp,HousePlug:Ap,HousePlus:wp,HouseWifi:Vp,IceCream:H2,IceCream2:u2,IceCreamBowl:u2,IceCreamCone:H2,IdCard:Lp,IdCardLanyard:Sp,Image:bp,ImageDown:fp,ImageMinus:kp,ImageOff:Pp,ImagePlay:zp,ImagePlus:Bp,ImageUp:Fp,ImageUpscale:Dp,Images:Rp,Import:Tp,Inbox:qp,Indent:$,IndentDecrease:v,IndentIncrease:$,IndianRupee:Up,Infinity:Op,Info:Zp,Inspect:q0,InspectionPanel:Gp,Instagram:Wp,Italic:Ip,IterationCcw:Ep,IterationCw:Xp,JapaneseYen:jp,Joystick:Np,Kanban:Kp,KanbanSquare:F0,KanbanSquareDashed:S0,Kayak:Qp,Key:_p,KeyRound:Jp,KeySquare:Yp,Keyboard:ti,KeyboardMusic:xp,KeyboardOff:ai,Lamp:ii,LampCeiling:hi,LampDesk:di,LampFloor:ci,LampWallDown:Mi,LampWallUp:pi,LandPlot:ni,Landmark:li,Languages:ei,Laptop:oi,Laptop2:A2,LaptopMinimal:A2,LaptopMinimalCheck:ri,Lasso:$i,LassoSelect:vi,Laugh:mi,Layers:w2,Layers2:yi,Layers3:w2,LayersPlus:si,Layout:I2,LayoutDashboard:gi,LayoutGrid:Ci,LayoutList:ui,LayoutPanelLeft:Hi,LayoutPanelTop:Ai,LayoutTemplate:wi,Leaf:Vi,LeafyGreen:Si,Lectern:Li,LetterText:Ma,Library:ki,LibraryBig:fi,LibrarySquare:D0,LifeBuoy:Pi,Ligature:Bi,Lightbulb:Fi,LightbulbOff:zi,LineChart:G,LineSquiggle:Di,Link:Ti,Link2:bi,Link2Off:Ri,Linkedin:qi,List:hn,ListCheck:Ui,ListChecks:Oi,ListChevronsDownUp:Zi,ListChevronsUpDown:Gi,ListCollapse:Wi,ListEnd:Ii,ListFilter:Xi,ListFilterPlus:Ei,ListIndentDecrease:v,ListIndentIncrease:$,ListMinus:ji,ListMusic:Ni,ListOrdered:Ki,ListPlus:Qi,ListRestart:Ji,ListStart:Yi,ListTodo:_i,ListTree:xi,ListVideo:an,ListX:tn,Loader:cn,Loader2:V2,LoaderCircle:V2,LoaderPinwheel:dn,Locate:nn,LocateFixed:Mn,LocateOff:pn,LocationEdit:k2,Lock:en,LockKeyhole:ln,LockKeyholeOpen:S2,LockOpen:L2,LogIn:rn,LogOut:on,Logs:vn,Lollipop:$n,Luggage:mn,MSquare:b0,Magnet:yn,Mail:Vn,MailCheck:sn,MailMinus:gn,MailOpen:Cn,MailPlus:un,MailQuestion:f2,MailQuestionMark:f2,MailSearch:Hn,MailWarning:An,MailX:wn,Mailbox:Sn,Mails:Ln,Map:Gn,MapMinus:fn,MapPin:Un,MapPinCheck:Pn,MapPinCheckInside:kn,MapPinHouse:Bn,MapPinMinus:Fn,MapPinMinusInside:zn,MapPinOff:Dn,MapPinPen:k2,MapPinPlus:Rn,MapPinPlusInside:bn,MapPinX:qn,MapPinXInside:Tn,MapPinned:On,MapPlus:Zn,Mars:In,MarsStroke:Wn,Martini:En,Maximize:jn,Maximize2:Xn,Medal:Qn,Megaphone:Kn,MegaphoneOff:Nn,Meh:Jn,MemoryStick:Yn,Menu:xn,MenuSquare:R0,Merge:_n,MessageCircle:ll,MessageCircleCode:al,MessageCircleDashed:tl,MessageCircleHeart:hl,MessageCircleMore:dl,MessageCircleOff:cl,MessageCirclePlus:Ml,MessageCircleQuestion:P2,MessageCircleQuestionMark:P2,MessageCircleReply:pl,MessageCircleWarning:il,MessageCircleX:nl,MessageSquare:Sl,MessageSquareCode:el,MessageSquareDashed:rl,MessageSquareDiff:ol,MessageSquareDot:vl,MessageSquareHeart:$l,MessageSquareLock:ml,MessageSquareMore:yl,MessageSquareOff:sl,MessageSquarePlus:gl,MessageSquareQuote:Cl,MessageSquareReply:ul,MessageSquareShare:Hl,MessageSquareText:Al,MessageSquareWarning:wl,MessageSquareX:Vl,MessagesSquare:Ll,Mic:kl,Mic2:B2,MicOff:fl,MicVocal:B2,Microchip:Pl,Microscope:Bl,Microwave:zl,Milestone:Fl,Milk:bl,MilkOff:Dl,Minimize:Tl,Minimize2:Rl,Minus:ql,MinusCircle:r1,MinusSquare:T0,Monitor:Yl,MonitorCheck:Ul,MonitorCloud:Ol,MonitorCog:Zl,MonitorDot:Gl,MonitorDown:Wl,MonitorOff:Il,MonitorPause:El,MonitorPlay:Xl,MonitorSmartphone:jl,MonitorSpeaker:Nl,MonitorStop:Kl,MonitorUp:Ql,MonitorX:Jl,Moon:ae,MoonStar:_l,MoreHorizontal:R1,MoreVertical:b1,Motorbike:xl,Mountain:he,MountainSnow:te,Mouse:le,MouseOff:de,MousePointer:ne,MousePointer2:Me,MousePointer2Off:ce,MousePointerBan:pe,MousePointerClick:ie,MousePointerSquareDashed:L0,Move:Ae,Move3D:z2,Move3d:z2,MoveDiagonal:re,MoveDiagonal2:ee,MoveDown:$e,MoveDownLeft:oe,MoveDownRight:ve,MoveHorizontal:ye,MoveLeft:me,MoveRight:se,MoveUp:ue,MoveUpLeft:ge,MoveUpRight:Ce,MoveVertical:He,Music:Le,Music2:we,Music3:Ve,Music4:Se,Navigation:Be,Navigation2:ke,Navigation2Off:fe,NavigationOff:Pe,Network:Fe,Newspaper:ze,Nfc:De,NonBinary:be,Notebook:Ue,NotebookPen:Re,NotebookTabs:Te,NotebookText:qe,NotepadText:Ze,NotepadTextDashed:Oe,Nut:We,NutOff:Ge,Octagon:Ee,OctagonAlert:F2,OctagonMinus:Ie,OctagonPause:D2,OctagonX:b2,Omega:Xe,Option:je,Orbit:Ne,Origami:Ke,Outdent:v,Package:hr,Package2:Qe,PackageCheck:Je,PackageMinus:Ye,PackageOpen:_e,PackagePlus:xe,PackageSearch:ar,PackageX:tr,PaintBucket:dr,PaintRoller:cr,Paintbrush:Mr,Paintbrush2:R2,PaintbrushVertical:R2,Palette:pr,Palmtree:la,Panda:ir,PanelBottom:er,PanelBottomClose:nr,PanelBottomDashed:T2,PanelBottomInactive:T2,PanelBottomOpen:lr,PanelLeft:Z2,PanelLeftClose:q2,PanelLeftDashed:U2,PanelLeftInactive:U2,PanelLeftOpen:O2,PanelLeftRightDashed:rr,PanelRight:$r,PanelRightClose:or,PanelRightDashed:G2,PanelRightInactive:G2,PanelRightOpen:vr,PanelTop:Cr,PanelTopBottomDashed:mr,PanelTopClose:yr,PanelTopDashed:W2,PanelTopInactive:W2,PanelTopOpen:sr,PanelsLeftBottom:gr,PanelsLeftRight:B1,PanelsRightBottom:ur,PanelsTopBottom:J2,PanelsTopLeft:I2,Paperclip:Hr,Parentheses:wr,ParkingCircle:v1,ParkingCircleOff:o1,ParkingMeter:Ar,ParkingSquare:O0,ParkingSquareOff:U0,PartyPopper:Vr,Pause:Sr,PauseCircle:$1,PauseOctagon:D2,PawPrint:Lr,PcCase:fr,Pen:X2,PenBox:i,PenLine:E2,PenOff:kr,PenSquare:i,PenTool:Pr,Pencil:Dr,PencilLine:Br,PencilOff:zr,PencilRuler:Fr,Pentagon:br,Percent:Rr,PercentCircle:m1,PercentDiamond:F1,PercentSquare:Z0,PersonStanding:Tr,PhilippinePeso:qr,Phone:Er,PhoneCall:Or,PhoneForwarded:Ur,PhoneIncoming:Zr,PhoneMissed:Gr,PhoneOff:Wr,PhoneOutgoing:Ir,Pi:Xr,PiSquare:G0,Piano:jr,Pickaxe:Nr,PictureInPicture:Qr,PictureInPicture2:Kr,PieChart:X,PiggyBank:Jr,Pilcrow:xr,PilcrowLeft:Yr,PilcrowRight:_r,PilcrowSquare:W0,Pill:to,PillBottle:ao,Pin:co,PinOff:ho,Pipette:Mo,Pizza:po,Plane:lo,PlaneLanding:io,PlaneTakeoff:no,Play:eo,PlayCircle:y1,PlaySquare:I0,Plug:oo,Plug2:ro,PlugZap:j2,PlugZap2:j2,Plus:vo,PlusCircle:s1,PlusSquare:E0,Pocket:mo,PocketKnife:$o,Podcast:yo,Pointer:go,PointerOff:so,Popcorn:Co,Popsicle:uo,PoundSterling:Ho,Power:wo,PowerCircle:g1,PowerOff:Ao,PowerSquare:X0,Presentation:Vo,Printer:So,PrinterCheck:Lo,Projector:fo,Proportions:ko,Puzzle:Po,Pyramid:Bo,QrCode:zo,Quote:Fo,Rabbit:Do,Radar:bo,Radiation:Ro,Radical:To,Radio:Oo,RadioReceiver:qo,RadioTower:Uo,Radius:Zo,RailSymbol:Go,Rainbow:Wo,Rat:Io,Ratio:Eo,Receipt:av,ReceiptCent:Xo,ReceiptEuro:jo,ReceiptIndianRupee:No,ReceiptJapaneseYen:Ko,ReceiptPoundSterling:Qo,ReceiptRussianRuble:Jo,ReceiptSwissFranc:Yo,ReceiptText:_o,ReceiptTurkishLira:xo,RectangleCircle:tv,RectangleEllipsis:N2,RectangleGoggles:hv,RectangleHorizontal:dv,RectangleVertical:cv,Recycle:Mv,Redo:nv,Redo2:pv,RedoDot:iv,RefreshCcw:ev,RefreshCcwDot:lv,RefreshCw:ov,RefreshCwOff:rv,Refrigerator:vv,Regex:$v,RemoveFormatting:mv,Repeat:gv,Repeat1:yv,Repeat2:sv,Replace:uv,ReplaceAll:Cv,Reply:Av,ReplyAll:Hv,Rewind:wv,Ribbon:Vv,Rocket:Sv,RockingChair:Lv,RollerCoaster:fv,Rose:kv,Rotate3D:K2,Rotate3d:K2,RotateCcw:zv,RotateCcwKey:Pv,RotateCcwSquare:Bv,RotateCw:Dv,RotateCwSquare:Fv,Route:bv,RouteOff:Rv,Router:Tv,Rows:Q2,Rows2:Q2,Rows3:J2,Rows4:qv,Rss:Uv,Ruler:Zv,RulerDimensionLine:Ov,RussianRuble:Gv,Sailboat:Wv,Salad:Iv,Sandwich:Ev,Satellite:jv,SatelliteDish:Xv,SaudiRiyal:Nv,Save:Jv,SaveAll:Kv,SaveOff:Qv,Scale:Yv,Scale3D:Y2,Scale3d:Y2,Scaling:_v,Scan:i$,ScanBarcode:a$,ScanEye:xv,ScanFace:t$,ScanHeart:h$,ScanLine:d$,ScanQrCode:c$,ScanSearch:M$,ScanText:p$,ScatterChart:j,School:n$,School2:oa,Scissors:e$,ScissorsLineDashed:l$,ScissorsSquare:j0,ScissorsSquareDashedBottom:s0,Scooter:r$,ScreenShare:v$,ScreenShareOff:o$,Scroll:m$,ScrollText:$$,Search:H$,SearchAlert:y$,SearchCheck:g$,SearchCode:s$,SearchSlash:C$,SearchX:u$,Section:A$,Send:V$,SendHorizonal:_2,SendHorizontal:_2,SendToBack:w$,SeparatorHorizontal:S$,SeparatorVertical:L$,Server:B$,ServerCog:f$,ServerCrash:k$,ServerOff:P$,Settings:F$,Settings2:z$,Shapes:D$,Share:R$,Share2:b$,Sheet:T$,Shell:q$,Shield:N$,ShieldAlert:U$,ShieldBan:O$,ShieldCheck:Z$,ShieldClose:a0,ShieldEllipsis:G$,ShieldHalf:W$,ShieldMinus:I$,ShieldOff:E$,ShieldPlus:X$,ShieldQuestion:x2,ShieldQuestionMark:x2,ShieldUser:j$,ShieldX:a0,Ship:Q$,ShipWheel:K$,Shirt:J$,ShoppingBag:Y$,ShoppingBasket:_$,ShoppingCart:x$,Shovel:am,ShowerHead:tm,Shredder:hm,Shrimp:dm,Shrink:cm,Shrub:Mm,Shuffle:pm,Sidebar:Z2,SidebarClose:q2,SidebarOpen:O2,Sigma:nm,SigmaSquare:N0,Signal:om,SignalHigh:im,SignalLow:lm,SignalMedium:em,SignalZero:rm,Signature:vm,Signpost:mm,SignpostBig:$m,Siren:ym,SkipBack:sm,SkipForward:Cm,Skull:gm,Slack:um,Slash:Hm,SlashSquare:K0,Slice:Am,Sliders:t0,SlidersHorizontal:wm,SlidersVertical:t0,Smartphone:Lm,SmartphoneCharging:Vm,SmartphoneNfc:Sm,Smile:km,SmilePlus:fm,Snail:Pm,Snowflake:Bm,SoapDispenserDroplet:zm,Sofa:Fm,SolarPanel:Dm,SortAsc:V,SortDesc:H,Soup:bm,Space:Rm,Spade:Tm,Sparkle:qm,Sparkles:h0,Speaker:Um,Speech:Om,SpellCheck:Gm,SpellCheck2:Zm,Spline:Im,SplinePointer:Wm,Split:Em,SplitSquareHorizontal:Q0,SplitSquareVertical:J0,Spool:Xm,Spotlight:jm,SprayCan:Nm,Sprout:Km,Square:My,SquareActivity:d0,SquareArrowDown:p0,SquareArrowDownLeft:c0,SquareArrowDownRight:M0,SquareArrowLeft:i0,SquareArrowOutDownLeft:n0,SquareArrowOutDownRight:l0,SquareArrowOutUpLeft:e0,SquareArrowOutUpRight:r0,SquareArrowRight:o0,SquareArrowUp:m0,SquareArrowUpLeft:v0,SquareArrowUpRight:$0,SquareAsterisk:y0,SquareBottomDashedScissors:s0,SquareChartGantt:m,SquareCheck:g0,SquareCheckBig:C0,SquareChevronDown:u0,SquareChevronLeft:H0,SquareChevronRight:A0,SquareChevronUp:w0,SquareCode:V0,SquareDashed:f0,SquareDashedBottom:Jm,SquareDashedBottomCode:Qm,SquareDashedKanban:S0,SquareDashedMousePointer:L0,SquareDashedTopSolid:Ym,SquareDivide:k0,SquareDot:P0,SquareEqual:B0,SquareFunction:z0,SquareGanttChart:m,SquareKanban:F0,SquareLibrary:D0,SquareM:b0,SquareMenu:R0,SquareMinus:T0,SquareMousePointer:q0,SquareParking:O0,SquareParkingOff:U0,SquarePause:_m,SquarePen:i,SquarePercent:Z0,SquarePi:G0,SquarePilcrow:W0,SquarePlay:I0,SquarePlus:E0,SquarePower:X0,SquareRadical:xm,SquareRoundCorner:ay,SquareScissors:j0,SquareSigma:N0,SquareSlash:K0,SquareSplitHorizontal:Q0,SquareSplitVertical:J0,SquareSquare:ty,SquareStack:hy,SquareStar:dy,SquareStop:cy,SquareTerminal:Y0,SquareUser:x0,SquareUserRound:_0,SquareX:aa,SquaresExclude:py,SquaresIntersect:iy,SquaresSubtract:ny,SquaresUnite:ey,Squircle:ry,SquircleDashed:ly,Squirrel:oy,Stamp:vy,Star:yy,StarHalf:my,StarOff:$y,Stars:h0,StepBack:sy,StepForward:gy,Stethoscope:Cy,Sticker:uy,StickyNote:Ay,Stone:Hy,StopCircle:u1,Store:wy,StretchHorizontal:Vy,StretchVertical:Sy,Strikethrough:Ly,Subscript:fy,Subtitles:D,Sun:Fy,SunDim:ky,SunMedium:Py,SunMoon:By,SunSnow:zy,Sunrise:Dy,Sunset:by,Superscript:Ry,SwatchBook:Ty,SwissFranc:qy,SwitchCamera:Uy,Sword:Oy,Swords:Zy,Syringe:Gy,Table:Qy,Table2:Wy,TableCellsMerge:Iy,TableCellsSplit:Ey,TableColumnsSplit:jy,TableConfig:e,TableOfContents:Xy,TableProperties:Ny,TableRowsSplit:Ky,Tablet:Yy,TabletSmartphone:Jy,Tablets:_y,Tag:xy,Tags:as,Tally1:ts,Tally2:hs,Tally3:ds,Tally4:cs,Tally5:Ms,Tangent:ps,Target:is,Telescope:ns,Tent:es,TentTree:ls,Terminal:rs,TerminalSquare:Y0,TestTube:os,TestTube2:ta,TestTubeDiagonal:ta,TestTubes:vs,Text:y,TextAlignCenter:ha,TextAlignEnd:da,TextAlignJustify:ca,TextAlignStart:y,TextCursor:ms,TextCursorInput:$s,TextInitial:Ma,TextQuote:ys,TextSearch:ss,TextSelect:pa,TextSelection:pa,TextWrap:ia,Theater:gs,Thermometer:Hs,ThermometerSnowflake:Cs,ThermometerSun:us,ThumbsDown:As,ThumbsUp:ws,Ticket:Bs,TicketCheck:Vs,TicketMinus:Ss,TicketPercent:Ls,TicketPlus:fs,TicketSlash:ks,TicketX:Ps,Tickets:Fs,TicketsPlane:zs,Timer:Rs,TimerOff:bs,TimerReset:Ds,ToggleLeft:Ts,ToggleRight:qs,Toilet:Us,ToolCase:Os,Toolbox:Zs,Tornado:Gs,Torus:Ws,Touchpad:Es,TouchpadOff:Is,TowerControl:Xs,ToyBrick:js,Tractor:Ks,TrafficCone:Ns,Train:na,TrainFront:Js,TrainFrontTunnel:Qs,TrainTrack:Ys,TramFront:na,Transgender:_s,Trash:ag,Trash2:xs,TreeDeciduous:tg,TreePalm:la,TreePine:hg,Trees:dg,Trello:cg,TrendingDown:Mg,TrendingUp:ig,TrendingUpDown:pg,Triangle:eg,TriangleAlert:ea,TriangleDashed:ng,TriangleRight:lg,Trophy:rg,Truck:vg,TruckElectric:og,TurkishLira:$g,Turntable:mg,Turtle:yg,Tv:gg,Tv2:ra,TvMinimal:ra,TvMinimalPlay:sg,Twitch:Cg,Twitter:ug,Type:Ag,TypeOutline:Hg,Umbrella:Vg,UmbrellaOff:wg,Underline:Sg,Undo:kg,Undo2:Lg,UndoDot:fg,UnfoldHorizontal:Pg,UnfoldVertical:Bg,Ungroup:zg,University:oa,Unlink:Dg,Unlink2:Fg,Unlock:L2,UnlockKeyhole:S2,Unplug:bg,Upload:Tg,UploadCloud:f1,Usb:Rg,User:Kg,User2:sa,UserCheck:qg,UserCheck2:va,UserCircle:A1,UserCircle2:H1,UserCog:Ug,UserCog2:$a,UserLock:Og,UserMinus:Zg,UserMinus2:ma,UserPen:Gg,UserPlus:Wg,UserPlus2:ya,UserRound:sa,UserRoundCheck:va,UserRoundCog:$a,UserRoundMinus:ma,UserRoundPen:Ig,UserRoundPlus:ya,UserRoundSearch:Eg,UserRoundX:ga,UserSearch:Xg,UserSquare:x0,UserSquare2:_0,UserStar:jg,UserX:Ng,UserX2:ga,Users:Qg,Users2:Ca,UsersRound:Ca,Utensils:Ha,UtensilsCrossed:ua,UtilityPole:Yg,Van:Jg,Variable:_g,Vault:xg,VectorSquare:aC,Vegan:tC,VenetianMask:hC,Venus:dC,VenusAndMars:cC,Verified:f,Vibrate:pC,VibrateOff:MC,Video:nC,VideoOff:iC,Videotape:eC,View:rC,Voicemail:lC,Volleyball:oC,Volume:sC,Volume1:vC,Volume2:$C,VolumeOff:mC,VolumeX:yC,Vote:gC,Wallet:uC,Wallet2:Aa,WalletCards:CC,WalletMinimal:Aa,Wallpaper:HC,Wand:AC,Wand2:wa,WandSparkles:wa,Warehouse:wC,WashingMachine:VC,Watch:SC,Waves:PC,WavesArrowDown:LC,WavesArrowUp:fC,WavesLadder:kC,Waypoints:BC,Webcam:zC,Webhook:DC,WebhookOff:FC,Weight:RC,WeightTilde:bC,Wheat:qC,WheatOff:TC,WholeWord:UC,Wifi:jC,WifiCog:OC,WifiHigh:GC,WifiLow:ZC,WifiOff:WC,WifiPen:IC,WifiSync:EC,WifiZero:XC,Wind:KC,WindArrowDown:NC,Wine:JC,WineOff:QC,Workflow:_C,Worm:YC,WrapText:ia,Wrench:xC,X:au,XCircle:w1,XOctagon:b2,XSquare:aa,Youtube:tu,Zap:du,ZapOff:hu,ZoomIn:cu,ZoomOut:Mu});const iu=({icons:t=pu,nameAttr:h="data-lucide",attrs:d={},root:c=document,inTemplates:M}={})=>{if(!Object.values(t).length)throw new Error(`Please provide an icons object. 9 + If you want to use all the icons you can import it like: 10 + \`import { createIcons, icons } from 'lucide'; 11 + lucide.createIcons({icons});\``);if(typeof c>"u")throw new Error("`createIcons()` only works in a browser environment.");if(Array.from(c.querySelectorAll(`[${h}]`)).forEach(p=>fa(p,{nameAttr:h,icons:t,attrs:d})),M&&Array.from(c.querySelectorAll("template")).forEach(p=>iu({icons:t,nameAttr:h,attrs:d,root:p.content,inTemplates:M})),h==="data-lucide"){const p=c.querySelectorAll("[icon-name]");p.length>0&&(console.warn("[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide"),Array.from(p).forEach(Va=>fa(Va,{nameAttr:"icon-name",icons:t,attrs:d})))}};a.AArrowDown=ka,a.AArrowUp=Pa,a.ALargeSmall=Ba,a.Accessibility=za,a.Activity=Fa,a.ActivitySquare=d0,a.AirVent=Da,a.Airplay=ba,a.AlarmCheck=s,a.AlarmClock=Ta,a.AlarmClockCheck=s,a.AlarmClockMinus=g,a.AlarmClockOff=Ra,a.AlarmClockPlus=C,a.AlarmMinus=g,a.AlarmPlus=C,a.AlarmSmoke=qa,a.Album=Ua,a.AlertCircle=K,a.AlertOctagon=F2,a.AlertTriangle=ea,a.AlignCenter=ha,a.AlignCenterHorizontal=Oa,a.AlignCenterVertical=Za,a.AlignEndHorizontal=Ga,a.AlignEndVertical=Wa,a.AlignHorizontalDistributeCenter=Ia,a.AlignHorizontalDistributeEnd=Xa,a.AlignHorizontalDistributeStart=Ea,a.AlignHorizontalJustifyCenter=ja,a.AlignHorizontalJustifyEnd=Na,a.AlignHorizontalJustifyStart=Ka,a.AlignHorizontalSpaceAround=Qa,a.AlignHorizontalSpaceBetween=Ja,a.AlignJustify=ca,a.AlignLeft=y,a.AlignRight=da,a.AlignStartHorizontal=Ya,a.AlignStartVertical=_a,a.AlignVerticalDistributeCenter=xa,a.AlignVerticalDistributeEnd=at,a.AlignVerticalDistributeStart=tt,a.AlignVerticalJustifyCenter=ht,a.AlignVerticalJustifyEnd=dt,a.AlignVerticalJustifyStart=ct,a.AlignVerticalSpaceAround=Mt,a.AlignVerticalSpaceBetween=pt,a.Ambulance=it,a.Ampersand=nt,a.Ampersands=lt,a.Amphora=et,a.Anchor=rt,a.Angry=ot,a.Annoyed=vt,a.Antenna=$t,a.Anvil=mt,a.Aperture=yt,a.AppWindow=gt,a.AppWindowMac=st,a.Apple=Ct,a.Archive=At,a.ArchiveRestore=ut,a.ArchiveX=Ht,a.AreaChart=b,a.Armchair=wt,a.ArrowBigDown=St,a.ArrowBigDownDash=Vt,a.ArrowBigLeft=ft,a.ArrowBigLeftDash=Lt,a.ArrowBigRight=Pt,a.ArrowBigRightDash=kt,a.ArrowBigUp=zt,a.ArrowBigUpDash=Bt,a.ArrowDown=Gt,a.ArrowDown01=Ft,a.ArrowDown10=Dt,a.ArrowDownAZ=u,a.ArrowDownAz=u,a.ArrowDownCircle=Q,a.ArrowDownFromLine=bt,a.ArrowDownLeft=Rt,a.ArrowDownLeftFromCircle=Y,a.ArrowDownLeftFromSquare=n0,a.ArrowDownLeftSquare=c0,a.ArrowDownNarrowWide=Ut,a.ArrowDownRight=Tt,a.ArrowDownRightFromCircle=_,a.ArrowDownRightFromSquare=l0,a.ArrowDownRightSquare=M0,a.ArrowDownSquare=p0,a.ArrowDownToDot=qt,a.ArrowDownToLine=Ot,a.ArrowDownUp=Zt,a.ArrowDownWideNarrow=H,a.ArrowDownZA=A,a.ArrowDownZa=A,a.ArrowLeft=Xt,a.ArrowLeftCircle=J,a.ArrowLeftFromLine=It,a.ArrowLeftRight=Wt,a.ArrowLeftSquare=i0,a.ArrowLeftToLine=Et,a.ArrowRight=Qt,a.ArrowRightCircle=t1,a.ArrowRightFromLine=jt,a.ArrowRightLeft=Nt,a.ArrowRightSquare=o0,a.ArrowRightToLine=Kt,a.ArrowUp=Mh,a.ArrowUp01=Jt,a.ArrowUp10=Yt,a.ArrowUpAZ=w,a.ArrowUpAz=w,a.ArrowUpCircle=h1,a.ArrowUpDown=_t,a.ArrowUpFromDot=xt,a.ArrowUpFromLine=ah,a.ArrowUpLeft=th,a.ArrowUpLeftFromCircle=x,a.ArrowUpLeftFromSquare=e0,a.ArrowUpLeftSquare=v0,a.ArrowUpNarrowWide=V,a.ArrowUpRight=hh,a.ArrowUpRightFromCircle=a1,a.ArrowUpRightFromSquare=r0,a.ArrowUpRightSquare=$0,a.ArrowUpSquare=m0,a.ArrowUpToLine=dh,a.ArrowUpWideNarrow=ch,a.ArrowUpZA=S,a.ArrowUpZa=S,a.ArrowsUpFromLine=ph,a.Asterisk=ih,a.AsteriskSquare=y0,a.AtSign=nh,a.Atom=lh,a.AudioLines=eh,a.AudioWaveform=oh,a.Award=rh,a.Axe=vh,a.Axis3D=L,a.Axis3d=L,a.Baby=$h,a.Backpack=mh,a.Badge=zh,a.BadgeAlert=yh,a.BadgeCent=sh,a.BadgeCheck=f,a.BadgeDollarSign=gh,a.BadgeEuro=Ch,a.BadgeHelp=k,a.BadgeIndianRupee=uh,a.BadgeInfo=Hh,a.BadgeJapaneseYen=Ah,a.BadgeMinus=wh,a.BadgePercent=Vh,a.BadgePlus=Sh,a.BadgePoundSterling=Lh,a.BadgeQuestionMark=k,a.BadgeRussianRuble=fh,a.BadgeSwissFranc=kh,a.BadgeTurkishLira=Ph,a.BadgeX=Bh,a.BaggageClaim=Fh,a.Balloon=Dh,a.Ban=bh,a.Banana=Rh,a.Bandage=Th,a.Banknote=Zh,a.BanknoteArrowDown=qh,a.BanknoteArrowUp=Uh,a.BanknoteX=Oh,a.BarChart=W,a.BarChart2=I,a.BarChart3=Z,a.BarChart4=O,a.BarChartBig=U,a.BarChartHorizontal=T,a.BarChartHorizontalBig=R,a.Barcode=Gh,a.Barrel=Wh,a.Baseline=Ih,a.Bath=Eh,a.Battery=Yh,a.BatteryCharging=Xh,a.BatteryFull=jh,a.BatteryLow=Nh,a.BatteryMedium=Qh,a.BatteryPlus=Kh,a.BatteryWarning=Jh,a.Beaker=_h,a.Bean=a5,a.BeanOff=xh,a.Bed=d5,a.BedDouble=t5,a.BedSingle=h5,a.Beef=c5,a.Beer=p5,a.BeerOff=M5,a.Bell=v5,a.BellDot=i5,a.BellElectric=n5,a.BellMinus=l5,a.BellOff=e5,a.BellPlus=r5,a.BellRing=o5,a.BetweenHorizonalEnd=P,a.BetweenHorizonalStart=B,a.BetweenHorizontalEnd=P,a.BetweenHorizontalStart=B,a.BetweenVerticalEnd=$5,a.BetweenVerticalStart=m5,a.BicepsFlexed=y5,a.Bike=s5,a.Binary=g5,a.Binoculars=u5,a.Biohazard=C5,a.Bird=A5,a.Birdhouse=H5,a.Bitcoin=w5,a.Blend=V5,a.Blinds=S5,a.Blocks=L5,a.Bluetooth=B5,a.BluetoothConnected=f5,a.BluetoothOff=k5,a.BluetoothSearching=P5,a.Bold=z5,a.Bolt=F5,a.Bomb=D5,a.Bone=b5,a.Book=M4,a.BookA=R5,a.BookAlert=T5,a.BookAudio=q5,a.BookCheck=U5,a.BookCopy=O5,a.BookDashed=z,a.BookDown=Z5,a.BookHeadphones=G5,a.BookHeart=W5,a.BookImage=E5,a.BookKey=I5,a.BookLock=X5,a.BookMarked=j5,a.BookMinus=N5,a.BookOpen=J5,a.BookOpenCheck=K5,a.BookOpenText=Q5,a.BookPlus=Y5,a.BookSearch=_5,a.BookTemplate=z,a.BookText=x5,a.BookType=t4,a.BookUp=a4,a.BookUp2=h4,a.BookUser=d4,a.BookX=c4,a.Bookmark=e4,a.BookmarkCheck=p4,a.BookmarkMinus=i4,a.BookmarkPlus=n4,a.BookmarkX=l4,a.BoomBox=r4,a.Bot=$4,a.BotMessageSquare=o4,a.BotOff=v4,a.BottleWine=m4,a.BowArrow=y4,a.Box=s4,a.BoxSelect=f0,a.Boxes=g4,a.Braces=F,a.Brackets=C4,a.Brain=A4,a.BrainCircuit=u4,a.BrainCog=H4,a.BrickWall=S4,a.BrickWallFire=w4,a.BrickWallShield=V4,a.Briefcase=P4,a.BriefcaseBusiness=L4,a.BriefcaseConveyorBelt=f4,a.BriefcaseMedical=k4,a.BringToFront=B4,a.Brush=F4,a.BrushCleaning=z4,a.Bubbles=D4,a.Bug=T4,a.BugOff=b4,a.BugPlay=R4,a.Building=U4,a.Building2=q4,a.Bus=Z4,a.BusFront=O4,a.Cable=W4,a.CableCar=G4,a.Cake=E4,a.CakeSlice=I4,a.Calculator=X4,a.Calendar=o3,a.Calendar1=j4,a.CalendarArrowDown=N4,a.CalendarArrowUp=K4,a.CalendarCheck=J4,a.CalendarCheck2=Q4,a.CalendarClock=_4,a.CalendarCog=Y4,a.CalendarDays=x4,a.CalendarFold=a3,a.CalendarHeart=t3,a.CalendarMinus=d3,a.CalendarMinus2=h3,a.CalendarOff=c3,a.CalendarPlus=p3,a.CalendarPlus2=M3,a.CalendarRange=i3,a.CalendarSearch=n3,a.CalendarSync=l3,a.CalendarX=r3,a.CalendarX2=e3,a.Calendars=v3,a.Camera=m3,a.CameraOff=$3,a.CandlestickChart=q,a.Candy=g3,a.CandyCane=y3,a.CandyOff=s3,a.Cannabis=u3,a.CannabisOff=C3,a.Captions=D,a.CaptionsOff=H3,a.Car=V3,a.CarFront=A3,a.CarTaxiFront=w3,a.Caravan=S3,a.CardSim=L3,a.Carrot=f3,a.CaseLower=k3,a.CaseSensitive=P3,a.CaseUpper=B3,a.CassetteTape=z3,a.Cast=F3,a.Castle=D3,a.Cat=b3,a.Cctv=R3,a.ChartArea=b,a.ChartBar=T,a.ChartBarBig=R,a.ChartBarDecreasing=T3,a.ChartBarIncreasing=q3,a.ChartBarStacked=U3,a.ChartCandlestick=q,a.ChartColumn=Z,a.ChartColumnBig=U,a.ChartColumnDecreasing=O3,a.ChartColumnIncreasing=O,a.ChartColumnStacked=Z3,a.ChartGantt=G3,a.ChartLine=G,a.ChartNetwork=W3,a.ChartNoAxesColumn=I,a.ChartNoAxesColumnDecreasing=I3,a.ChartNoAxesColumnIncreasing=W,a.ChartNoAxesCombined=E3,a.ChartNoAxesGantt=E,a.ChartPie=X,a.ChartScatter=j,a.ChartSpline=X3,a.Check=K3,a.CheckCheck=j3,a.CheckCircle=c1,a.CheckCircle2=d1,a.CheckLine=N3,a.CheckSquare=C0,a.CheckSquare2=g0,a.ChefHat=Q3,a.Cherry=J3,a.ChessBishop=Y3,a.ChessKing=_3,a.ChessKnight=x3,a.ChessPawn=ad,a.ChessQueen=td,a.ChessRook=cd,a.ChevronDown=hd,a.ChevronDownCircle=M1,a.ChevronDownSquare=u0,a.ChevronFirst=dd,a.ChevronLast=Md,a.ChevronLeft=pd,a.ChevronLeftCircle=p1,a.ChevronLeftSquare=H0,a.ChevronRight=id,a.ChevronRightCircle=i1,a.ChevronRightSquare=A0,a.ChevronUp=nd,a.ChevronUpCircle=n1,a.ChevronUpSquare=w0,a.ChevronsDown=ed,a.ChevronsDownUp=ld,a.ChevronsLeft=vd,a.ChevronsLeftRight=od,a.ChevronsLeftRightEllipsis=rd,a.ChevronsRight=md,a.ChevronsRightLeft=$d,a.ChevronsUp=sd,a.ChevronsUpDown=yd,a.Chrome=N,a.Chromium=N,a.Church=gd,a.Cigarette=ud,a.CigaretteOff=Cd,a.Circle=Rd,a.CircleAlert=K,a.CircleArrowDown=Q,a.CircleArrowLeft=J,a.CircleArrowOutDownLeft=Y,a.CircleArrowOutDownRight=_,a.CircleArrowOutUpLeft=x,a.CircleArrowOutUpRight=a1,a.CircleArrowRight=t1,a.CircleArrowUp=h1,a.CircleCheck=d1,a.CircleCheckBig=c1,a.CircleChevronDown=M1,a.CircleChevronLeft=p1,a.CircleChevronRight=i1,a.CircleChevronUp=n1,a.CircleDashed=Hd,a.CircleDivide=l1,a.CircleDollarSign=Ad,a.CircleDot=Vd,a.CircleDotDashed=wd,a.CircleEllipsis=Sd,a.CircleEqual=Ld,a.CircleFadingArrowUp=fd,a.CircleFadingPlus=kd,a.CircleGauge=e1,a.CircleHelp=l,a.CircleMinus=r1,a.CircleOff=Pd,a.CircleParking=v1,a.CircleParkingOff=o1,a.CirclePause=$1,a.CirclePercent=m1,a.CirclePile=Bd,a.CirclePlay=y1,a.CirclePlus=s1,a.CirclePoundSterling=zd,a.CirclePower=g1,a.CircleQuestionMark=l,a.CircleSlash=Fd,a.CircleSlash2=C1,a.CircleSlashed=C1,a.CircleSmall=Dd,a.CircleStar=bd,a.CircleStop=u1,a.CircleUser=A1,a.CircleUserRound=H1,a.CircleX=w1,a.CircuitBoard=Td,a.Citrus=qd,a.Clapperboard=Ud,a.Clipboard=Kd,a.ClipboardCheck=Od,a.ClipboardClock=Zd,a.ClipboardCopy=Gd,a.ClipboardEdit=S1,a.ClipboardList=Wd,a.ClipboardMinus=Id,a.ClipboardPaste=Ed,a.ClipboardPen=S1,a.ClipboardPenLine=V1,a.ClipboardPlus=Xd,a.ClipboardSignature=V1,a.ClipboardType=jd,a.ClipboardX=Nd,a.Clock=v8,a.Clock1=Qd,a.Clock10=Jd,a.Clock11=Yd,a.Clock12=_d,a.Clock2=xd,a.Clock3=a8,a.Clock4=t8,a.Clock5=h8,a.Clock6=d8,a.Clock7=c8,a.Clock8=M8,a.Clock9=p8,a.ClockAlert=i8,a.ClockArrowDown=n8,a.ClockArrowUp=l8,a.ClockCheck=e8,a.ClockFading=r8,a.ClockPlus=o8,a.ClosedCaption=$8,a.Cloud=F8,a.CloudAlert=m8,a.CloudBackup=y8,a.CloudCheck=s8,a.CloudCog=g8,a.CloudDownload=L1,a.CloudDrizzle=C8,a.CloudFog=u8,a.CloudHail=H8,a.CloudLightning=A8,a.CloudMoon=V8,a.CloudMoonRain=w8,a.CloudOff=L8,a.CloudRain=f8,a.CloudRainWind=S8,a.CloudSnow=k8,a.CloudSun=B8,a.CloudSunRain=P8,a.CloudSync=z8,a.CloudUpload=f1,a.Cloudy=D8,a.Clover=b8,a.Club=R8,a.Code=T8,a.Code2=k1,a.CodeSquare=V0,a.CodeXml=k1,a.Codepen=q8,a.Codesandbox=U8,a.Coffee=O8,a.Cog=G8,a.Coins=Z8,a.Columns=P1,a.Columns2=P1,a.Columns3=B1,a.Columns3Cog=e,a.Columns4=W8,a.ColumnsSettings=e,a.Combine=I8,a.Command=E8,a.Compass=X8,a.Component=j8,a.Computer=N8,a.ConciergeBell=K8,a.Cone=Q8,a.Construction=J8,a.Contact=Y8,a.Contact2=z1,a.ContactRound=z1,a.Container=_8,a.Contrast=x8,a.Cookie=a6,a.CookingPot=t6,a.Copy=p6,a.CopyCheck=h6,a.CopyMinus=d6,a.CopyPlus=c6,a.CopySlash=M6,a.CopyX=i6,a.Copyleft=n6,a.Copyright=l6,a.CornerDownLeft=r6,a.CornerDownRight=e6,a.CornerLeftDown=o6,a.CornerLeftUp=v6,a.CornerRightDown=$6,a.CornerRightUp=m6,a.CornerUpLeft=y6,a.CornerUpRight=s6,a.Cpu=g6,a.CreativeCommons=C6,a.CreditCard=u6,a.Croissant=H6,a.Crop=A6,a.Cross=w6,a.Crosshair=V6,a.Crown=S6,a.Cuboid=L6,a.CupSoda=f6,a.CurlyBraces=F,a.Currency=k6,a.Cylinder=P6,a.Dam=B6,a.Database=D6,a.DatabaseBackup=z6,a.DatabaseZap=F6,a.DecimalsArrowLeft=b6,a.DecimalsArrowRight=R6,a.Delete=T6,a.Dessert=q6,a.Diameter=U6,a.Diamond=W6,a.DiamondMinus=O6,a.DiamondPercent=F1,a.DiamondPlus=Z6,a.Dice1=G6,a.Dice2=I6,a.Dice3=E6,a.Dice4=N6,a.Dice5=X6,a.Dice6=j6,a.Dices=K6,a.Diff=Q6,a.Disc=x6,a.Disc2=J6,a.Disc3=Y6,a.DiscAlbum=_6,a.Divide=ac,a.DivideCircle=l1,a.DivideSquare=k0,a.Dna=dc,a.DnaOff=tc,a.Dock=hc,a.Dog=cc,a.DollarSign=Mc,a.Donut=pc,a.DoorClosed=nc,a.DoorClosedLocked=ic,a.DoorOpen=ec,a.Dot=lc,a.DotSquare=P0,a.Download=rc,a.DownloadCloud=L1,a.DraftingCompass=oc,a.Drama=vc,a.Dribbble=$c,a.Drill=mc,a.Drone=yc,a.Droplet=gc,a.DropletOff=sc,a.Droplets=Cc,a.Drum=uc,a.Drumstick=Hc,a.Dumbbell=Ac,a.Ear=Vc,a.EarOff=wc,a.Earth=D1,a.EarthLock=Sc,a.Eclipse=Lc,a.Edit=i,a.Edit2=X2,a.Edit3=E2,a.Egg=Pc,a.EggFried=fc,a.EggOff=kc,a.Ellipsis=R1,a.EllipsisVertical=b1,a.Equal=Dc,a.EqualApproximately=Bc,a.EqualNot=zc,a.EqualSquare=B0,a.Eraser=Fc,a.EthernetPort=bc,a.Euro=Rc,a.EvCharger=Tc,a.Expand=Uc,a.ExternalLink=qc,a.Eye=Gc,a.EyeClosed=Oc,a.EyeOff=Zc,a.Facebook=Ic,a.Factory=Wc,a.Fan=Ec,a.FastForward=Xc,a.Feather=jc,a.Fence=Nc,a.FerrisWheel=Kc,a.Figma=Qc,a.File=V7,a.FileArchive=Jc,a.FileAudio=r,a.FileAudio2=r,a.FileAxis3D=T1,a.FileAxis3d=T1,a.FileBadge=q1,a.FileBadge2=q1,a.FileBarChart=Z1,a.FileBarChart2=G1,a.FileBox=Yc,a.FileBraces=O1,a.FileBracesCorner=U1,a.FileChartColumn=G1,a.FileChartColumnIncreasing=Z1,a.FileChartLine=W1,a.FileChartPie=I1,a.FileCheck=_c,a.FileCheck2=E1,a.FileCheckCorner=E1,a.FileClock=xc,a.FileCode=a7,a.FileCode2=X1,a.FileCodeCorner=X1,a.FileCog=j1,a.FileCog2=j1,a.FileDiff=t7,a.FileDigit=h7,a.FileDown=d7,a.FileEdit=_1,a.FileExclamationPoint=N1,a.FileHeadphone=r,a.FileHeart=c7,a.FileImage=M7,a.FileInput=p7,a.FileJson=O1,a.FileJson2=U1,a.FileKey=K1,a.FileKey2=K1,a.FileLineChart=W1,a.FileLock=Q1,a.FileLock2=Q1,a.FileMinus=n7,a.FileMinus2=J1,a.FileMinusCorner=J1,a.FileMusic=i7,a.FileOutput=l7,a.FilePen=_1,a.FilePenLine=Y1,a.FilePieChart=I1,a.FilePlay=x1,a.FilePlus=e7,a.FilePlus2=t2,a.FilePlusCorner=t2,a.FileQuestion=a2,a.FileQuestionMark=a2,a.FileScan=r7,a.FileSearch=o7,a.FileSearch2=h2,a.FileSearchCorner=h2,a.FileSignal=d2,a.FileSignature=Y1,a.FileSliders=$7,a.FileSpreadsheet=v7,a.FileStack=m7,a.FileSymlink=y7,a.FileTerminal=s7,a.FileText=g7,a.FileType=C7,a.FileType2=c2,a.FileTypeCorner=c2,a.FileUp=u7,a.FileUser=H7,a.FileVideo=x1,a.FileVideo2=M2,a.FileVideoCamera=M2,a.FileVolume=A7,a.FileVolume2=d2,a.FileWarning=N1,a.FileX=w7,a.FileX2=p2,a.FileXCorner=p2,a.Files=S7,a.Film=L7,a.Filter=r2,a.FilterX=e2,a.Fingerprint=i2,a.FingerprintPattern=i2,a.FireExtinguisher=f7,a.Fish=B7,a.FishOff=k7,a.FishSymbol=P7,a.FishingHook=z7,a.Flag=T7,a.FlagOff=F7,a.FlagTriangleLeft=D7,a.FlagTriangleRight=b7,a.Flame=q7,a.FlameKindling=R7,a.Flashlight=O7,a.FlashlightOff=U7,a.FlaskConical=W7,a.FlaskConicalOff=Z7,a.FlaskRound=G7,a.FlipHorizontal=I7,a.FlipHorizontal2=E7,a.FlipVertical=j7,a.FlipVertical2=X7,a.Flower=K7,a.Flower2=N7,a.Focus=Q7,a.FoldHorizontal=J7,a.FoldVertical=Y7,a.Folder=SM,a.FolderArchive=_7,a.FolderCheck=x7,a.FolderClock=aM,a.FolderClosed=tM,a.FolderCode=hM,a.FolderCog=n2,a.FolderCog2=n2,a.FolderDot=dM,a.FolderDown=cM,a.FolderEdit=l2,a.FolderGit=pM,a.FolderGit2=MM,a.FolderHeart=iM,a.FolderInput=nM,a.FolderKanban=lM,a.FolderKey=eM,a.FolderLock=rM,a.FolderMinus=oM,a.FolderOpen=$M,a.FolderOpenDot=vM,a.FolderOutput=mM,a.FolderPen=l2,a.FolderPlus=yM,a.FolderRoot=sM,a.FolderSearch=CM,a.FolderSearch2=gM,a.FolderSymlink=uM,a.FolderSync=HM,a.FolderTree=AM,a.FolderUp=wM,a.FolderX=VM,a.Folders=LM,a.Footprints=fM,a.ForkKnife=Ha,a.ForkKnifeCrossed=ua,a.Forklift=kM,a.Form=PM,a.FormInput=N2,a.Forward=BM,a.Frame=zM,a.Framer=FM,a.Frown=DM,a.Fuel=bM,a.Fullscreen=RM,a.FunctionSquare=z0,a.Funnel=r2,a.FunnelPlus=TM,a.FunnelX=e2,a.GalleryHorizontal=UM,a.GalleryHorizontalEnd=qM,a.GalleryThumbnails=OM,a.GalleryVertical=GM,a.GalleryVerticalEnd=ZM,a.Gamepad=EM,a.Gamepad2=WM,a.GamepadDirectional=IM,a.GanttChart=E,a.GanttChartSquare=m,a.Gauge=XM,a.GaugeCircle=e1,a.Gavel=jM,a.Gem=NM,a.GeorgianLari=KM,a.Ghost=QM,a.Gift=JM,a.GitBranch=_M,a.GitBranchMinus=YM,a.GitBranchPlus=xM,a.GitCommit=o2,a.GitCommitHorizontal=o2,a.GitCommitVertical=a9,a.GitCompare=h9,a.GitCompareArrows=t9,a.GitFork=d9,a.GitGraph=c9,a.GitMerge=M9,a.GitPullRequest=r9,a.GitPullRequestArrow=p9,a.GitPullRequestClosed=i9,a.GitPullRequestCreate=l9,a.GitPullRequestCreateArrow=n9,a.GitPullRequestDraft=e9,a.Github=o9,a.Gitlab=v9,a.GlassWater=$9,a.Glasses=m9,a.Globe=s9,a.Globe2=D1,a.GlobeLock=y9,a.Goal=g9,a.Gpu=C9,a.Grab=s2,a.GraduationCap=u9,a.Grape=H9,a.Grid=o,a.Grid2X2=y2,a.Grid2X2Check=v2,a.Grid2X2Plus=$2,a.Grid2X2X=m2,a.Grid2x2=y2,a.Grid2x2Check=v2,a.Grid2x2Plus=$2,a.Grid2x2X=m2,a.Grid3X3=o,a.Grid3x2=A9,a.Grid3x3=o,a.Grip=S9,a.GripHorizontal=w9,a.GripVertical=V9,a.Group=L9,a.Guitar=k9,a.Ham=f9,a.Hamburger=P9,a.Hammer=B9,a.Hand=T9,a.HandCoins=z9,a.HandFist=F9,a.HandGrab=s2,a.HandHeart=D9,a.HandHelping=g2,a.HandMetal=b9,a.HandPlatter=R9,a.Handbag=q9,a.Handshake=U9,a.HardDrive=I9,a.HardDriveDownload=O9,a.HardDriveUpload=Z9,a.HardHat=G9,a.Hash=W9,a.HatGlasses=E9,a.Haze=X9,a.Hd=j9,a.HdmiPort=N9,a.Heading=ap,a.Heading1=K9,a.Heading2=J9,a.Heading3=Q9,a.Heading4=Y9,a.Heading5=_9,a.Heading6=x9,a.HeadphoneOff=tp,a.Headphones=hp,a.Headset=dp,a.Heart=rp,a.HeartCrack=cp,a.HeartHandshake=Mp,a.HeartMinus=pp,a.HeartOff=ip,a.HeartPlus=np,a.HeartPulse=lp,a.Heater=ep,a.Helicopter=op,a.HelpCircle=l,a.HelpingHand=g2,a.Hexagon=vp,a.Highlighter=$p,a.History=mp,a.Home=C2,a.Hop=yp,a.HopOff=sp,a.Hospital=gp,a.Hotel=Cp,a.Hourglass=up,a.House=C2,a.HouseHeart=Hp,a.HousePlug=Ap,a.HousePlus=wp,a.HouseWifi=Vp,a.IceCream=H2,a.IceCream2=u2,a.IceCreamBowl=u2,a.IceCreamCone=H2,a.IdCard=Lp,a.IdCardLanyard=Sp,a.Image=bp,a.ImageDown=fp,a.ImageMinus=kp,a.ImageOff=Pp,a.ImagePlay=zp,a.ImagePlus=Bp,a.ImageUp=Fp,a.ImageUpscale=Dp,a.Images=Rp,a.Import=Tp,a.Inbox=qp,a.Indent=$,a.IndentDecrease=v,a.IndentIncrease=$,a.IndianRupee=Up,a.Infinity=Op,a.Info=Zp,a.Inspect=q0,a.InspectionPanel=Gp,a.Instagram=Wp,a.Italic=Ip,a.IterationCcw=Ep,a.IterationCw=Xp,a.JapaneseYen=jp,a.Joystick=Np,a.Kanban=Kp,a.KanbanSquare=F0,a.KanbanSquareDashed=S0,a.Kayak=Qp,a.Key=_p,a.KeyRound=Jp,a.KeySquare=Yp,a.Keyboard=ti,a.KeyboardMusic=xp,a.KeyboardOff=ai,a.Lamp=ii,a.LampCeiling=hi,a.LampDesk=di,a.LampFloor=ci,a.LampWallDown=Mi,a.LampWallUp=pi,a.LandPlot=ni,a.Landmark=li,a.Languages=ei,a.Laptop=oi,a.Laptop2=A2,a.LaptopMinimal=A2,a.LaptopMinimalCheck=ri,a.Lasso=$i,a.LassoSelect=vi,a.Laugh=mi,a.Layers=w2,a.Layers2=yi,a.Layers3=w2,a.LayersPlus=si,a.Layout=I2,a.LayoutDashboard=gi,a.LayoutGrid=Ci,a.LayoutList=ui,a.LayoutPanelLeft=Hi,a.LayoutPanelTop=Ai,a.LayoutTemplate=wi,a.Leaf=Vi,a.LeafyGreen=Si,a.Lectern=Li,a.LetterText=Ma,a.Library=ki,a.LibraryBig=fi,a.LibrarySquare=D0,a.LifeBuoy=Pi,a.Ligature=Bi,a.Lightbulb=Fi,a.LightbulbOff=zi,a.LineChart=G,a.LineSquiggle=Di,a.Link=Ti,a.Link2=bi,a.Link2Off=Ri,a.Linkedin=qi,a.List=hn,a.ListCheck=Ui,a.ListChecks=Oi,a.ListChevronsDownUp=Zi,a.ListChevronsUpDown=Gi,a.ListCollapse=Wi,a.ListEnd=Ii,a.ListFilter=Xi,a.ListFilterPlus=Ei,a.ListIndentDecrease=v,a.ListIndentIncrease=$,a.ListMinus=ji,a.ListMusic=Ni,a.ListOrdered=Ki,a.ListPlus=Qi,a.ListRestart=Ji,a.ListStart=Yi,a.ListTodo=_i,a.ListTree=xi,a.ListVideo=an,a.ListX=tn,a.Loader=cn,a.Loader2=V2,a.LoaderCircle=V2,a.LoaderPinwheel=dn,a.Locate=nn,a.LocateFixed=Mn,a.LocateOff=pn,a.LocationEdit=k2,a.Lock=en,a.LockKeyhole=ln,a.LockKeyholeOpen=S2,a.LockOpen=L2,a.LogIn=rn,a.LogOut=on,a.Logs=vn,a.Lollipop=$n,a.Luggage=mn,a.MSquare=b0,a.Magnet=yn,a.Mail=Vn,a.MailCheck=sn,a.MailMinus=gn,a.MailOpen=Cn,a.MailPlus=un,a.MailQuestion=f2,a.MailQuestionMark=f2,a.MailSearch=Hn,a.MailWarning=An,a.MailX=wn,a.Mailbox=Sn,a.Mails=Ln,a.Map=Gn,a.MapMinus=fn,a.MapPin=Un,a.MapPinCheck=Pn,a.MapPinCheckInside=kn,a.MapPinHouse=Bn,a.MapPinMinus=Fn,a.MapPinMinusInside=zn,a.MapPinOff=Dn,a.MapPinPen=k2,a.MapPinPlus=Rn,a.MapPinPlusInside=bn,a.MapPinX=qn,a.MapPinXInside=Tn,a.MapPinned=On,a.MapPlus=Zn,a.Mars=In,a.MarsStroke=Wn,a.Martini=En,a.Maximize=jn,a.Maximize2=Xn,a.Medal=Qn,a.Megaphone=Kn,a.MegaphoneOff=Nn,a.Meh=Jn,a.MemoryStick=Yn,a.Menu=xn,a.MenuSquare=R0,a.Merge=_n,a.MessageCircle=ll,a.MessageCircleCode=al,a.MessageCircleDashed=tl,a.MessageCircleHeart=hl,a.MessageCircleMore=dl,a.MessageCircleOff=cl,a.MessageCirclePlus=Ml,a.MessageCircleQuestion=P2,a.MessageCircleQuestionMark=P2,a.MessageCircleReply=pl,a.MessageCircleWarning=il,a.MessageCircleX=nl,a.MessageSquare=Sl,a.MessageSquareCode=el,a.MessageSquareDashed=rl,a.MessageSquareDiff=ol,a.MessageSquareDot=vl,a.MessageSquareHeart=$l,a.MessageSquareLock=ml,a.MessageSquareMore=yl,a.MessageSquareOff=sl,a.MessageSquarePlus=gl,a.MessageSquareQuote=Cl,a.MessageSquareReply=ul,a.MessageSquareShare=Hl,a.MessageSquareText=Al,a.MessageSquareWarning=wl,a.MessageSquareX=Vl,a.MessagesSquare=Ll,a.Mic=kl,a.Mic2=B2,a.MicOff=fl,a.MicVocal=B2,a.Microchip=Pl,a.Microscope=Bl,a.Microwave=zl,a.Milestone=Fl,a.Milk=bl,a.MilkOff=Dl,a.Minimize=Tl,a.Minimize2=Rl,a.Minus=ql,a.MinusCircle=r1,a.MinusSquare=T0,a.Monitor=Yl,a.MonitorCheck=Ul,a.MonitorCloud=Ol,a.MonitorCog=Zl,a.MonitorDot=Gl,a.MonitorDown=Wl,a.MonitorOff=Il,a.MonitorPause=El,a.MonitorPlay=Xl,a.MonitorSmartphone=jl,a.MonitorSpeaker=Nl,a.MonitorStop=Kl,a.MonitorUp=Ql,a.MonitorX=Jl,a.Moon=ae,a.MoonStar=_l,a.MoreHorizontal=R1,a.MoreVertical=b1,a.Motorbike=xl,a.Mountain=he,a.MountainSnow=te,a.Mouse=le,a.MouseOff=de,a.MousePointer=ne,a.MousePointer2=Me,a.MousePointer2Off=ce,a.MousePointerBan=pe,a.MousePointerClick=ie,a.MousePointerSquareDashed=L0,a.Move=Ae,a.Move3D=z2,a.Move3d=z2,a.MoveDiagonal=re,a.MoveDiagonal2=ee,a.MoveDown=$e,a.MoveDownLeft=oe,a.MoveDownRight=ve,a.MoveHorizontal=ye,a.MoveLeft=me,a.MoveRight=se,a.MoveUp=ue,a.MoveUpLeft=ge,a.MoveUpRight=Ce,a.MoveVertical=He,a.Music=Le,a.Music2=we,a.Music3=Ve,a.Music4=Se,a.Navigation=Be,a.Navigation2=ke,a.Navigation2Off=fe,a.NavigationOff=Pe,a.Network=Fe,a.Newspaper=ze,a.Nfc=De,a.NonBinary=be,a.Notebook=Ue,a.NotebookPen=Re,a.NotebookTabs=Te,a.NotebookText=qe,a.NotepadText=Ze,a.NotepadTextDashed=Oe,a.Nut=We,a.NutOff=Ge,a.Octagon=Ee,a.OctagonAlert=F2,a.OctagonMinus=Ie,a.OctagonPause=D2,a.OctagonX=b2,a.Omega=Xe,a.Option=je,a.Orbit=Ne,a.Origami=Ke,a.Outdent=v,a.Package=hr,a.Package2=Qe,a.PackageCheck=Je,a.PackageMinus=Ye,a.PackageOpen=_e,a.PackagePlus=xe,a.PackageSearch=ar,a.PackageX=tr,a.PaintBucket=dr,a.PaintRoller=cr,a.Paintbrush=Mr,a.Paintbrush2=R2,a.PaintbrushVertical=R2,a.Palette=pr,a.Palmtree=la,a.Panda=ir,a.PanelBottom=er,a.PanelBottomClose=nr,a.PanelBottomDashed=T2,a.PanelBottomInactive=T2,a.PanelBottomOpen=lr,a.PanelLeft=Z2,a.PanelLeftClose=q2,a.PanelLeftDashed=U2,a.PanelLeftInactive=U2,a.PanelLeftOpen=O2,a.PanelLeftRightDashed=rr,a.PanelRight=$r,a.PanelRightClose=or,a.PanelRightDashed=G2,a.PanelRightInactive=G2,a.PanelRightOpen=vr,a.PanelTop=Cr,a.PanelTopBottomDashed=mr,a.PanelTopClose=yr,a.PanelTopDashed=W2,a.PanelTopInactive=W2,a.PanelTopOpen=sr,a.PanelsLeftBottom=gr,a.PanelsLeftRight=B1,a.PanelsRightBottom=ur,a.PanelsTopBottom=J2,a.PanelsTopLeft=I2,a.Paperclip=Hr,a.Parentheses=wr,a.ParkingCircle=v1,a.ParkingCircleOff=o1,a.ParkingMeter=Ar,a.ParkingSquare=O0,a.ParkingSquareOff=U0,a.PartyPopper=Vr,a.Pause=Sr,a.PauseCircle=$1,a.PauseOctagon=D2,a.PawPrint=Lr,a.PcCase=fr,a.Pen=X2,a.PenBox=i,a.PenLine=E2,a.PenOff=kr,a.PenSquare=i,a.PenTool=Pr,a.Pencil=Dr,a.PencilLine=Br,a.PencilOff=zr,a.PencilRuler=Fr,a.Pentagon=br,a.Percent=Rr,a.PercentCircle=m1,a.PercentDiamond=F1,a.PercentSquare=Z0,a.PersonStanding=Tr,a.PhilippinePeso=qr,a.Phone=Er,a.PhoneCall=Or,a.PhoneForwarded=Ur,a.PhoneIncoming=Zr,a.PhoneMissed=Gr,a.PhoneOff=Wr,a.PhoneOutgoing=Ir,a.Pi=Xr,a.PiSquare=G0,a.Piano=jr,a.Pickaxe=Nr,a.PictureInPicture=Qr,a.PictureInPicture2=Kr,a.PieChart=X,a.PiggyBank=Jr,a.Pilcrow=xr,a.PilcrowLeft=Yr,a.PilcrowRight=_r,a.PilcrowSquare=W0,a.Pill=to,a.PillBottle=ao,a.Pin=co,a.PinOff=ho,a.Pipette=Mo,a.Pizza=po,a.Plane=lo,a.PlaneLanding=io,a.PlaneTakeoff=no,a.Play=eo,a.PlayCircle=y1,a.PlaySquare=I0,a.Plug=oo,a.Plug2=ro,a.PlugZap=j2,a.PlugZap2=j2,a.Plus=vo,a.PlusCircle=s1,a.PlusSquare=E0,a.Pocket=mo,a.PocketKnife=$o,a.Podcast=yo,a.Pointer=go,a.PointerOff=so,a.Popcorn=Co,a.Popsicle=uo,a.PoundSterling=Ho,a.Power=wo,a.PowerCircle=g1,a.PowerOff=Ao,a.PowerSquare=X0,a.Presentation=Vo,a.Printer=So,a.PrinterCheck=Lo,a.Projector=fo,a.Proportions=ko,a.Puzzle=Po,a.Pyramid=Bo,a.QrCode=zo,a.Quote=Fo,a.Rabbit=Do,a.Radar=bo,a.Radiation=Ro,a.Radical=To,a.Radio=Oo,a.RadioReceiver=qo,a.RadioTower=Uo,a.Radius=Zo,a.RailSymbol=Go,a.Rainbow=Wo,a.Rat=Io,a.Ratio=Eo,a.Receipt=av,a.ReceiptCent=Xo,a.ReceiptEuro=jo,a.ReceiptIndianRupee=No,a.ReceiptJapaneseYen=Ko,a.ReceiptPoundSterling=Qo,a.ReceiptRussianRuble=Jo,a.ReceiptSwissFranc=Yo,a.ReceiptText=_o,a.ReceiptTurkishLira=xo,a.RectangleCircle=tv,a.RectangleEllipsis=N2,a.RectangleGoggles=hv,a.RectangleHorizontal=dv,a.RectangleVertical=cv,a.Recycle=Mv,a.Redo=nv,a.Redo2=pv,a.RedoDot=iv,a.RefreshCcw=ev,a.RefreshCcwDot=lv,a.RefreshCw=ov,a.RefreshCwOff=rv,a.Refrigerator=vv,a.Regex=$v,a.RemoveFormatting=mv,a.Repeat=gv,a.Repeat1=yv,a.Repeat2=sv,a.Replace=uv,a.ReplaceAll=Cv,a.Reply=Av,a.ReplyAll=Hv,a.Rewind=wv,a.Ribbon=Vv,a.Rocket=Sv,a.RockingChair=Lv,a.RollerCoaster=fv,a.Rose=kv,a.Rotate3D=K2,a.Rotate3d=K2,a.RotateCcw=zv,a.RotateCcwKey=Pv,a.RotateCcwSquare=Bv,a.RotateCw=Dv,a.RotateCwSquare=Fv,a.Route=bv,a.RouteOff=Rv,a.Router=Tv,a.Rows=Q2,a.Rows2=Q2,a.Rows3=J2,a.Rows4=qv,a.Rss=Uv,a.Ruler=Zv,a.RulerDimensionLine=Ov,a.RussianRuble=Gv,a.Sailboat=Wv,a.Salad=Iv,a.Sandwich=Ev,a.Satellite=jv,a.SatelliteDish=Xv,a.SaudiRiyal=Nv,a.Save=Jv,a.SaveAll=Kv,a.SaveOff=Qv,a.Scale=Yv,a.Scale3D=Y2,a.Scale3d=Y2,a.Scaling=_v,a.Scan=i$,a.ScanBarcode=a$,a.ScanEye=xv,a.ScanFace=t$,a.ScanHeart=h$,a.ScanLine=d$,a.ScanQrCode=c$,a.ScanSearch=M$,a.ScanText=p$,a.ScatterChart=j,a.School=n$,a.School2=oa,a.Scissors=e$,a.ScissorsLineDashed=l$,a.ScissorsSquare=j0,a.ScissorsSquareDashedBottom=s0,a.Scooter=r$,a.ScreenShare=v$,a.ScreenShareOff=o$,a.Scroll=m$,a.ScrollText=$$,a.Search=H$,a.SearchAlert=y$,a.SearchCheck=g$,a.SearchCode=s$,a.SearchSlash=C$,a.SearchX=u$,a.Section=A$,a.Send=V$,a.SendHorizonal=_2,a.SendHorizontal=_2,a.SendToBack=w$,a.SeparatorHorizontal=S$,a.SeparatorVertical=L$,a.Server=B$,a.ServerCog=f$,a.ServerCrash=k$,a.ServerOff=P$,a.Settings=F$,a.Settings2=z$,a.Shapes=D$,a.Share=R$,a.Share2=b$,a.Sheet=T$,a.Shell=q$,a.Shield=N$,a.ShieldAlert=U$,a.ShieldBan=O$,a.ShieldCheck=Z$,a.ShieldClose=a0,a.ShieldEllipsis=G$,a.ShieldHalf=W$,a.ShieldMinus=I$,a.ShieldOff=E$,a.ShieldPlus=X$,a.ShieldQuestion=x2,a.ShieldQuestionMark=x2,a.ShieldUser=j$,a.ShieldX=a0,a.Ship=Q$,a.ShipWheel=K$,a.Shirt=J$,a.ShoppingBag=Y$,a.ShoppingBasket=_$,a.ShoppingCart=x$,a.Shovel=am,a.ShowerHead=tm,a.Shredder=hm,a.Shrimp=dm,a.Shrink=cm,a.Shrub=Mm,a.Shuffle=pm,a.Sidebar=Z2,a.SidebarClose=q2,a.SidebarOpen=O2,a.Sigma=nm,a.SigmaSquare=N0,a.Signal=om,a.SignalHigh=im,a.SignalLow=lm,a.SignalMedium=em,a.SignalZero=rm,a.Signature=vm,a.Signpost=mm,a.SignpostBig=$m,a.Siren=ym,a.SkipBack=sm,a.SkipForward=Cm,a.Skull=gm,a.Slack=um,a.Slash=Hm,a.SlashSquare=K0,a.Slice=Am,a.Sliders=t0,a.SlidersHorizontal=wm,a.SlidersVertical=t0,a.Smartphone=Lm,a.SmartphoneCharging=Vm,a.SmartphoneNfc=Sm,a.Smile=km,a.SmilePlus=fm,a.Snail=Pm,a.Snowflake=Bm,a.SoapDispenserDroplet=zm,a.Sofa=Fm,a.SolarPanel=Dm,a.SortAsc=V,a.SortDesc=H,a.Soup=bm,a.Space=Rm,a.Spade=Tm,a.Sparkle=qm,a.Sparkles=h0,a.Speaker=Um,a.Speech=Om,a.SpellCheck=Gm,a.SpellCheck2=Zm,a.Spline=Im,a.SplinePointer=Wm,a.Split=Em,a.SplitSquareHorizontal=Q0,a.SplitSquareVertical=J0,a.Spool=Xm,a.Spotlight=jm,a.SprayCan=Nm,a.Sprout=Km,a.Square=My,a.SquareActivity=d0,a.SquareArrowDown=p0,a.SquareArrowDownLeft=c0,a.SquareArrowDownRight=M0,a.SquareArrowLeft=i0,a.SquareArrowOutDownLeft=n0,a.SquareArrowOutDownRight=l0,a.SquareArrowOutUpLeft=e0,a.SquareArrowOutUpRight=r0,a.SquareArrowRight=o0,a.SquareArrowUp=m0,a.SquareArrowUpLeft=v0,a.SquareArrowUpRight=$0,a.SquareAsterisk=y0,a.SquareBottomDashedScissors=s0,a.SquareChartGantt=m,a.SquareCheck=g0,a.SquareCheckBig=C0,a.SquareChevronDown=u0,a.SquareChevronLeft=H0,a.SquareChevronRight=A0,a.SquareChevronUp=w0,a.SquareCode=V0,a.SquareDashed=f0,a.SquareDashedBottom=Jm,a.SquareDashedBottomCode=Qm,a.SquareDashedKanban=S0,a.SquareDashedMousePointer=L0,a.SquareDashedTopSolid=Ym,a.SquareDivide=k0,a.SquareDot=P0,a.SquareEqual=B0,a.SquareFunction=z0,a.SquareGanttChart=m,a.SquareKanban=F0,a.SquareLibrary=D0,a.SquareM=b0,a.SquareMenu=R0,a.SquareMinus=T0,a.SquareMousePointer=q0,a.SquareParking=O0,a.SquareParkingOff=U0,a.SquarePause=_m,a.SquarePen=i,a.SquarePercent=Z0,a.SquarePi=G0,a.SquarePilcrow=W0,a.SquarePlay=I0,a.SquarePlus=E0,a.SquarePower=X0,a.SquareRadical=xm,a.SquareRoundCorner=ay,a.SquareScissors=j0,a.SquareSigma=N0,a.SquareSlash=K0,a.SquareSplitHorizontal=Q0,a.SquareSplitVertical=J0,a.SquareSquare=ty,a.SquareStack=hy,a.SquareStar=dy,a.SquareStop=cy,a.SquareTerminal=Y0,a.SquareUser=x0,a.SquareUserRound=_0,a.SquareX=aa,a.SquaresExclude=py,a.SquaresIntersect=iy,a.SquaresSubtract=ny,a.SquaresUnite=ey,a.Squircle=ry,a.SquircleDashed=ly,a.Squirrel=oy,a.Stamp=vy,a.Star=yy,a.StarHalf=my,a.StarOff=$y,a.Stars=h0,a.StepBack=sy,a.StepForward=gy,a.Stethoscope=Cy,a.Sticker=uy,a.StickyNote=Ay,a.Stone=Hy,a.StopCircle=u1,a.Store=wy,a.StretchHorizontal=Vy,a.StretchVertical=Sy,a.Strikethrough=Ly,a.Subscript=fy,a.Subtitles=D,a.Sun=Fy,a.SunDim=ky,a.SunMedium=Py,a.SunMoon=By,a.SunSnow=zy,a.Sunrise=Dy,a.Sunset=by,a.Superscript=Ry,a.SwatchBook=Ty,a.SwissFranc=qy,a.SwitchCamera=Uy,a.Sword=Oy,a.Swords=Zy,a.Syringe=Gy,a.Table=Qy,a.Table2=Wy,a.TableCellsMerge=Iy,a.TableCellsSplit=Ey,a.TableColumnsSplit=jy,a.TableConfig=e,a.TableOfContents=Xy,a.TableProperties=Ny,a.TableRowsSplit=Ky,a.Tablet=Yy,a.TabletSmartphone=Jy,a.Tablets=_y,a.Tag=xy,a.Tags=as,a.Tally1=ts,a.Tally2=hs,a.Tally3=ds,a.Tally4=cs,a.Tally5=Ms,a.Tangent=ps,a.Target=is,a.Telescope=ns,a.Tent=es,a.TentTree=ls,a.Terminal=rs,a.TerminalSquare=Y0,a.TestTube=os,a.TestTube2=ta,a.TestTubeDiagonal=ta,a.TestTubes=vs,a.Text=y,a.TextAlignCenter=ha,a.TextAlignEnd=da,a.TextAlignJustify=ca,a.TextAlignStart=y,a.TextCursor=ms,a.TextCursorInput=$s,a.TextInitial=Ma,a.TextQuote=ys,a.TextSearch=ss,a.TextSelect=pa,a.TextSelection=pa,a.TextWrap=ia,a.Theater=gs,a.Thermometer=Hs,a.ThermometerSnowflake=Cs,a.ThermometerSun=us,a.ThumbsDown=As,a.ThumbsUp=ws,a.Ticket=Bs,a.TicketCheck=Vs,a.TicketMinus=Ss,a.TicketPercent=Ls,a.TicketPlus=fs,a.TicketSlash=ks,a.TicketX=Ps,a.Tickets=Fs,a.TicketsPlane=zs,a.Timer=Rs,a.TimerOff=bs,a.TimerReset=Ds,a.ToggleLeft=Ts,a.ToggleRight=qs,a.Toilet=Us,a.ToolCase=Os,a.Toolbox=Zs,a.Tornado=Gs,a.Torus=Ws,a.Touchpad=Es,a.TouchpadOff=Is,a.TowerControl=Xs,a.ToyBrick=js,a.Tractor=Ks,a.TrafficCone=Ns,a.Train=na,a.TrainFront=Js,a.TrainFrontTunnel=Qs,a.TrainTrack=Ys,a.TramFront=na,a.Transgender=_s,a.Trash=ag,a.Trash2=xs,a.TreeDeciduous=tg,a.TreePalm=la,a.TreePine=hg,a.Trees=dg,a.Trello=cg,a.TrendingDown=Mg,a.TrendingUp=ig,a.TrendingUpDown=pg,a.Triangle=eg,a.TriangleAlert=ea,a.TriangleDashed=ng,a.TriangleRight=lg,a.Trophy=rg,a.Truck=vg,a.TruckElectric=og,a.TurkishLira=$g,a.Turntable=mg,a.Turtle=yg,a.Tv=gg,a.Tv2=ra,a.TvMinimal=ra,a.TvMinimalPlay=sg,a.Twitch=Cg,a.Twitter=ug,a.Type=Ag,a.TypeOutline=Hg,a.Umbrella=Vg,a.UmbrellaOff=wg,a.Underline=Sg,a.Undo=kg,a.Undo2=Lg,a.UndoDot=fg,a.UnfoldHorizontal=Pg,a.UnfoldVertical=Bg,a.Ungroup=zg,a.University=oa,a.Unlink=Dg,a.Unlink2=Fg,a.Unlock=L2,a.UnlockKeyhole=S2,a.Unplug=bg,a.Upload=Tg,a.UploadCloud=f1,a.Usb=Rg,a.User=Kg,a.User2=sa,a.UserCheck=qg,a.UserCheck2=va,a.UserCircle=A1,a.UserCircle2=H1,a.UserCog=Ug,a.UserCog2=$a,a.UserLock=Og,a.UserMinus=Zg,a.UserMinus2=ma,a.UserPen=Gg,a.UserPlus=Wg,a.UserPlus2=ya,a.UserRound=sa,a.UserRoundCheck=va,a.UserRoundCog=$a,a.UserRoundMinus=ma,a.UserRoundPen=Ig,a.UserRoundPlus=ya,a.UserRoundSearch=Eg,a.UserRoundX=ga,a.UserSearch=Xg,a.UserSquare=x0,a.UserSquare2=_0,a.UserStar=jg,a.UserX=Ng,a.UserX2=ga,a.Users=Qg,a.Users2=Ca,a.UsersRound=Ca,a.Utensils=Ha,a.UtensilsCrossed=ua,a.UtilityPole=Yg,a.Van=Jg,a.Variable=_g,a.Vault=xg,a.VectorSquare=aC,a.Vegan=tC,a.VenetianMask=hC,a.Venus=dC,a.VenusAndMars=cC,a.Verified=f,a.Vibrate=pC,a.VibrateOff=MC,a.Video=nC,a.VideoOff=iC,a.Videotape=eC,a.View=rC,a.Voicemail=lC,a.Volleyball=oC,a.Volume=sC,a.Volume1=vC,a.Volume2=$C,a.VolumeOff=mC,a.VolumeX=yC,a.Vote=gC,a.Wallet=uC,a.Wallet2=Aa,a.WalletCards=CC,a.WalletMinimal=Aa,a.Wallpaper=HC,a.Wand=AC,a.Wand2=wa,a.WandSparkles=wa,a.Warehouse=wC,a.WashingMachine=VC,a.Watch=SC,a.Waves=PC,a.WavesArrowDown=LC,a.WavesArrowUp=fC,a.WavesLadder=kC,a.Waypoints=BC,a.Webcam=zC,a.Webhook=DC,a.WebhookOff=FC,a.Weight=RC,a.WeightTilde=bC,a.Wheat=qC,a.WheatOff=TC,a.WholeWord=UC,a.Wifi=jC,a.WifiCog=OC,a.WifiHigh=GC,a.WifiLow=ZC,a.WifiOff=WC,a.WifiPen=IC,a.WifiSync=EC,a.WifiZero=XC,a.Wind=KC,a.WindArrowDown=NC,a.Wine=JC,a.WineOff=QC,a.Workflow=_C,a.Worm=YC,a.WrapText=ia,a.Wrench=xC,a.X=au,a.XCircle=w1,a.XOctagon=b2,a.XSquare=aa,a.Youtube=tu,a.Zap=du,a.ZapOff=hu,a.ZoomIn=cu,a.ZoomOut=Mu,a.createElement=La,a.createIcons=iu,a.icons=pu})); 12 + //# sourceMappingURL=lucide.min.js.map
+18
pkg/hold/admin/templates/components/nav.html
···
··· 1 + {{define "nav"}} 2 + <nav class="nav"> 3 + <div class="nav-brand"> 4 + <a href="/admin">Hold Admin</a> 5 + </div> 6 + <ul class="nav-links"> 7 + <li><a href="/admin" class="{{if eq .ActivePage "dashboard"}}active{{end}}">Dashboard</a></li> 8 + <li><a href="/admin/crew" class="{{if eq .ActivePage "crew"}}active{{end}}">Crew</a></li> 9 + <li><a href="/admin/settings" class="{{if eq .ActivePage "settings"}}active{{end}}">Settings</a></li> 10 + </ul> 11 + {{if .User}} 12 + <div class="nav-user"> 13 + <span>{{.User.Handle}}</span> 14 + <a href="/admin/auth/logout" class="btn btn-sm">Logout</a> 15 + </div> 16 + {{end}} 17 + </nav> 18 + {{end}}
+81
pkg/hold/admin/templates/pages/crew.html
···
··· 1 + {{define "pages/crew.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <div class="page-header"> 21 + <h1>Crew Management</h1> 22 + <a href="/admin/crew/add" class="btn btn-primary"> 23 + <i data-lucide="user-plus"></i> 24 + Add Crew Member 25 + </a> 26 + </div> 27 + 28 + {{if .Crew}} 29 + <table class="table"> 30 + <thead> 31 + <tr> 32 + <th>Member</th> 33 + <th>Role</th> 34 + <th>Permissions</th> 35 + <th>Tier</th> 36 + <th>Usage</th> 37 + <th class="actions-header">Actions</th> 38 + </tr> 39 + </thead> 40 + <tbody id="crew-list"> 41 + {{range .Crew}} 42 + <tr id="crew-{{.RKey}}"> 43 + <td class="member-cell"> 44 + {{if .Handle}}<strong>{{.Handle}}</strong><br>{{end}} 45 + <code class="did-code">{{.DID}}</code> 46 + </td> 47 + <td>{{.Role}}</td> 48 + <td class="permissions-cell">{{range .Permissions}}<span class="badge">{{.}}</span>{{end}}</td> 49 + <td><span class="badge badge-tier">{{.Tier}}</span><br><small class="tier-limit">{{.TierLimit}}</small></td> 50 + <td> 51 + <div class="usage-cell"> 52 + <span>{{.UsageHuman}}</span> 53 + <div class="progress-bar"> 54 + <div class="progress-fill {{if gt .UsagePercent 90}}danger{{else if gt .UsagePercent 75}}warning{{end}}" style="width: {{.UsagePercent}}%"></div> 55 + </div> 56 + <small>{{.UsagePercent}}%</small> 57 + </div> 58 + </td> 59 + <td class="actions"> 60 + <a href="/admin/crew/{{.RKey}}" class="btn btn-icon" title="Edit"> 61 + <i data-lucide="pencil"></i> 62 + </a> 63 + <button class="btn btn-icon btn-danger" title="Delete" hx-post="/admin/crew/{{.RKey}}/delete" hx-confirm="Remove this crew member?" hx-target="#crew-{{.RKey}}" hx-swap="outerHTML"> 64 + <i data-lucide="trash-2"></i> 65 + </button> 66 + </td> 67 + </tr> 68 + {{end}} 69 + </tbody> 70 + </table> 71 + {{else}} 72 + <p class="empty">No crew members yet. <a href="/admin/crew/add">Add your first crew member</a>.</p> 73 + {{end}} 74 + </main> 75 + 76 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 77 + 78 + <script>lucide.createIcons();</script> 79 + </body> 80 + </html> 81 + {{end}}
+152
pkg/hold/admin/templates/pages/crew_add.html
···
··· 1 + {{define "pages/crew_add.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <div class="page-header"> 21 + <h1>Add Crew Member</h1> 22 + <a href="/admin/crew" class="btn"> 23 + <i data-lucide="arrow-left"></i> 24 + Back to Crew 25 + </a> 26 + </div> 27 + 28 + <form action="/admin/crew/add" method="POST" class="form"> 29 + <div class="form-group"> 30 + <label for="did">DID</label> 31 + <div class="input-with-lookup"> 32 + <input type="text" id="did" name="did" placeholder="did:plc:..." required> 33 + <button type="button" id="lookup-btn" class="btn btn-sm" title="Lookup handle"> 34 + <i data-lucide="search"></i> 35 + </button> 36 + </div> 37 + <small>The member's ATProto DID</small> 38 + <div id="handle-result" class="handle-lookup-result"></div> 39 + </div> 40 + 41 + <div class="form-group"> 42 + <label for="role">Role</label> 43 + <input type="text" id="role" name="role" placeholder="member" value="member"> 44 + <small>Optional role name (e.g., member, admin)</small> 45 + </div> 46 + 47 + <div class="form-group"> 48 + <label>Permissions</label> 49 + <div class="checkbox-group"> 50 + <label class="checkbox"> 51 + <input type="checkbox" name="perm_read" checked> 52 + <span>blob:read</span> 53 + <small>Can pull/download blobs</small> 54 + </label> 55 + <label class="checkbox"> 56 + <input type="checkbox" name="perm_write" checked> 57 + <span>blob:write</span> 58 + <small>Can push/upload blobs</small> 59 + </label> 60 + <label class="checkbox"> 61 + <input type="checkbox" name="perm_admin"> 62 + <span>crew:admin</span> 63 + <small>Can manage crew members</small> 64 + </label> 65 + </div> 66 + </div> 67 + 68 + {{if .Tiers}} 69 + <div class="form-group"> 70 + <label for="tier">Quota Tier</label> 71 + <select id="tier" name="tier"> 72 + {{range .Tiers}} 73 + <option value="{{.Key}}">{{.Name}} ({{.Limit}})</option> 74 + {{end}} 75 + </select> 76 + <small>Storage quota limit for this member</small> 77 + </div> 78 + {{end}} 79 + 80 + <div class="form-actions"> 81 + <button type="submit" class="btn btn-primary"> 82 + <i data-lucide="user-plus"></i> 83 + Add Crew Member 84 + </button> 85 + <a href="/admin/crew" class="btn">Cancel</a> 86 + </div> 87 + </form> 88 + </main> 89 + 90 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 91 + 92 + <script> 93 + lucide.createIcons(); 94 + 95 + // DID to handle lookup 96 + const didInput = document.getElementById('did'); 97 + const lookupBtn = document.getElementById('lookup-btn'); 98 + const handleResult = document.getElementById('handle-result'); 99 + 100 + async function lookupHandle() { 101 + const did = didInput.value.trim(); 102 + if (!did.startsWith('did:')) { 103 + handleResult.innerHTML = '<span class="error">Invalid DID format</span>'; 104 + return; 105 + } 106 + 107 + handleResult.innerHTML = '<span class="loading">Looking up...</span>'; 108 + 109 + try { 110 + // Use plc.directory for did:plc or did:web resolution 111 + let url; 112 + if (did.startsWith('did:plc:')) { 113 + url = `https://plc.directory/${did}`; 114 + } else if (did.startsWith('did:web:')) { 115 + const host = did.replace('did:web:', '').replace(/%3A/g, ':'); 116 + url = `https://${host}/.well-known/did.json`; 117 + } else { 118 + handleResult.innerHTML = '<span class="error">Unsupported DID method</span>'; 119 + return; 120 + } 121 + 122 + const resp = await fetch(url); 123 + if (!resp.ok) throw new Error('DID not found'); 124 + 125 + const doc = await resp.json(); 126 + // Look for handle in alsoKnownAs 127 + const aka = doc.alsoKnownAs || []; 128 + const handleUri = aka.find(u => u.startsWith('at://')); 129 + if (handleUri) { 130 + const handle = handleUri.replace('at://', ''); 131 + handleResult.innerHTML = `<span class="success"><i data-lucide="check-circle"></i> <strong>${handle}</strong></span>`; 132 + lucide.createIcons(); 133 + } else { 134 + handleResult.innerHTML = '<span class="warning">No handle found</span>'; 135 + } 136 + } catch (err) { 137 + handleResult.innerHTML = `<span class="error">Lookup failed: ${err.message}</span>`; 138 + } 139 + } 140 + 141 + lookupBtn.addEventListener('click', lookupHandle); 142 + 143 + // Auto-lookup on blur if DID looks valid 144 + didInput.addEventListener('blur', function() { 145 + if (this.value.startsWith('did:') && this.value.length > 10) { 146 + lookupHandle(); 147 + } 148 + }); 149 + </script> 150 + </body> 151 + </html> 152 + {{end}}
+89
pkg/hold/admin/templates/pages/crew_edit.html
···
··· 1 + {{define "pages/crew_edit.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <div class="page-header"> 21 + <h1>Edit Crew Member</h1> 22 + <a href="/admin/crew" class="btn"> 23 + <i data-lucide="arrow-left"></i> 24 + Back to Crew 25 + </a> 26 + </div> 27 + 28 + <div class="card"> 29 + <div class="card-header member-header"> 30 + <div class="member-info"> 31 + {{if .MemberHandle}}<strong id="member-handle">{{.MemberHandle}}</strong>{{end}} 32 + <code class="did-code">{{.Member.Member}}</code> 33 + </div> 34 + {{if .IsOwner}}<span class="badge badge-gold">Owner</span>{{end}} 35 + </div> 36 + 37 + <form action="/admin/crew/{{.RKey}}/update" method="POST" class="form"> 38 + <div class="form-group"> 39 + <label for="role">Role</label> 40 + <input type="text" id="role" name="role" value="{{.Member.Role}}" {{if .IsOwner}}disabled{{end}}> 41 + </div> 42 + 43 + <div class="form-group"> 44 + <label>Permissions</label> 45 + <div class="checkbox-group"> 46 + <label class="checkbox"> 47 + <input type="checkbox" name="perm_read" {{if contains .Member.Permissions "blob:read"}}checked{{end}} {{if .IsOwner}}disabled{{end}}> 48 + <span>blob:read</span> 49 + </label> 50 + <label class="checkbox"> 51 + <input type="checkbox" name="perm_write" {{if contains .Member.Permissions "blob:write"}}checked{{end}} {{if .IsOwner}}disabled{{end}}> 52 + <span>blob:write</span> 53 + </label> 54 + <label class="checkbox"> 55 + <input type="checkbox" name="perm_admin" {{if contains .Member.Permissions "crew:admin"}}checked{{end}} {{if .IsOwner}}disabled{{end}}> 56 + <span>crew:admin</span> 57 + </label> 58 + </div> 59 + </div> 60 + 61 + {{if .Tiers}} 62 + <div class="form-group"> 63 + <label for="tier">Quota Tier</label> 64 + <select id="tier" name="tier" {{if .IsOwner}}disabled{{end}}> 65 + {{range .Tiers}} 66 + <option value="{{.Key}}" {{if eq .Key $.Member.Tier}}selected{{end}}>{{.Name}} ({{.Limit}})</option> 67 + {{end}} 68 + </select> 69 + </div> 70 + {{end}} 71 + 72 + {{if .IsOwner}} 73 + <p class="note">Owner permissions cannot be modified.</p> 74 + {{else}} 75 + <div class="form-actions"> 76 + <button type="submit" class="btn btn-primary">Save Changes</button> 77 + <a href="/admin/crew" class="btn">Cancel</a> 78 + </div> 79 + {{end}} 80 + </form> 81 + </div> 82 + </main> 83 + 84 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 85 + 86 + <script>lucide.createIcons();</script> 87 + </body> 88 + </html> 89 + {{end}}
+58
pkg/hold/admin/templates/pages/dashboard.html
···
··· 1 + {{define "pages/dashboard.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 10 + </head> 11 + <body> 12 + {{template "nav" .}} 13 + 14 + <main class="container"> 15 + {{if .Flash}} 16 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 17 + {{end}} 18 + 19 + <h1>Dashboard</h1> 20 + 21 + <div class="stats-grid"> 22 + <div class="stat-card"> 23 + <h3>Crew Members</h3> 24 + <p class="stat-value">{{.Stats.TotalCrewMembers}}</p> 25 + </div> 26 + <div class="stat-card" hx-get="/admin/api/stats" hx-trigger="load" hx-swap="innerHTML"> 27 + <p class="loading">Loading storage stats...</p> 28 + </div> 29 + </div> 30 + 31 + <section class="section"> 32 + <h2>Tier Distribution</h2> 33 + {{if .Stats.TierDistribution}} 34 + <div class="tier-chart"> 35 + {{range $tier, $count := .Stats.TierDistribution}} 36 + <div class="tier-bar"> 37 + <span class="tier-name">{{$tier}}</span> 38 + <span class="tier-count">{{$count}} members</span> 39 + </div> 40 + {{end}} 41 + </div> 42 + {{else}} 43 + <p class="empty">No crew members yet.</p> 44 + {{end}} 45 + </section> 46 + 47 + <section class="section"> 48 + <h2>Top Users by Storage</h2> 49 + <div hx-get="/admin/api/top-users?limit=10" hx-trigger="load" hx-swap="innerHTML"> 50 + <p class="loading">Loading top users...</p> 51 + </div> 52 + </section> 53 + </main> 54 + 55 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 56 + </body> 57 + </html> 58 + {{end}}
+24
pkg/hold/admin/templates/pages/error.html
···
··· 1 + {{define "pages/error.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>Error - Hold Admin</title> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 9 + </head> 10 + <body> 11 + {{template "nav" .}} 12 + 13 + <main class="container"> 14 + <div class="error-page"> 15 + <h1>Error</h1> 16 + <p class="error-message">{{.Error}}</p> 17 + <a href="/admin" class="btn btn-primary">Back to Dashboard</a> 18 + </div> 19 + </main> 20 + 21 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 22 + </body> 23 + </html> 24 + {{end}}
+48
pkg/hold/admin/templates/pages/login.html
···
··· 1 + {{define "pages/login.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>Login - Hold Admin</title> 8 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 9 + </head> 10 + <body class="login-page"> 11 + <div class="login-container"> 12 + <div class="login-card"> 13 + <h1>Hold Admin</h1> 14 + <p class="login-subtitle">Sign in with your ATProto account</p> 15 + 16 + {{if .Error}} 17 + <div class="flash flash-error"> 18 + {{.Error}} 19 + </div> 20 + {{end}} 21 + 22 + <form action="/admin/auth/oauth/authorize" method="GET" class="login-form"> 23 + <input type="hidden" name="return_to" value="{{.ReturnTo}}"> 24 + 25 + <div class="form-group"> 26 + <label for="handle">Handle or DID</label> 27 + <input type="text" id="handle" name="handle" 28 + placeholder="alice.bsky.social" 29 + required autofocus> 30 + </div> 31 + 32 + <button type="submit" class="btn btn-primary btn-block"> 33 + Sign in 34 + </button> 35 + </form> 36 + 37 + <p class="login-note"> 38 + Only the hold owner can access the admin panel. 39 + </p> 40 + </div> 41 + 42 + <footer class="login-footer"> 43 + <p>Hold: <code>{{.HoldDID}}</code></p> 44 + </footer> 45 + </div> 46 + </body> 47 + </html> 48 + {{end}}
+91
pkg/hold/admin/templates/pages/settings.html
···
··· 1 + {{define "pages/settings.html"}} 2 + <!DOCTYPE html> 3 + <html lang="en"> 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>{{.Title}} - Hold Admin</title> 8 + <script src="/admin/static/js/htmx.min.js"></script> 9 + <script src="/admin/static/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/static/css/admin.css"> 11 + </head> 12 + <body> 13 + {{template "nav" .}} 14 + 15 + <main class="container"> 16 + {{if .Flash}} 17 + <div class="flash flash-{{.Flash.Category}}">{{.Flash.Message}}</div> 18 + {{end}} 19 + 20 + <h1>Hold Settings</h1> 21 + 22 + <form action="/admin/settings/update" method="POST" class="form settings-form"> 23 + <section class="section"> 24 + <h2>Access Control</h2> 25 + 26 + <label class="toggle-setting"> 27 + <input type="checkbox" name="public" {{if .Settings.Public}}checked{{end}}> 28 + <span class="toggle-label"> 29 + <strong>Public Hold</strong> 30 + <small>Allow anonymous users to read blobs (no auth required for pulls)</small> 31 + </span> 32 + </label> 33 + 34 + <label class="toggle-setting"> 35 + <input type="checkbox" name="allow_all_crew" {{if .Settings.AllowAllCrew}}checked{{end}}> 36 + <span class="toggle-label"> 37 + <strong>Open Registration</strong> 38 + <small>Allow any authenticated user to join as crew via requestCrew</small> 39 + </span> 40 + </label> 41 + </section> 42 + 43 + <section class="section"> 44 + <h2>Integrations</h2> 45 + 46 + <label class="toggle-setting"> 47 + <input type="checkbox" name="enable_bluesky_posts" {{if .Settings.EnableBlueskyPosts}}checked{{end}}> 48 + <span class="toggle-label"> 49 + <strong>Bluesky Posts</strong> 50 + <small>Post to Bluesky when images are pushed to this hold</small> 51 + </span> 52 + </label> 53 + </section> 54 + 55 + <section class="section"> 56 + <h2>Hold Information</h2> 57 + <dl class="info-list"> 58 + <dt>Hold DID</dt> 59 + <dd><code>{{.Settings.HoldDID}}</code></dd> 60 + <dt>Owner</dt> 61 + <dd> 62 + {{if .Settings.OwnerHandle}}<strong>{{.Settings.OwnerHandle}}</strong><br>{{end}} 63 + <code class="did-code">{{.Settings.OwnerDID}}</code> 64 + </dd> 65 + <dt>Quotas</dt> 66 + <dd> 67 + {{if .Settings.QuotasEnabled}} 68 + <span class="badge badge-tier">Enabled</span> 69 + <small>({{.Settings.TierCount}} tiers, default: {{.Settings.DefaultTier}})</small> 70 + {{else}} 71 + <span class="badge">Disabled</span> 72 + {{end}} 73 + </dd> 74 + </dl> 75 + </section> 76 + 77 + <div class="form-actions"> 78 + <button type="submit" class="btn btn-primary"> 79 + <i data-lucide="save"></i> 80 + Save Settings 81 + </button> 82 + </div> 83 + </form> 84 + </main> 85 + 86 + <footer class="footer"><p>Hold: <code>{{.HoldDID}}</code></p></footer> 87 + 88 + <script>lucide.createIcons();</script> 89 + </body> 90 + </html> 91 + {{end}}
+27
pkg/hold/admin/templates/partials/top_users.html
···
··· 1 + {{define "partials/top_users.html"}} 2 + {{if .Users}} 3 + <table class="table"> 4 + <thead> 5 + <tr> 6 + <th>Member</th> 7 + <th>Usage</th> 8 + <th>Blobs</th> 9 + </tr> 10 + </thead> 11 + <tbody> 12 + {{range .Users}} 13 + <tr> 14 + <td class="member-cell"> 15 + {{if .Handle}}<strong>{{.Handle}}</strong><br>{{end}} 16 + <code class="did-code">{{.DID}}</code> 17 + </td> 18 + <td>{{.UsageHuman}}</td> 19 + <td>{{.BlobCount}}</td> 20 + </tr> 21 + {{end}} 22 + </tbody> 23 + </table> 24 + {{else}} 25 + <p class="empty">No usage data yet.</p> 26 + {{end}} 27 + {{end}}
+5
pkg/hold/admin/templates/partials/usage_stats.html
···
··· 1 + {{define "partials/usage_stats.html"}} 2 + <h3>Storage</h3> 3 + <p class="stat-value">{{.Stats.TotalHuman}}</p> 4 + <p class="stat-detail">{{.Stats.UniqueDigests}} unique blobs</p> 5 + {{end}}
+10
pkg/hold/config.go
··· 26 Server ServerConfig `yaml:"server"` 27 Registration RegistrationConfig `yaml:"registration"` 28 Database DatabaseConfig `yaml:"database"` 29 } 30 31 // RegistrationConfig defines auto-registration settings ··· 137 if err != nil { 138 return nil, fmt.Errorf("failed to build storage config: %w", err) 139 } 140 141 return cfg, nil 142 }
··· 26 Server ServerConfig `yaml:"server"` 27 Registration RegistrationConfig `yaml:"registration"` 28 Database DatabaseConfig `yaml:"database"` 29 + Admin AdminConfig `yaml:"admin"` 30 + } 31 + 32 + // AdminConfig defines admin panel settings 33 + type AdminConfig struct { 34 + // Enabled controls whether the admin panel is accessible (from env: HOLD_ADMIN_ENABLED) 35 + Enabled bool `yaml:"enabled"` 36 } 37 38 // RegistrationConfig defines auto-registration settings ··· 144 if err != nil { 145 return nil, fmt.Errorf("failed to build storage config: %w", err) 146 } 147 + 148 + // Admin panel configuration 149 + cfg.Admin.Enabled = os.Getenv("HOLD_ADMIN_ENABLED") == "true" 150 151 return cfg, nil 152 }
-90
pkg/hold/oci/helpers_test.go
··· 5 ) 6 7 // Tests for helper functions 8 - 9 - func TestBlobPath_SHA256(t *testing.T) { 10 - tests := []struct { 11 - name string 12 - digest string 13 - expected string 14 - }{ 15 - { 16 - name: "standard sha256 digest", 17 - digest: "sha256:abc123def456", 18 - expected: "/docker/registry/v2/blobs/sha256/ab/abc123def456/data", 19 - }, 20 - { 21 - name: "short hash (less than 2 chars)", 22 - digest: "sha256:a", 23 - expected: "/docker/registry/v2/blobs/sha256/a/data", 24 - }, 25 - { 26 - name: "exactly 2 char hash", 27 - digest: "sha256:ab", 28 - expected: "/docker/registry/v2/blobs/sha256/ab/ab/data", 29 - }, 30 - } 31 - 32 - for _, tt := range tests { 33 - t.Run(tt.name, func(t *testing.T) { 34 - result := blobPath(tt.digest) 35 - if result != tt.expected { 36 - t.Errorf("Expected %s, got %s", tt.expected, result) 37 - } 38 - }) 39 - } 40 - } 41 - 42 - func TestBlobPath_TempUpload(t *testing.T) { 43 - tests := []struct { 44 - name string 45 - digest string 46 - expected string 47 - }{ 48 - { 49 - name: "temp upload path", 50 - digest: "uploads/temp-uuid-123", 51 - expected: "/docker/registry/v2/uploads/temp-uuid-123/data", 52 - }, 53 - { 54 - name: "temp upload with different uuid", 55 - digest: "uploads/temp-abc-def-456", 56 - expected: "/docker/registry/v2/uploads/temp-abc-def-456/data", 57 - }, 58 - } 59 - 60 - for _, tt := range tests { 61 - t.Run(tt.name, func(t *testing.T) { 62 - result := blobPath(tt.digest) 63 - if result != tt.expected { 64 - t.Errorf("Expected %s, got %s", tt.expected, result) 65 - } 66 - }) 67 - } 68 - } 69 - 70 - func TestBlobPath_MalformedDigest(t *testing.T) { 71 - tests := []struct { 72 - name string 73 - digest string 74 - expected string 75 - }{ 76 - { 77 - name: "no colon in digest", 78 - digest: "malformed-digest", 79 - expected: "/docker/registry/v2/blobs/malformed-digest/data", 80 - }, 81 - { 82 - name: "empty digest", 83 - digest: "", 84 - expected: "/docker/registry/v2/blobs//data", 85 - }, 86 - } 87 - 88 - for _, tt := range tests { 89 - t.Run(tt.name, func(t *testing.T) { 90 - result := blobPath(tt.digest) 91 - if result != tt.expected { 92 - t.Errorf("Expected %s, got %s", tt.expected, result) 93 - } 94 - }) 95 - } 96 - } 97 - 98 func TestNormalizeETag(t *testing.T) { 99 tests := []struct { 100 name string
··· 5 ) 6 7 // Tests for helper functions 8 func TestNormalizeETag(t *testing.T) { 9 tests := []struct { 10 name string
+15 -43
pkg/hold/oci/multipart.go
··· 12 "time" 13 14 "atcr.io/pkg/atproto" 15 - "github.com/aws/aws-sdk-go/service/s3" 16 "github.com/google/uuid" 17 ) 18 ··· 237 if h.s3Service.Client == nil { 238 return "", S3Native, fmt.Errorf("S3 not configured") 239 } 240 - path := blobPath(digest) 241 s3Key := strings.TrimPrefix(path, "/") 242 if h.s3Service.PathPrefix != "" { 243 s3Key = h.s3Service.PathPrefix + "/" + s3Key 244 } 245 246 - result, err := h.s3Service.Client.CreateMultipartUploadWithContext(ctx, &s3.CreateMultipartUploadInput{ 247 Bucket: &h.s3Service.Bucket, 248 Key: &s3Key, 249 }) ··· 280 return nil, fmt.Errorf("S3 not configured") 281 } 282 283 - path := blobPath(session.Digest) 284 s3Key := strings.TrimPrefix(path, "/") 285 if h.s3Service.PathPrefix != "" { 286 s3Key = h.s3Service.PathPrefix + "/" + s3Key 287 } 288 pnum := int64(partNumber) 289 - req, _ := h.s3Service.Client.UploadPartRequest(&s3.UploadPartInput{ 290 Bucket: &h.s3Service.Bucket, 291 Key: &s3Key, 292 UploadId: &session.S3UploadID, ··· 342 343 // Convert to S3 CompletedPart format 344 // IMPORTANT: S3 requires ETags to be quoted in the CompleteMultipartUpload XML 345 - s3Parts := make([]*s3.CompletedPart, len(parts)) 346 for i, p := range parts { 347 etag := normalizeETag(p.ETag) 348 pnum := int64(p.PartNumber) 349 - s3Parts[i] = &s3.CompletedPart{ 350 PartNumber: &pnum, 351 ETag: &etag, 352 } 353 } 354 - sourcePath := blobPath(session.Digest) 355 s3Key := strings.TrimPrefix(sourcePath, "/") 356 if h.s3Service.PathPrefix != "" { 357 s3Key = h.s3Service.PathPrefix + "/" + s3Key 358 } 359 360 - _, err = h.s3Service.Client.CompleteMultipartUploadWithContext(ctx, &s3.CompleteMultipartUploadInput{ 361 Bucket: &h.s3Service.Bucket, 362 Key: &s3Key, 363 UploadId: &session.S3UploadID, 364 - MultipartUpload: &s3.CompletedMultipartUpload{ 365 Parts: s3Parts, 366 }, 367 }) ··· 374 "parts", len(s3Parts)) 375 376 // Verify the blob exists at temp location before moving 377 - destPath := blobPath(finalDigest) 378 slog.Debug("About to move blob", 379 "source", sourcePath, 380 "dest", destPath) ··· 412 } 413 414 // Write assembled blob to final digest location (not temp) 415 - path := blobPath(finalDigest) 416 writer, err := h.driver.Writer(ctx, path, false) 417 if err != nil { 418 return fmt.Errorf("failed to create writer: %w", err) ··· 448 if h.s3Service.Client == nil { 449 return fmt.Errorf("S3 not configured") 450 } 451 - path := blobPath(session.Digest) 452 s3Key := strings.TrimPrefix(path, "/") 453 if h.s3Service.PathPrefix != "" { 454 s3Key = h.s3Service.PathPrefix + "/" + s3Key 455 } 456 457 - _, err := h.s3Service.Client.AbortMultipartUploadWithContext(ctx, &s3.AbortMultipartUploadInput{ 458 Bucket: &h.s3Service.Bucket, 459 Key: &s3Key, 460 UploadId: &session.S3UploadID, ··· 499 // Add quotes 500 return fmt.Sprintf("\"%s\"", etag) 501 } 502 - 503 - // blobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path 504 - // Distribution stores blobs as: /docker/registry/v2/blobs/{algorithm}/{xx}/{hash}/data 505 - // where xx is the first 2 characters of the hash for directory sharding 506 - // NOTE: Path must start with / for filesystem driver 507 - // This is used for OCI container layers (content-addressed, globally deduplicated) 508 - func blobPath(digest string) string { 509 - // Handle temp paths (start with uploads/temp-) 510 - if strings.HasPrefix(digest, "uploads/temp-") { 511 - return fmt.Sprintf("/docker/registry/v2/%s/data", digest) 512 - } 513 - 514 - // Split digest into algorithm and hash 515 - parts := strings.SplitN(digest, ":", 2) 516 - if len(parts) != 2 { 517 - // Fallback for malformed digest 518 - return fmt.Sprintf("/docker/registry/v2/blobs/%s/data", digest) 519 - } 520 - 521 - algorithm := parts[0] 522 - hash := parts[1] 523 - 524 - // Use first 2 characters for sharding 525 - if len(hash) < 2 { 526 - return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/data", algorithm, hash) 527 - } 528 - 529 - return fmt.Sprintf("/docker/registry/v2/blobs/%s/%s/%s/data", algorithm, hash[:2], hash) 530 - }
··· 12 "time" 13 14 "atcr.io/pkg/atproto" 15 + "atcr.io/pkg/s3" 16 + awss3 "github.com/aws/aws-sdk-go/service/s3" 17 "github.com/google/uuid" 18 ) 19 ··· 238 if h.s3Service.Client == nil { 239 return "", S3Native, fmt.Errorf("S3 not configured") 240 } 241 + path := s3.BlobPath(digest) 242 s3Key := strings.TrimPrefix(path, "/") 243 if h.s3Service.PathPrefix != "" { 244 s3Key = h.s3Service.PathPrefix + "/" + s3Key 245 } 246 247 + result, err := h.s3Service.Client.CreateMultipartUploadWithContext(ctx, &awss3.CreateMultipartUploadInput{ 248 Bucket: &h.s3Service.Bucket, 249 Key: &s3Key, 250 }) ··· 281 return nil, fmt.Errorf("S3 not configured") 282 } 283 284 + path := s3.BlobPath(session.Digest) 285 s3Key := strings.TrimPrefix(path, "/") 286 if h.s3Service.PathPrefix != "" { 287 s3Key = h.s3Service.PathPrefix + "/" + s3Key 288 } 289 pnum := int64(partNumber) 290 + req, _ := h.s3Service.Client.UploadPartRequest(&awss3.UploadPartInput{ 291 Bucket: &h.s3Service.Bucket, 292 Key: &s3Key, 293 UploadId: &session.S3UploadID, ··· 343 344 // Convert to S3 CompletedPart format 345 // IMPORTANT: S3 requires ETags to be quoted in the CompleteMultipartUpload XML 346 + s3Parts := make([]*awss3.CompletedPart, len(parts)) 347 for i, p := range parts { 348 etag := normalizeETag(p.ETag) 349 pnum := int64(p.PartNumber) 350 + s3Parts[i] = &awss3.CompletedPart{ 351 PartNumber: &pnum, 352 ETag: &etag, 353 } 354 } 355 + sourcePath := s3.BlobPath(session.Digest) 356 s3Key := strings.TrimPrefix(sourcePath, "/") 357 if h.s3Service.PathPrefix != "" { 358 s3Key = h.s3Service.PathPrefix + "/" + s3Key 359 } 360 361 + _, err = h.s3Service.Client.CompleteMultipartUploadWithContext(ctx, &awss3.CompleteMultipartUploadInput{ 362 Bucket: &h.s3Service.Bucket, 363 Key: &s3Key, 364 UploadId: &session.S3UploadID, 365 + MultipartUpload: &awss3.CompletedMultipartUpload{ 366 Parts: s3Parts, 367 }, 368 }) ··· 375 "parts", len(s3Parts)) 376 377 // Verify the blob exists at temp location before moving 378 + destPath := s3.BlobPath(finalDigest) 379 slog.Debug("About to move blob", 380 "source", sourcePath, 381 "dest", destPath) ··· 413 } 414 415 // Write assembled blob to final digest location (not temp) 416 + path := s3.BlobPath(finalDigest) 417 writer, err := h.driver.Writer(ctx, path, false) 418 if err != nil { 419 return fmt.Errorf("failed to create writer: %w", err) ··· 449 if h.s3Service.Client == nil { 450 return fmt.Errorf("S3 not configured") 451 } 452 + path := s3.BlobPath(session.Digest) 453 s3Key := strings.TrimPrefix(path, "/") 454 if h.s3Service.PathPrefix != "" { 455 s3Key = h.s3Service.PathPrefix + "/" + s3Key 456 } 457 458 + _, err := h.s3Service.Client.AbortMultipartUploadWithContext(ctx, &awss3.AbortMultipartUploadInput{ 459 Bucket: &h.s3Service.Bucket, 460 Key: &s3Key, 461 UploadId: &session.S3UploadID, ··· 500 // Add quotes 501 return fmt.Sprintf("\"%s\"", etag) 502 }
+43 -12
pkg/hold/oci/xrpc.go
··· 9 10 "atcr.io/pkg/atproto" 11 "atcr.io/pkg/hold/pds" 12 "atcr.io/pkg/s3" 13 storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" 14 "github.com/go-chi/chi/v5" ··· 23 pds *pds.HoldPDS 24 httpClient pds.HTTPClient 25 enableBlueskyPosts bool 26 } 27 28 // NewXRPCHandler creates a new OCI XRPC handler 29 - func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, driver storagedriver.StorageDriver, disablePresignedURLs bool, enableBlueskyPosts bool, httpClient pds.HTTPClient) *XRPCHandler { 30 return &XRPCHandler{ 31 driver: driver, 32 disablePresignedURLs: disablePresignedURLs, ··· 35 pds: holdPDS, 36 httpClient: httpClient, 37 enableBlueskyPosts: enableBlueskyPosts, 38 } 39 } 40 ··· 217 218 // Parse request 219 var req struct { 220 - Repository string `json:"repository"` 221 - Tag string `json:"tag"` 222 - UserDID string `json:"userDid"` 223 - UserHandle string `json:"userHandle"` 224 - Operation string `json:"operation"` // "push" or "pull", defaults to "push" for backward compatibility 225 - Manifest struct { 226 MediaType string `json:"mediaType"` 227 Config struct { 228 Digest string `json:"digest"` ··· 276 277 // Only create layer records and Bluesky posts for pushes 278 if operation == "push" { 279 // Check if manifest posts are enabled 280 // Read from captain record (which is synced with HOLD_BLUESKY_POSTS_ENABLED env var) 281 postsEnabled := false ··· 287 postsEnabled = h.enableBlueskyPosts 288 } 289 290 // Create layer records for each blob 291 for _, layer := range req.Manifest.Layers { 292 record := atproto.NewLayerRecord( 293 layer.Digest, 294 layer.Size, 295 layer.MediaType, 296 - req.Repository, 297 req.UserDID, 298 - req.UserHandle, 299 ) 300 301 _, _, err := h.pds.CreateLayerRecord(ctx, record) ··· 327 } 328 } 329 330 - // Create Bluesky post if enabled 331 - if postsEnabled { 332 // Extract manifest digest from first layer (or use config digest as fallback) 333 manifestDigest := req.Manifest.Config.Digest 334 if len(req.Manifest.Layers) > 0 { ··· 340 h.driver, 341 req.Repository, 342 req.Tag, 343 - req.UserHandle, 344 req.UserDID, 345 manifestDigest, 346 totalSize,
··· 9 10 "atcr.io/pkg/atproto" 11 "atcr.io/pkg/hold/pds" 12 + "atcr.io/pkg/hold/quota" 13 "atcr.io/pkg/s3" 14 storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" 15 "github.com/go-chi/chi/v5" ··· 24 pds *pds.HoldPDS 25 httpClient pds.HTTPClient 26 enableBlueskyPosts bool 27 + quotaMgr *quota.Manager // Quota manager for tier-based limits 28 } 29 30 // NewXRPCHandler creates a new OCI XRPC handler 31 + func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, driver storagedriver.StorageDriver, disablePresignedURLs bool, enableBlueskyPosts bool, httpClient pds.HTTPClient, quotaMgr *quota.Manager) *XRPCHandler { 32 return &XRPCHandler{ 33 driver: driver, 34 disablePresignedURLs: disablePresignedURLs, ··· 37 pds: holdPDS, 38 httpClient: httpClient, 39 enableBlueskyPosts: enableBlueskyPosts, 40 + quotaMgr: quotaMgr, 41 } 42 } 43 ··· 220 221 // Parse request 222 var req struct { 223 + Repository string `json:"repository"` 224 + Tag string `json:"tag"` 225 + UserDID string `json:"userDid"` 226 + ManifestDigest string `json:"manifestDigest"` // For building layer record AT-URIs 227 + Operation string `json:"operation"` // "push" or "pull", defaults to "push" for backward compatibility 228 + Manifest struct { 229 MediaType string `json:"mediaType"` 230 Config struct { 231 Digest string `json:"digest"` ··· 279 280 // Only create layer records and Bluesky posts for pushes 281 if operation == "push" { 282 + // Soft limit check: block if ALREADY over quota 283 + // (blobs already uploaded to S3 by this point, no sense rejecting) 284 + stats, err := h.pds.GetQuotaForUserWithTier(ctx, req.UserDID, h.quotaMgr) 285 + if err == nil && stats.Limit != nil && stats.TotalSize > *stats.Limit { 286 + slog.Warn("Quota exceeded for push", 287 + "userDid", req.UserDID, 288 + "currentUsage", stats.TotalSize, 289 + "limit", *stats.Limit, 290 + "repository", req.Repository, 291 + "tag", req.Tag, 292 + ) 293 + RespondError(w, http.StatusForbidden, fmt.Sprintf( 294 + "quota exceeded: current=%d bytes, limit=%d bytes. Delete images to free space.", 295 + stats.TotalSize, *stats.Limit, 296 + )) 297 + return 298 + } 299 + 300 // Check if manifest posts are enabled 301 // Read from captain record (which is synced with HOLD_BLUESKY_POSTS_ENABLED env var) 302 postsEnabled := false ··· 308 postsEnabled = h.enableBlueskyPosts 309 } 310 311 + // Build manifest AT-URI for layer records 312 + manifestURI := atproto.BuildManifestURI(req.UserDID, req.ManifestDigest) 313 + 314 // Create layer records for each blob 315 for _, layer := range req.Manifest.Layers { 316 record := atproto.NewLayerRecord( 317 layer.Digest, 318 layer.Size, 319 layer.MediaType, 320 req.UserDID, 321 + manifestURI, 322 ) 323 324 _, _, err := h.pds.CreateLayerRecord(ctx, record) ··· 350 } 351 } 352 353 + // Create Bluesky post if enabled and tag is present 354 + // Skip posts for tagless pushes (e.g., buildx platform manifests pushed by digest) 355 + if postsEnabled && req.Tag != "" { 356 + // Resolve handle from DID (cached, 24-hour TTL) 357 + _, userHandle, _, resolveErr := atproto.ResolveIdentity(ctx, req.UserDID) 358 + if resolveErr != nil { 359 + slog.Warn("Failed to resolve handle for user", "did", req.UserDID, "error", resolveErr) 360 + userHandle = req.UserDID // Fallback to DID if resolution fails 361 + } 362 + 363 // Extract manifest digest from first layer (or use config digest as fallback) 364 manifestDigest := req.Manifest.Config.Digest 365 if len(req.Manifest.Layers) > 0 { ··· 371 h.driver, 372 req.Repository, 373 req.Tag, 374 + userHandle, 375 req.UserDID, 376 manifestDigest, 377 totalSize,
+1 -1
pkg/hold/oci/xrpc_test.go
··· 127 128 // Create OCI handler with buffered mode (no S3) 129 mockS3 := s3.S3Service{} 130 - handler := NewXRPCHandler(holdPDS, mockS3, driver, true, false, mockClient) 131 132 return handler, ctx 133 }
··· 127 128 // Create OCI handler with buffered mode (no S3) 129 mockS3 := s3.S3Service{} 130 + handler := NewXRPCHandler(holdPDS, mockS3, driver, true, false, mockClient, nil) 131 132 return handler, ctx 133 }
+51
pkg/hold/pds/crew.go
··· 149 150 return nil 151 }
··· 149 150 return nil 151 } 152 + 153 + // UpdateCrewMemberTier updates a crew member's tier 154 + // Since ATProto records are immutable, this finds the member's record by DID, 155 + // deletes it, and recreates it with the new tier value. 156 + func (p *HoldPDS) UpdateCrewMemberTier(ctx context.Context, memberDID, tier string) error { 157 + // Find the crew member's record by iterating over crew records 158 + members, err := p.ListCrewMembers(ctx) 159 + if err != nil { 160 + return fmt.Errorf("failed to list crew members: %w", err) 161 + } 162 + 163 + // Find the member with matching DID 164 + var targetMember *CrewMemberWithKey 165 + for _, m := range members { 166 + if m.Record.Member == memberDID { 167 + targetMember = m 168 + break 169 + } 170 + } 171 + 172 + if targetMember == nil { 173 + return fmt.Errorf("crew member not found: %s", memberDID) 174 + } 175 + 176 + // If tier is already the same, no update needed 177 + if targetMember.Record.Tier == tier { 178 + return nil 179 + } 180 + 181 + // Delete the old record 182 + if err := p.RemoveCrewMember(ctx, targetMember.Rkey); err != nil { 183 + return fmt.Errorf("failed to remove old crew record: %w", err) 184 + } 185 + 186 + // Create new record with updated tier 187 + newRecord := &atproto.CrewRecord{ 188 + Type: atproto.CrewCollection, 189 + Member: targetMember.Record.Member, 190 + Role: targetMember.Record.Role, 191 + Permissions: targetMember.Record.Permissions, 192 + Tier: tier, 193 + AddedAt: targetMember.Record.AddedAt, // Preserve original add time 194 + } 195 + 196 + _, _, err = p.repomgr.CreateRecord(ctx, p.uid, atproto.CrewCollection, newRecord) 197 + if err != nil { 198 + return fmt.Errorf("failed to create updated crew record: %w", err) 199 + } 200 + 201 + return nil 202 + }
+1 -1
pkg/hold/pds/events_test.go
··· 150 testCID, _ := cid.Decode("bafyreib2rxk3rkhh5ylyxj3x3gathxt3s32qvwj2lf3qg4kmzr6b7teqke") 151 152 // Broadcast 5 events (exceeds maxHistory of 3) 153 - for i := 0; i < 5; i++ { 154 event := &RepoEvent{ 155 NewRoot: testCID, 156 Rev: "test-rev",
··· 150 testCID, _ := cid.Decode("bafyreib2rxk3rkhh5ylyxj3x3gathxt3s32qvwj2lf3qg4kmzr6b7teqke") 151 152 // Broadcast 5 events (exceeds maxHistory of 3) 153 + for range 5 { 154 event := &RepoEvent{ 155 NewRoot: testCID, 156 Rev: "test-rev",
+155
pkg/hold/pds/layer.go
··· 5 "fmt" 6 7 "atcr.io/pkg/atproto" 8 ) 9 10 // CreateLayerRecord creates a new layer record in the hold's PDS ··· 57 // not for runtime queries 58 return nil, "", fmt.Errorf("ListLayerRecords not yet implemented") 59 }
··· 5 "fmt" 6 7 "atcr.io/pkg/atproto" 8 + "atcr.io/pkg/hold/quota" 9 + lexutil "github.com/bluesky-social/indigo/lex/util" 10 + "github.com/bluesky-social/indigo/repo" 11 ) 12 13 // CreateLayerRecord creates a new layer record in the hold's PDS ··· 60 // not for runtime queries 61 return nil, "", fmt.Errorf("ListLayerRecords not yet implemented") 62 } 63 + 64 + // QuotaStats represents storage quota information for a user 65 + type QuotaStats struct { 66 + UserDID string `json:"userDid"` 67 + UniqueBlobs int `json:"uniqueBlobs"` 68 + TotalSize int64 `json:"totalSize"` 69 + Limit *int64 `json:"limit,omitempty"` // nil = unlimited 70 + Tier string `json:"tier,omitempty"` // quota tier (e.g., 'deckhand', 'bosun', 'quartermaster') 71 + } 72 + 73 + // GetQuotaForUser calculates storage quota for a specific user 74 + // It iterates through all layer records, filters by userDid, deduplicates by digest, 75 + // and sums the sizes of unique blobs. 76 + func (p *HoldPDS) GetQuotaForUser(ctx context.Context, userDID string) (*QuotaStats, error) { 77 + if p.recordsIndex == nil { 78 + return nil, fmt.Errorf("records index not available") 79 + } 80 + 81 + // Get session for reading record data 82 + session, err := p.carstore.ReadOnlySession(p.uid) 83 + if err != nil { 84 + return nil, fmt.Errorf("failed to create session: %w", err) 85 + } 86 + 87 + head, err := p.carstore.GetUserRepoHead(ctx, p.uid) 88 + if err != nil { 89 + return nil, fmt.Errorf("failed to get repo head: %w", err) 90 + } 91 + 92 + if !head.Defined() { 93 + // Empty repo - return zero stats 94 + return &QuotaStats{UserDID: userDID}, nil 95 + } 96 + 97 + repoHandle, err := repo.OpenRepo(ctx, session, head) 98 + if err != nil { 99 + return nil, fmt.Errorf("failed to open repo: %w", err) 100 + } 101 + 102 + // Track unique digests and their sizes 103 + digestSizes := make(map[string]int64) 104 + 105 + // Iterate all layer records via the index 106 + cursor := "" 107 + batchSize := 1000 // Process in batches 108 + 109 + for { 110 + records, nextCursor, err := p.recordsIndex.ListRecords(atproto.LayerCollection, batchSize, cursor, true) 111 + if err != nil { 112 + return nil, fmt.Errorf("failed to list layer records: %w", err) 113 + } 114 + 115 + for _, rec := range records { 116 + // Construct record path and get the record data 117 + recordPath := rec.Collection + "/" + rec.Rkey 118 + 119 + _, recBytes, err := repoHandle.GetRecordBytes(ctx, recordPath) 120 + if err != nil { 121 + // Skip records we can't read 122 + continue 123 + } 124 + 125 + // Decode the layer record 126 + recordValue, err := lexutil.CborDecodeValue(*recBytes) 127 + if err != nil { 128 + continue 129 + } 130 + 131 + layerRecord, ok := recordValue.(*atproto.LayerRecord) 132 + if !ok { 133 + continue 134 + } 135 + 136 + // Filter by userDID 137 + if layerRecord.UserDID != userDID { 138 + continue 139 + } 140 + 141 + // Deduplicate by digest - keep the size (could be different pushes of same blob) 142 + // Store the size - we only count each unique digest once 143 + if _, exists := digestSizes[layerRecord.Digest]; !exists { 144 + digestSizes[layerRecord.Digest] = layerRecord.Size 145 + } 146 + } 147 + 148 + if nextCursor == "" { 149 + break 150 + } 151 + cursor = nextCursor 152 + } 153 + 154 + // Calculate totals 155 + var totalSize int64 156 + for _, size := range digestSizes { 157 + totalSize += size 158 + } 159 + 160 + return &QuotaStats{ 161 + UserDID: userDID, 162 + UniqueBlobs: len(digestSizes), 163 + TotalSize: totalSize, 164 + }, nil 165 + } 166 + 167 + // GetQuotaForUserWithTier calculates quota with tier-aware limits 168 + // It returns the base quota stats plus the tier limit and tier name. 169 + // Captain (owner) always has unlimited quota. 170 + func (p *HoldPDS) GetQuotaForUserWithTier(ctx context.Context, userDID string, quotaMgr *quota.Manager) (*QuotaStats, error) { 171 + // Get base stats 172 + stats, err := p.GetQuotaForUser(ctx, userDID) 173 + if err != nil { 174 + return nil, err 175 + } 176 + 177 + // If quota manager is nil or disabled, return unlimited 178 + if quotaMgr == nil || !quotaMgr.IsEnabled() { 179 + return stats, nil 180 + } 181 + 182 + // Check if user is captain (owner) - always unlimited 183 + _, captain, err := p.GetCaptainRecord(ctx) 184 + if err == nil && captain.Owner == userDID { 185 + stats.Tier = "owner" 186 + // Limit remains nil (unlimited) 187 + return stats, nil 188 + } 189 + 190 + // Get crew record to find tier 191 + crewTier := p.getCrewTier(ctx, userDID) 192 + 193 + // Resolve limit from quota manager 194 + stats.Limit = quotaMgr.GetTierLimit(crewTier) 195 + stats.Tier = quotaMgr.GetTierName(crewTier) 196 + 197 + return stats, nil 198 + } 199 + 200 + // getCrewTier returns the tier for a crew member, or empty string if not found 201 + func (p *HoldPDS) getCrewTier(ctx context.Context, userDID string) string { 202 + crewMembers, err := p.ListCrewMembers(ctx) 203 + if err != nil { 204 + return "" 205 + } 206 + 207 + for _, member := range crewMembers { 208 + if member.Record.Member == userDID { 209 + return member.Record.Tier 210 + } 211 + } 212 + 213 + return "" 214 + }
+498 -73
pkg/hold/pds/layer_test.go
··· 1 package pds 2 3 import ( 4 "testing" 5 6 "atcr.io/pkg/atproto" 7 ) 8 9 func TestCreateLayerRecord(t *testing.T) { ··· 22 "sha256:abc123def456", 23 1048576, // 1 MB 24 "application/vnd.oci.image.layer.v1.tar+gzip", 25 - "myapp", 26 "did:plc:alice123", 27 - "alice.bsky.social", 28 ), 29 wantErr: false, 30 }, ··· 34 "sha256:fedcba987654", 35 1073741824, // 1 GB 36 "application/vnd.docker.image.rootfs.diff.tar.gzip", 37 - "debian", 38 "did:plc:bob456", 39 - "bob.example.com", 40 ), 41 wantErr: false, 42 }, 43 { 44 name: "invalid record type", 45 record: &atproto.LayerRecord{ 46 - Type: "wrong.type", 47 - Digest: "sha256:abc123", 48 - Size: 1024, 49 - MediaType: "application/vnd.oci.image.layer.v1.tar", 50 - Repository: "test", 51 - UserDID: "did:plc:test", 52 - UserHandle: "test.example.com", 53 }, 54 wantErr: true, 55 errSubstr: "invalid record type", ··· 57 { 58 name: "missing digest", 59 record: &atproto.LayerRecord{ 60 - Type: atproto.LayerCollection, 61 - Digest: "", 62 - Size: 1024, 63 - MediaType: "application/vnd.oci.image.layer.v1.tar", 64 - Repository: "test", 65 - UserDID: "did:plc:test", 66 - UserHandle: "test.example.com", 67 }, 68 wantErr: true, 69 errSubstr: "digest is required", ··· 71 { 72 name: "zero size", 73 record: &atproto.LayerRecord{ 74 - Type: atproto.LayerCollection, 75 - Digest: "sha256:abc123", 76 - Size: 0, 77 - MediaType: "application/vnd.oci.image.layer.v1.tar", 78 - Repository: "test", 79 - UserDID: "did:plc:test", 80 - UserHandle: "test.example.com", 81 }, 82 wantErr: true, 83 errSubstr: "size must be positive", ··· 85 { 86 name: "negative size", 87 record: &atproto.LayerRecord{ 88 - Type: atproto.LayerCollection, 89 - Digest: "sha256:abc123", 90 - Size: -1, 91 - MediaType: "application/vnd.oci.image.layer.v1.tar", 92 - Repository: "test", 93 - UserDID: "did:plc:test", 94 - UserHandle: "test.example.com", 95 }, 96 wantErr: true, 97 errSubstr: "size must be positive", ··· 134 func TestCreateLayerRecord_MultipleRecords(t *testing.T) { 135 // Test creating multiple layer records for the same manifest 136 pds, ctx := setupTestPDS(t) 137 138 layers := []struct { 139 digest string ··· 151 layer.digest, 152 layer.size, 153 "application/vnd.oci.image.layer.v1.tar+gzip", 154 - "multi-layer-app", 155 "did:plc:test123", 156 - "test.example.com", 157 ) 158 159 rkey, cid, err := pds.CreateLayerRecord(ctx, record) ··· 180 digest := "sha256:abc123def456" 181 size := int64(1048576) 182 mediaType := "application/vnd.oci.image.layer.v1.tar+gzip" 183 - repository := "myapp" 184 userDID := "did:plc:alice123" 185 - userHandle := "alice.bsky.social" 186 187 - record := atproto.NewLayerRecord(digest, size, mediaType, repository, userDID, userHandle) 188 189 if record == nil { 190 t.Fatal("NewLayerRecord() returned nil") ··· 207 t.Errorf("MediaType = %q, want %q", record.MediaType, mediaType) 208 } 209 210 - if record.Repository != repository { 211 - t.Errorf("Repository = %q, want %q", record.Repository, repository) 212 } 213 214 if record.UserDID != userDID { 215 t.Errorf("UserDID = %q, want %q", record.UserDID, userDID) 216 - } 217 - 218 - if record.UserHandle != userHandle { 219 - t.Errorf("UserHandle = %q, want %q", record.UserHandle, userHandle) 220 } 221 222 if record.CreatedAt == "" { ··· 229 func TestLayerRecord_FieldValidation(t *testing.T) { 230 // Test various field values 231 tests := []struct { 232 - name string 233 - digest string 234 - size int64 235 - mediaType string 236 - repository string 237 - userDID string 238 - userHandle string 239 }{ 240 { 241 - name: "typical OCI layer", 242 - digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", 243 - size: 12582912, // 12 MB 244 - mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 245 - repository: "hsm-secrets-operator", 246 - userDID: "did:plc:evan123", 247 - userHandle: "evan.jarrett.net", 248 }, 249 { 250 - name: "Docker layer format", 251 - digest: "sha256:abc123", 252 - size: 1024, 253 - mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", 254 - repository: "nginx", 255 - userDID: "did:plc:user456", 256 - userHandle: "user.example.com", 257 }, 258 { 259 - name: "uncompressed layer", 260 - digest: "sha256:def456", 261 - size: 2048, 262 - mediaType: "application/vnd.oci.image.layer.v1.tar", 263 - repository: "alpine", 264 - userDID: "did:plc:user789", 265 - userHandle: "user.bsky.social", 266 }, 267 } 268 ··· 272 tt.digest, 273 tt.size, 274 tt.mediaType, 275 - tt.repository, 276 tt.userDID, 277 - tt.userHandle, 278 ) 279 280 if record == nil { ··· 289 if record.Digest != tt.digest { 290 t.Errorf("Digest = %q, want %q", record.Digest, tt.digest) 291 } 292 }) 293 } 294 }
··· 1 package pds 2 3 import ( 4 + "os" 5 + "path/filepath" 6 "testing" 7 8 "atcr.io/pkg/atproto" 9 + "atcr.io/pkg/hold/quota" 10 ) 11 12 func TestCreateLayerRecord(t *testing.T) { ··· 25 "sha256:abc123def456", 26 1048576, // 1 MB 27 "application/vnd.oci.image.layer.v1.tar+gzip", 28 "did:plc:alice123", 29 + "at://did:plc:alice123/io.atcr.manifest/abc123def456", 30 ), 31 wantErr: false, 32 }, ··· 36 "sha256:fedcba987654", 37 1073741824, // 1 GB 38 "application/vnd.docker.image.rootfs.diff.tar.gzip", 39 "did:plc:bob456", 40 + "at://did:plc:bob456/io.atcr.manifest/fedcba987654", 41 ), 42 wantErr: false, 43 }, 44 { 45 name: "invalid record type", 46 record: &atproto.LayerRecord{ 47 + Type: "wrong.type", 48 + Digest: "sha256:abc123", 49 + Size: 1024, 50 + MediaType: "application/vnd.oci.image.layer.v1.tar", 51 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 52 + UserDID: "did:plc:test", 53 }, 54 wantErr: true, 55 errSubstr: "invalid record type", ··· 57 { 58 name: "missing digest", 59 record: &atproto.LayerRecord{ 60 + Type: atproto.LayerCollection, 61 + Digest: "", 62 + Size: 1024, 63 + MediaType: "application/vnd.oci.image.layer.v1.tar", 64 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 65 + UserDID: "did:plc:test", 66 }, 67 wantErr: true, 68 errSubstr: "digest is required", ··· 70 { 71 name: "zero size", 72 record: &atproto.LayerRecord{ 73 + Type: atproto.LayerCollection, 74 + Digest: "sha256:abc123", 75 + Size: 0, 76 + MediaType: "application/vnd.oci.image.layer.v1.tar", 77 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 78 + UserDID: "did:plc:test", 79 }, 80 wantErr: true, 81 errSubstr: "size must be positive", ··· 83 { 84 name: "negative size", 85 record: &atproto.LayerRecord{ 86 + Type: atproto.LayerCollection, 87 + Digest: "sha256:abc123", 88 + Size: -1, 89 + MediaType: "application/vnd.oci.image.layer.v1.tar", 90 + Manifest: "at://did:plc:test/io.atcr.manifest/abc123", 91 + UserDID: "did:plc:test", 92 }, 93 wantErr: true, 94 errSubstr: "size must be positive", ··· 131 func TestCreateLayerRecord_MultipleRecords(t *testing.T) { 132 // Test creating multiple layer records for the same manifest 133 pds, ctx := setupTestPDS(t) 134 + 135 + manifestURI := "at://did:plc:test123/io.atcr.manifest/manifestabc123" 136 137 layers := []struct { 138 digest string ··· 150 layer.digest, 151 layer.size, 152 "application/vnd.oci.image.layer.v1.tar+gzip", 153 "did:plc:test123", 154 + manifestURI, 155 ) 156 157 rkey, cid, err := pds.CreateLayerRecord(ctx, record) ··· 178 digest := "sha256:abc123def456" 179 size := int64(1048576) 180 mediaType := "application/vnd.oci.image.layer.v1.tar+gzip" 181 userDID := "did:plc:alice123" 182 + manifestURI := "at://did:plc:alice123/io.atcr.manifest/abc123def456" 183 184 + record := atproto.NewLayerRecord(digest, size, mediaType, userDID, manifestURI) 185 186 if record == nil { 187 t.Fatal("NewLayerRecord() returned nil") ··· 204 t.Errorf("MediaType = %q, want %q", record.MediaType, mediaType) 205 } 206 207 + if record.Manifest != manifestURI { 208 + t.Errorf("Manifest = %q, want %q", record.Manifest, manifestURI) 209 } 210 211 if record.UserDID != userDID { 212 t.Errorf("UserDID = %q, want %q", record.UserDID, userDID) 213 } 214 215 if record.CreatedAt == "" { ··· 222 func TestLayerRecord_FieldValidation(t *testing.T) { 223 // Test various field values 224 tests := []struct { 225 + name string 226 + digest string 227 + size int64 228 + mediaType string 229 + userDID string 230 + manifestURI string 231 }{ 232 { 233 + name: "typical OCI layer", 234 + digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", 235 + size: 12582912, // 12 MB 236 + mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", 237 + userDID: "did:plc:evan123", 238 + manifestURI: "at://did:plc:evan123/io.atcr.manifest/abc123", 239 }, 240 { 241 + name: "Docker layer format", 242 + digest: "sha256:abc123", 243 + size: 1024, 244 + mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", 245 + userDID: "did:plc:user456", 246 + manifestURI: "at://did:plc:user456/io.atcr.manifest/def456", 247 }, 248 { 249 + name: "uncompressed layer", 250 + digest: "sha256:def456", 251 + size: 2048, 252 + mediaType: "application/vnd.oci.image.layer.v1.tar", 253 + userDID: "did:plc:user789", 254 + manifestURI: "at://did:plc:user789/io.atcr.manifest/ghi789", 255 }, 256 } 257 ··· 261 tt.digest, 262 tt.size, 263 tt.mediaType, 264 tt.userDID, 265 + tt.manifestURI, 266 ) 267 268 if record == nil { ··· 277 if record.Digest != tt.digest { 278 t.Errorf("Digest = %q, want %q", record.Digest, tt.digest) 279 } 280 + 281 + if record.Manifest != tt.manifestURI { 282 + t.Errorf("Manifest = %q, want %q", record.Manifest, tt.manifestURI) 283 + } 284 }) 285 } 286 } 287 + 288 + // setupTestPDSWithIndex creates a PDS with file-based database (enables RecordsIndex) 289 + // and bootstraps it with the given owner. Required for quota tests. 290 + func setupTestPDSWithIndex(t *testing.T, ownerDID string) (*HoldPDS, func()) { 291 + t.Helper() 292 + 293 + ctx := sharedCtx 294 + tmpDir := t.TempDir() 295 + 296 + // Use file-based database to enable RecordsIndex 297 + dbPath := filepath.Join(tmpDir, "pds.db") 298 + keyPath := filepath.Join(tmpDir, "signing-key") 299 + 300 + // Copy shared signing key 301 + if err := os.WriteFile(keyPath, sharedTestKey, 0600); err != nil { 302 + t.Fatalf("Failed to copy shared signing key: %v", err) 303 + } 304 + 305 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 306 + if err != nil { 307 + t.Fatalf("Failed to create test PDS: %v", err) 308 + } 309 + 310 + // Bootstrap with owner 311 + if err := pds.Bootstrap(ctx, nil, ownerDID, true, false, ""); err != nil { 312 + t.Fatalf("Failed to bootstrap PDS: %v", err) 313 + } 314 + 315 + // Wire up records indexing 316 + indexingHandler := pds.CreateRecordsIndexEventHandler(nil) 317 + pds.RepomgrRef().SetEventHandler(indexingHandler, true) 318 + 319 + // Backfill index from MST 320 + if err := pds.BackfillRecordsIndex(ctx); err != nil { 321 + t.Fatalf("Failed to backfill records index: %v", err) 322 + } 323 + 324 + cleanup := func() { 325 + pds.Close() 326 + } 327 + 328 + return pds, cleanup 329 + } 330 + 331 + // addCrewMemberWithTier adds a crew member with a specific tier 332 + func addCrewMemberWithTier(t *testing.T, pds *HoldPDS, memberDID, role string, permissions []string, tier string) { 333 + t.Helper() 334 + 335 + crewRecord := &atproto.CrewRecord{ 336 + Type: atproto.CrewCollection, 337 + Member: memberDID, 338 + Role: role, 339 + Permissions: permissions, 340 + Tier: tier, 341 + AddedAt: "2026-01-04T12:00:00Z", 342 + } 343 + 344 + _, _, err := pds.repomgr.CreateRecord(sharedCtx, pds.uid, atproto.CrewCollection, crewRecord) 345 + if err != nil { 346 + t.Fatalf("Failed to add crew member with tier: %v", err) 347 + } 348 + } 349 + 350 + func TestGetQuotaForUserWithTier_OwnerUnlimited(t *testing.T) { 351 + ownerDID := "did:plc:owner123" 352 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 353 + defer cleanup() 354 + 355 + ctx := sharedCtx 356 + 357 + // Create quota manager with config 358 + tmpDir := t.TempDir() 359 + configPath := filepath.Join(tmpDir, "quotas.yaml") 360 + configContent := ` 361 + tiers: 362 + deckhand: 363 + quota: 5GB 364 + bosun: 365 + quota: 50GB 366 + 367 + defaults: 368 + new_crew_tier: deckhand 369 + ` 370 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 371 + t.Fatalf("Failed to write quota config: %v", err) 372 + } 373 + 374 + quotaMgr, err := quota.NewManager(configPath) 375 + if err != nil { 376 + t.Fatalf("Failed to create quota manager: %v", err) 377 + } 378 + 379 + // Create layer records for owner 380 + for i := range 3 { 381 + record := atproto.NewLayerRecord( 382 + "sha256:owner"+string(rune('a'+i)), 383 + 1024*1024*100, // 100MB each 384 + "application/vnd.oci.image.layer.v1.tar+gzip", 385 + ownerDID, 386 + "at://"+ownerDID+"/io.atcr.manifest/test123", 387 + ) 388 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 389 + t.Fatalf("Failed to create layer record: %v", err) 390 + } 391 + } 392 + 393 + // Get quota for owner 394 + stats, err := pds.GetQuotaForUserWithTier(ctx, ownerDID, quotaMgr) 395 + if err != nil { 396 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 397 + } 398 + 399 + // Owner should have unlimited quota (nil limit) 400 + if stats.Limit != nil { 401 + t.Errorf("Expected nil limit for owner, got %d", *stats.Limit) 402 + } 403 + 404 + // Tier should be "owner" 405 + if stats.Tier != "owner" { 406 + t.Errorf("Expected tier 'owner', got %q", stats.Tier) 407 + } 408 + 409 + // Should have 3 unique blobs 410 + if stats.UniqueBlobs != 3 { 411 + t.Errorf("Expected 3 unique blobs, got %d", stats.UniqueBlobs) 412 + } 413 + 414 + // Total size should be 300MB 415 + expectedSize := int64(3 * 100 * 1024 * 1024) 416 + if stats.TotalSize != expectedSize { 417 + t.Errorf("Expected total size %d, got %d", expectedSize, stats.TotalSize) 418 + } 419 + 420 + t.Logf("Owner quota stats: %+v", stats) 421 + } 422 + 423 + func TestGetQuotaForUserWithTier_CrewWithDefaultTier(t *testing.T) { 424 + ownerDID := "did:plc:owner456" 425 + crewDID := "did:plc:crew123" 426 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 427 + defer cleanup() 428 + 429 + ctx := sharedCtx 430 + 431 + // Create quota manager 432 + tmpDir := t.TempDir() 433 + configPath := filepath.Join(tmpDir, "quotas.yaml") 434 + configContent := ` 435 + tiers: 436 + deckhand: 437 + quota: 5GB 438 + bosun: 439 + quota: 50GB 440 + 441 + defaults: 442 + new_crew_tier: deckhand 443 + ` 444 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 445 + t.Fatalf("Failed to write quota config: %v", err) 446 + } 447 + 448 + quotaMgr, err := quota.NewManager(configPath) 449 + if err != nil { 450 + t.Fatalf("Failed to create quota manager: %v", err) 451 + } 452 + 453 + // Add crew member with no tier (should use default) 454 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "") 455 + 456 + // Create layer records for crew member 457 + for i := range 2 { 458 + record := atproto.NewLayerRecord( 459 + "sha256:crew"+string(rune('a'+i)), 460 + 1024*1024*50, // 50MB each 461 + "application/vnd.oci.image.layer.v1.tar+gzip", 462 + crewDID, 463 + "at://"+crewDID+"/io.atcr.manifest/test456", 464 + ) 465 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 466 + t.Fatalf("Failed to create layer record: %v", err) 467 + } 468 + } 469 + 470 + // Get quota for crew member 471 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 472 + if err != nil { 473 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 474 + } 475 + 476 + // Should have 5GB limit (deckhand tier) 477 + expectedLimit := int64(5 * 1024 * 1024 * 1024) 478 + if stats.Limit == nil { 479 + t.Fatal("Expected non-nil limit for crew member") 480 + } 481 + if *stats.Limit != expectedLimit { 482 + t.Errorf("Expected limit %d, got %d", expectedLimit, *stats.Limit) 483 + } 484 + 485 + // Tier should be "deckhand" 486 + if stats.Tier != "deckhand" { 487 + t.Errorf("Expected tier 'deckhand', got %q", stats.Tier) 488 + } 489 + 490 + // Should have 2 unique blobs 491 + if stats.UniqueBlobs != 2 { 492 + t.Errorf("Expected 2 unique blobs, got %d", stats.UniqueBlobs) 493 + } 494 + 495 + t.Logf("Crew (deckhand tier) quota stats: %+v", stats) 496 + } 497 + 498 + func TestGetQuotaForUserWithTier_CrewWithExplicitTier(t *testing.T) { 499 + ownerDID := "did:plc:owner789" 500 + crewDID := "did:plc:bosuncrew456" 501 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 502 + defer cleanup() 503 + 504 + ctx := sharedCtx 505 + 506 + // Create quota manager 507 + tmpDir := t.TempDir() 508 + configPath := filepath.Join(tmpDir, "quotas.yaml") 509 + configContent := ` 510 + tiers: 511 + deckhand: 512 + quota: 5GB 513 + bosun: 514 + quota: 50GB 515 + 516 + defaults: 517 + new_crew_tier: deckhand 518 + ` 519 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 520 + t.Fatalf("Failed to write quota config: %v", err) 521 + } 522 + 523 + quotaMgr, err := quota.NewManager(configPath) 524 + if err != nil { 525 + t.Fatalf("Failed to create quota manager: %v", err) 526 + } 527 + 528 + // Add crew member with explicit "bosun" tier 529 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "bosun") 530 + 531 + // Create layer records for crew member 532 + record := atproto.NewLayerRecord( 533 + "sha256:bosunlayer1", 534 + 1024*1024*1024, // 1GB 535 + "application/vnd.oci.image.layer.v1.tar+gzip", 536 + crewDID, 537 + "at://"+crewDID+"/io.atcr.manifest/test789", 538 + ) 539 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 540 + t.Fatalf("Failed to create layer record: %v", err) 541 + } 542 + 543 + // Get quota for crew member 544 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 545 + if err != nil { 546 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 547 + } 548 + 549 + // Should have 50GB limit (bosun tier) 550 + expectedLimit := int64(50 * 1024 * 1024 * 1024) 551 + if stats.Limit == nil { 552 + t.Fatal("Expected non-nil limit for crew member") 553 + } 554 + if *stats.Limit != expectedLimit { 555 + t.Errorf("Expected limit %d, got %d", expectedLimit, *stats.Limit) 556 + } 557 + 558 + // Tier should be "bosun" 559 + if stats.Tier != "bosun" { 560 + t.Errorf("Expected tier 'bosun', got %q", stats.Tier) 561 + } 562 + 563 + t.Logf("Crew (bosun tier) quota stats: %+v", stats) 564 + } 565 + 566 + func TestGetQuotaForUserWithTier_NoQuotaManager(t *testing.T) { 567 + ownerDID := "did:plc:ownerabc" 568 + crewDID := "did:plc:crewabc" 569 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 570 + defer cleanup() 571 + 572 + ctx := sharedCtx 573 + 574 + // Add crew member 575 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "deckhand") 576 + 577 + // Create layer record 578 + record := atproto.NewLayerRecord( 579 + "sha256:noquotalayer1", 580 + 1024*1024*100, 581 + "application/vnd.oci.image.layer.v1.tar+gzip", 582 + crewDID, 583 + "at://"+crewDID+"/io.atcr.manifest/testabc", 584 + ) 585 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 586 + t.Fatalf("Failed to create layer record: %v", err) 587 + } 588 + 589 + // Get quota with nil quota manager (no enforcement) 590 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, nil) 591 + if err != nil { 592 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 593 + } 594 + 595 + // Should have nil limit (unlimited) 596 + if stats.Limit != nil { 597 + t.Errorf("Expected nil limit when quota manager is nil, got %d", *stats.Limit) 598 + } 599 + 600 + // Tier should be empty 601 + if stats.Tier != "" { 602 + t.Errorf("Expected empty tier, got %q", stats.Tier) 603 + } 604 + 605 + t.Logf("No quota manager stats: %+v", stats) 606 + } 607 + 608 + func TestGetQuotaForUserWithTier_DisabledQuotas(t *testing.T) { 609 + ownerDID := "did:plc:ownerdef" 610 + crewDID := "did:plc:crewdef" 611 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 612 + defer cleanup() 613 + 614 + ctx := sharedCtx 615 + 616 + // Create quota manager with nonexistent config (disabled) 617 + quotaMgr, err := quota.NewManager("/nonexistent/quotas.yaml") 618 + if err != nil { 619 + t.Fatalf("Failed to create quota manager: %v", err) 620 + } 621 + 622 + if quotaMgr.IsEnabled() { 623 + t.Fatal("Expected quotas to be disabled") 624 + } 625 + 626 + // Add crew member 627 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "bosun") 628 + 629 + // Create layer record 630 + record := atproto.NewLayerRecord( 631 + "sha256:disabledlayer1", 632 + 1024*1024*100, 633 + "application/vnd.oci.image.layer.v1.tar+gzip", 634 + crewDID, 635 + "at://"+crewDID+"/io.atcr.manifest/testdef", 636 + ) 637 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 638 + t.Fatalf("Failed to create layer record: %v", err) 639 + } 640 + 641 + // Get quota with disabled quota manager 642 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 643 + if err != nil { 644 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 645 + } 646 + 647 + // Should have nil limit (unlimited when quotas disabled) 648 + if stats.Limit != nil { 649 + t.Errorf("Expected nil limit when quotas disabled, got %d", *stats.Limit) 650 + } 651 + 652 + t.Logf("Disabled quotas stats: %+v", stats) 653 + } 654 + 655 + func TestGetQuotaForUserWithTier_DeduplicatesBlobs(t *testing.T) { 656 + ownerDID := "did:plc:ownerghi" 657 + crewDID := "did:plc:crewghi" 658 + pds, cleanup := setupTestPDSWithIndex(t, ownerDID) 659 + defer cleanup() 660 + 661 + ctx := sharedCtx 662 + 663 + // Create quota manager 664 + tmpDir := t.TempDir() 665 + configPath := filepath.Join(tmpDir, "quotas.yaml") 666 + configContent := ` 667 + tiers: 668 + deckhand: 669 + quota: 5GB 670 + 671 + defaults: 672 + new_crew_tier: deckhand 673 + ` 674 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 675 + t.Fatalf("Failed to write quota config: %v", err) 676 + } 677 + 678 + quotaMgr, err := quota.NewManager(configPath) 679 + if err != nil { 680 + t.Fatalf("Failed to create quota manager: %v", err) 681 + } 682 + 683 + // Add crew member 684 + addCrewMemberWithTier(t, pds, crewDID, "writer", []string{"blob:write"}, "") 685 + 686 + // Create multiple layer records with same digest (should be deduplicated) 687 + digest := "sha256:duplicatelayer" 688 + for i := range 5 { 689 + record := atproto.NewLayerRecord( 690 + digest, 691 + 1024*1024*100, // 100MB 692 + "application/vnd.oci.image.layer.v1.tar+gzip", 693 + crewDID, 694 + "at://"+crewDID+"/io.atcr.manifest/manifest"+string(rune('a'+i)), 695 + ) 696 + if _, _, err := pds.CreateLayerRecord(ctx, record); err != nil { 697 + t.Fatalf("Failed to create layer record %d: %v", i, err) 698 + } 699 + } 700 + 701 + // Get quota 702 + stats, err := pds.GetQuotaForUserWithTier(ctx, crewDID, quotaMgr) 703 + if err != nil { 704 + t.Fatalf("GetQuotaForUserWithTier failed: %v", err) 705 + } 706 + 707 + // Should have 1 unique blob (deduplicated) 708 + if stats.UniqueBlobs != 1 { 709 + t.Errorf("Expected 1 unique blob (deduplicated), got %d", stats.UniqueBlobs) 710 + } 711 + 712 + // Total size should be 100MB (not 500MB) 713 + expectedSize := int64(100 * 1024 * 1024) 714 + if stats.TotalSize != expectedSize { 715 + t.Errorf("Expected total size %d, got %d", expectedSize, stats.TotalSize) 716 + } 717 + 718 + t.Logf("Deduplicated quota stats: %+v", stats) 719 + }
+3 -23
pkg/hold/pds/records_test.go
··· 1 package pds 2 3 import ( 4 - "context" 5 "os" 6 "path/filepath" 7 "testing" 8 9 - "github.com/bluesky-social/indigo/repo" 10 _ "github.com/mattn/go-sqlite3" 11 ) 12 ··· 324 defer ri.Close() 325 326 // Add 5 records 327 - for i := 0; i < 5; i++ { 328 rkey := string(rune('a' + i)) 329 if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey); err != nil { 330 t.Fatalf("IndexRecord() error = %v", err) ··· 475 defer ri.Close() 476 477 // Add records to two collections 478 - for i := 0; i < 3; i++ { 479 ri.IndexRecord("io.atcr.hold.crew", string(rune('a'+i)), "cid1") 480 } 481 - for i := 0; i < 5; i++ { 482 ri.IndexRecord("io.atcr.hold.captain", string(rune('a'+i)), "cid2") 483 } 484 ··· 607 t.Errorf("Expected captain count 1 after deleting crew, got %d", count) 608 } 609 } 610 - 611 - // mockRepo is a minimal mock for testing backfill 612 - // Note: Full backfill testing requires integration tests with real repo 613 - type mockRepo struct { 614 - records map[string]string // key -> cid 615 - } 616 - 617 - func (m *mockRepo) ForEach(ctx context.Context, prefix string, fn func(string, interface{}) error) error { 618 - for k, v := range m.records { 619 - if err := fn(k, v); err != nil { 620 - if err == repo.ErrDoneIterating { 621 - return nil 622 - } 623 - return err 624 - } 625 - } 626 - return nil 627 - }
··· 1 package pds 2 3 import ( 4 "os" 5 "path/filepath" 6 "testing" 7 8 _ "github.com/mattn/go-sqlite3" 9 ) 10 ··· 322 defer ri.Close() 323 324 // Add 5 records 325 + for i := range 5 { 326 rkey := string(rune('a' + i)) 327 if err := ri.IndexRecord("io.atcr.hold.crew", rkey, "cid-"+rkey); err != nil { 328 t.Fatalf("IndexRecord() error = %v", err) ··· 473 defer ri.Close() 474 475 // Add records to two collections 476 + for i := range 3 { 477 ri.IndexRecord("io.atcr.hold.crew", string(rune('a'+i)), "cid1") 478 } 479 + for i := range 5 { 480 ri.IndexRecord("io.atcr.hold.captain", string(rune('a'+i)), "cid2") 481 } 482 ··· 605 t.Errorf("Expected captain count 1 after deleting crew, got %d", count) 606 } 607 }
+1 -2
pkg/hold/pds/server.go
··· 103 // Uses same database as carstore for simplicity 104 var recordsIndex *RecordsIndex 105 if dbPath != ":memory:" { 106 - recordsDbPath := dbPath + "/db.sqlite3" 107 - recordsIndex, err = NewRecordsIndex(recordsDbPath) 108 if err != nil { 109 return nil, fmt.Errorf("failed to create records index: %w", err) 110 }
··· 103 // Uses same database as carstore for simplicity 104 var recordsIndex *RecordsIndex 105 if dbPath != ":memory:" { 106 + recordsIndex, err = NewRecordsIndex(dbPath + "/db.sqlite3") 107 if err != nil { 108 return nil, fmt.Errorf("failed to create records index: %w", err) 109 }
+3 -3
pkg/hold/pds/status_test.go
··· 55 } 56 57 // Create handler for XRPC endpoints 58 - handler := NewXRPCHandler(holdPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}) 59 60 // Helper function to list posts via XRPC 61 listPosts := func() ([]map[string]any, error) { ··· 232 } 233 234 func findSubstring(s, substr string) bool { 235 - for i := 0; i <= len(s)-len(substr); i++ { 236 if s[i:i+len(substr)] == substr { 237 return true 238 } ··· 283 } 284 285 // Create shared handler 286 - sharedHandler = NewXRPCHandler(sharedPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}) 287 288 // Run tests 289 code := m.Run()
··· 55 } 56 57 // Create handler for XRPC endpoints 58 + handler := NewXRPCHandler(holdPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}, nil) 59 60 // Helper function to list posts via XRPC 61 listPosts := func() ([]map[string]any, error) { ··· 232 } 233 234 func findSubstring(s, substr string) bool { 235 + for i := range len(s) - len(substr) + 1 { 236 if s[i:i+len(substr)] == substr { 237 return true 238 } ··· 283 } 284 285 // Create shared handler 286 + sharedHandler = NewXRPCHandler(sharedPDS, s3.S3Service{}, nil, nil, &mockPDSClient{}, nil) 287 288 // Run tests 289 code := m.Run()
+39 -4
pkg/hold/pds/xrpc.go
··· 7 "fmt" 8 9 "atcr.io/pkg/atproto" 10 "atcr.io/pkg/s3" 11 "github.com/bluesky-social/indigo/api/bsky" 12 lexutil "github.com/bluesky-social/indigo/lex/util" ··· 46 s3Service s3.S3Service 47 storageDriver driver.StorageDriver 48 broadcaster *EventBroadcaster 49 - httpClient HTTPClient // For testing - allows injecting mock HTTP client 50 } 51 52 // PartInfo represents a completed part in a multipart upload ··· 64 } 65 66 // NewXRPCHandler creates a new XRPC handler 67 - func NewXRPCHandler(pds *HoldPDS, s3Service s3.S3Service, storageDriver driver.StorageDriver, broadcaster *EventBroadcaster, httpClient HTTPClient) *XRPCHandler { 68 return &XRPCHandler{ 69 pds: pds, 70 s3Service: s3Service, 71 storageDriver: storageDriver, 72 broadcaster: broadcaster, 73 httpClient: httpClient, 74 } 75 } 76 ··· 192 r.Use(h.requireAuth) 193 r.Post(atproto.HoldRequestCrew, h.HandleRequestCrew) 194 }) 195 } 196 197 // HandleHealth returns health check information ··· 211 hostname := h.pds.PublicURL 212 hostname = strings.TrimPrefix(hostname, "http://") 213 hostname = strings.TrimPrefix(hostname, "https://") 214 - hostname = strings.Split(hostname, "/")[0] // Remove path 215 - hostname = strings.Split(hostname, ":")[0] // Remove port 216 217 response := map[string]any{ 218 "did": h.pds.DID(), ··· 1513 // Clients should use multipart upload flow via com.atproto.repo.uploadBlob 1514 return "" 1515 }
··· 7 "fmt" 8 9 "atcr.io/pkg/atproto" 10 + "atcr.io/pkg/hold/quota" 11 "atcr.io/pkg/s3" 12 "github.com/bluesky-social/indigo/api/bsky" 13 lexutil "github.com/bluesky-social/indigo/lex/util" ··· 47 s3Service s3.S3Service 48 storageDriver driver.StorageDriver 49 broadcaster *EventBroadcaster 50 + httpClient HTTPClient // For testing - allows injecting mock HTTP client 51 + quotaMgr *quota.Manager // Quota manager for tier-based limits 52 } 53 54 // PartInfo represents a completed part in a multipart upload ··· 66 } 67 68 // NewXRPCHandler creates a new XRPC handler 69 + func NewXRPCHandler(pds *HoldPDS, s3Service s3.S3Service, storageDriver driver.StorageDriver, broadcaster *EventBroadcaster, httpClient HTTPClient, quotaMgr *quota.Manager) *XRPCHandler { 70 return &XRPCHandler{ 71 pds: pds, 72 s3Service: s3Service, 73 storageDriver: storageDriver, 74 broadcaster: broadcaster, 75 httpClient: httpClient, 76 + quotaMgr: quotaMgr, 77 } 78 } 79 ··· 195 r.Use(h.requireAuth) 196 r.Post(atproto.HoldRequestCrew, h.HandleRequestCrew) 197 }) 198 + 199 + // Public quota endpoint (no auth - quota is per-user, just needs userDid param) 200 + r.Get(atproto.HoldGetQuota, h.HandleGetQuota) 201 } 202 203 // HandleHealth returns health check information ··· 217 hostname := h.pds.PublicURL 218 hostname = strings.TrimPrefix(hostname, "http://") 219 hostname = strings.TrimPrefix(hostname, "https://") 220 + hostname, _, _ = strings.Cut(hostname, "/") // Remove path 221 + hostname, _, _ = strings.Cut(hostname, ":") // Remove port 222 223 response := map[string]any{ 224 "did": h.pds.DID(), ··· 1519 // Clients should use multipart upload flow via com.atproto.repo.uploadBlob 1520 return "" 1521 } 1522 + 1523 + // HandleGetQuota returns storage quota information for a user 1524 + // This calculates the total unique blob storage used by a specific user 1525 + // by iterating layer records and deduplicating by digest. 1526 + // Also returns tier-aware quota limits if quotas.yaml is configured. 1527 + func (h *XRPCHandler) HandleGetQuota(w http.ResponseWriter, r *http.Request) { 1528 + userDID := r.URL.Query().Get("userDid") 1529 + if userDID == "" { 1530 + http.Error(w, "missing required parameter: userDid", http.StatusBadRequest) 1531 + return 1532 + } 1533 + 1534 + // Validate DID format 1535 + if !atproto.IsDID(userDID) { 1536 + http.Error(w, "invalid userDid format", http.StatusBadRequest) 1537 + return 1538 + } 1539 + 1540 + // Get quota stats with tier-aware limits 1541 + stats, err := h.pds.GetQuotaForUserWithTier(r.Context(), userDID, h.quotaMgr) 1542 + if err != nil { 1543 + slog.Error("Failed to get quota", "userDid", userDID, "error", err) 1544 + http.Error(w, fmt.Sprintf("failed to get quota: %v", err), http.StatusInternalServerError) 1545 + return 1546 + } 1547 + 1548 + w.Header().Set("Content-Type", "application/json") 1549 + json.NewEncoder(w).Encode(stats) 1550 + }
+10 -10
pkg/hold/pds/xrpc_test.go
··· 76 mockS3 := s3.S3Service{} 77 78 // Create XRPC handler with mock HTTP client 79 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 80 81 return handler, ctx 82 } ··· 143 mockS3 := s3.S3Service{} 144 145 // Create XRPC handler with mock HTTP client 146 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 147 148 return handler, ctx 149 } ··· 609 610 // Note: Bootstrap already added 1 crew member 611 // Add 4 more for a total of 5 612 - for i := 0; i < 4; i++ { 613 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 614 if err != nil { 615 t.Fatalf("Failed to add crew member: %v", err) ··· 673 holdDID := "did:web:hold.example.com" 674 675 // Add crew members 676 - for i := 0; i < 3; i++ { 677 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 678 if err != nil { 679 t.Fatalf("Failed to add crew member: %v", err) ··· 753 pds, ctx := setupTestPDS(t) // Don't bootstrap - no records created yet 754 mockClient := &mockPDSClient{} 755 mockS3 := s3.S3Service{} 756 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 757 758 // Initialize repo manually (setupTestPDS doesn't call Bootstrap, so no crew members) 759 err := pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "") ··· 888 holdDID := "did:web:hold.example.com" 889 890 // Add 4 more crew members for total of 5 891 - for i := 0; i < 4; i++ { 892 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 893 if err != nil { 894 t.Fatalf("Failed to add crew member: %v", err) ··· 968 holdDID := "did:web:hold.example.com" 969 970 // Add crew members 971 - for i := 0; i < 3; i++ { 972 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 973 if err != nil { 974 t.Fatalf("Failed to add crew member: %v", err) ··· 1231 pds, ctx := setupTestPDS(t) // Don't bootstrap 1232 mockClient := &mockPDSClient{} 1233 mockS3 := s3.S3Service{} 1234 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 1235 1236 // setupTestPDS creates the PDS/database but doesn't initialize the repo 1237 // Check if implementation returns repos before initialization ··· 1317 pds, ctx := setupTestPDS(t) // Don't bootstrap 1318 mockClient := &mockPDSClient{} 1319 mockS3 := s3.S3Service{} 1320 - handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 1321 holdDID := "did:web:hold.example.com" 1322 1323 // Initialize repo but don't add any records ··· 2014 mockClient := &mockPDSClient{} 2015 2016 // Create XRPC handler with mock s3 service and real filesystem driver 2017 - handler := NewXRPCHandler(pds, mockS3Svc.toS3Service(), driver, nil, mockClient) 2018 2019 return handler, mockS3Svc, ctx 2020 }
··· 76 mockS3 := s3.S3Service{} 77 78 // Create XRPC handler with mock HTTP client 79 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 80 81 return handler, ctx 82 } ··· 143 mockS3 := s3.S3Service{} 144 145 // Create XRPC handler with mock HTTP client 146 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 147 148 return handler, ctx 149 } ··· 609 610 // Note: Bootstrap already added 1 crew member 611 // Add 4 more for a total of 5 612 + for i := range 4 { 613 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 614 if err != nil { 615 t.Fatalf("Failed to add crew member: %v", err) ··· 673 holdDID := "did:web:hold.example.com" 674 675 // Add crew members 676 + for i := range 3 { 677 _, err := handler.pds.AddCrewMember(ctx, "did:plc:member"+string(rune(i+'0')), "reader", []string{"blob:read"}) 678 if err != nil { 679 t.Fatalf("Failed to add crew member: %v", err) ··· 753 pds, ctx := setupTestPDS(t) // Don't bootstrap - no records created yet 754 mockClient := &mockPDSClient{} 755 mockS3 := s3.S3Service{} 756 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 757 758 // Initialize repo manually (setupTestPDS doesn't call Bootstrap, so no crew members) 759 err := pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "") ··· 888 holdDID := "did:web:hold.example.com" 889 890 // Add 4 more crew members for total of 5 891 + for i := range 4 { 892 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 893 if err != nil { 894 t.Fatalf("Failed to add crew member: %v", err) ··· 968 holdDID := "did:web:hold.example.com" 969 970 // Add crew members 971 + for i := range 3 { 972 _, err := handler.pds.AddCrewMember(ctx, fmt.Sprintf("did:plc:member%d", i), "reader", []string{"blob:read"}) 973 if err != nil { 974 t.Fatalf("Failed to add crew member: %v", err) ··· 1231 pds, ctx := setupTestPDS(t) // Don't bootstrap 1232 mockClient := &mockPDSClient{} 1233 mockS3 := s3.S3Service{} 1234 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 1235 1236 // setupTestPDS creates the PDS/database but doesn't initialize the repo 1237 // Check if implementation returns repos before initialization ··· 1317 pds, ctx := setupTestPDS(t) // Don't bootstrap 1318 mockClient := &mockPDSClient{} 1319 mockS3 := s3.S3Service{} 1320 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient, nil) 1321 holdDID := "did:web:hold.example.com" 1322 1323 // Initialize repo but don't add any records ··· 2014 mockClient := &mockPDSClient{} 2015 2016 // Create XRPC handler with mock s3 service and real filesystem driver 2017 + handler := NewXRPCHandler(pds, mockS3Svc.toS3Service(), driver, nil, mockClient, nil) 2018 2019 return handler, mockS3Svc, ctx 2020 }
+221
pkg/hold/quota/config.go
···
··· 1 + // Package quota provides storage quota management for hold services. 2 + package quota 3 + 4 + import ( 5 + "fmt" 6 + "os" 7 + "regexp" 8 + "strconv" 9 + "strings" 10 + 11 + "go.yaml.in/yaml/v4" 12 + ) 13 + 14 + // Config represents the quotas.yaml configuration 15 + type Config struct { 16 + Tiers map[string]TierConfig `yaml:"tiers"` 17 + Defaults DefaultsConfig `yaml:"defaults"` 18 + } 19 + 20 + // TierConfig represents a single tier's configuration 21 + type TierConfig struct { 22 + Quota string `yaml:"quota"` // Human-readable size: "5GB", "50GB", etc. 23 + } 24 + 25 + // DefaultsConfig represents default settings 26 + type DefaultsConfig struct { 27 + NewCrewTier string `yaml:"new_crew_tier"` 28 + } 29 + 30 + // Manager manages quota configuration and tier resolution 31 + type Manager struct { 32 + config *Config 33 + tiers map[string]int64 // resolved tier name -> bytes 34 + } 35 + 36 + // NewManager creates a quota manager, loading config from file if present 37 + func NewManager(configPath string) (*Manager, error) { 38 + m := &Manager{ 39 + tiers: make(map[string]int64), 40 + } 41 + 42 + // Try to load config file 43 + data, err := os.ReadFile(configPath) 44 + if err != nil { 45 + if os.IsNotExist(err) { 46 + // No config file = no quotas enforced 47 + return m, nil 48 + } 49 + return nil, fmt.Errorf("failed to read quota config: %w", err) 50 + } 51 + 52 + var cfg Config 53 + if err := yaml.Unmarshal(data, &cfg); err != nil { 54 + return nil, fmt.Errorf("failed to parse quota config: %w", err) 55 + } 56 + 57 + m.config = &cfg 58 + 59 + // Parse and resolve all tiers 60 + for name, tier := range cfg.Tiers { 61 + bytes, err := ParseHumanBytes(tier.Quota) 62 + if err != nil { 63 + return nil, fmt.Errorf("invalid quota for tier %q: %w", name, err) 64 + } 65 + m.tiers[name] = bytes 66 + } 67 + 68 + return m, nil 69 + } 70 + 71 + // IsEnabled returns true if quotas are being enforced 72 + func (m *Manager) IsEnabled() bool { 73 + return m.config != nil 74 + } 75 + 76 + // GetTierLimit resolves the quota limit for a tier key 77 + // Returns nil for unlimited (captain, no config, or tier not found with no default) 78 + // 79 + // Resolution order: 80 + // 1. If quotas disabled โ†’ nil (unlimited) 81 + // 2. If tierKey provided and found โ†’ return that tier's limit 82 + // 3. If tierKey not found or empty โ†’ use defaults.new_crew_tier 83 + // 4. If default tier not found โ†’ nil (unlimited) 84 + func (m *Manager) GetTierLimit(tierKey string) *int64 { 85 + if !m.IsEnabled() { 86 + return nil 87 + } 88 + 89 + // Try the provided tier key first 90 + if tierKey != "" { 91 + if limit, ok := m.tiers[tierKey]; ok { 92 + return &limit 93 + } 94 + } 95 + 96 + // Fall back to default tier 97 + if m.config.Defaults.NewCrewTier != "" { 98 + if limit, ok := m.tiers[m.config.Defaults.NewCrewTier]; ok { 99 + return &limit 100 + } 101 + } 102 + 103 + // No valid tier found - unlimited 104 + return nil 105 + } 106 + 107 + // GetTierName resolves the tier name for a tier key 108 + // Returns the actual tier name being used (after fallback resolution) 109 + func (m *Manager) GetTierName(tierKey string) string { 110 + if !m.IsEnabled() { 111 + return "" 112 + } 113 + 114 + // Try the provided tier key first 115 + if tierKey != "" { 116 + if _, ok := m.tiers[tierKey]; ok { 117 + return tierKey 118 + } 119 + } 120 + 121 + // Fall back to default tier 122 + if m.config.Defaults.NewCrewTier != "" { 123 + if _, ok := m.tiers[m.config.Defaults.NewCrewTier]; ok { 124 + return m.config.Defaults.NewCrewTier 125 + } 126 + } 127 + 128 + return "" 129 + } 130 + 131 + // GetDefaultTier returns the default tier name for new crew members 132 + func (m *Manager) GetDefaultTier() string { 133 + if m.config == nil { 134 + return "" 135 + } 136 + return m.config.Defaults.NewCrewTier 137 + } 138 + 139 + // TierCount returns the number of configured tiers 140 + func (m *Manager) TierCount() int { 141 + return len(m.tiers) 142 + } 143 + 144 + // TierInfo represents tier information for listing 145 + type TierInfo struct { 146 + Key string 147 + Limit *int64 148 + } 149 + 150 + // ListTiers returns all configured tiers with their limits 151 + func (m *Manager) ListTiers() []TierInfo { 152 + if !m.IsEnabled() { 153 + return nil 154 + } 155 + 156 + tiers := make([]TierInfo, 0, len(m.tiers)) 157 + for key, limit := range m.tiers { 158 + limitCopy := limit // Create copy to take address of 159 + tiers = append(tiers, TierInfo{ 160 + Key: key, 161 + Limit: &limitCopy, 162 + }) 163 + } 164 + return tiers 165 + } 166 + 167 + // ParseHumanBytes parses human-readable byte sizes like "5GB", "100MB", "1.5TB" 168 + func ParseHumanBytes(s string) (int64, error) { 169 + s = strings.TrimSpace(strings.ToUpper(s)) 170 + if s == "" { 171 + return 0, fmt.Errorf("empty size string") 172 + } 173 + 174 + // Match number (with optional decimal) followed by optional unit 175 + re := regexp.MustCompile(`^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|PB)?$`) 176 + matches := re.FindStringSubmatch(s) 177 + if matches == nil { 178 + return 0, fmt.Errorf("invalid size format: %s", s) 179 + } 180 + 181 + value, err := strconv.ParseFloat(matches[1], 64) 182 + if err != nil { 183 + return 0, fmt.Errorf("invalid number: %w", err) 184 + } 185 + 186 + unit := matches[2] 187 + if unit == "" { 188 + unit = "B" 189 + } 190 + 191 + multipliers := map[string]float64{ 192 + "B": 1, 193 + "KB": 1024, 194 + "MB": 1024 * 1024, 195 + "GB": 1024 * 1024 * 1024, 196 + "TB": 1024 * 1024 * 1024 * 1024, 197 + "PB": 1024 * 1024 * 1024 * 1024 * 1024, 198 + } 199 + 200 + mult, ok := multipliers[unit] 201 + if !ok { 202 + return 0, fmt.Errorf("unknown unit: %s", unit) 203 + } 204 + 205 + return int64(value * mult), nil 206 + } 207 + 208 + // FormatHumanBytes formats bytes as a human-readable string 209 + func FormatHumanBytes(bytes int64) string { 210 + const unit = 1024 211 + if bytes < unit { 212 + return fmt.Sprintf("%d B", bytes) 213 + } 214 + div, exp := int64(unit), 0 215 + for n := bytes / unit; n >= unit; n /= unit { 216 + div *= unit 217 + exp++ 218 + } 219 + units := []string{"KB", "MB", "GB", "TB", "PB"} 220 + return fmt.Sprintf("%.1f %s", float64(bytes)/float64(div), units[exp]) 221 + }
+275
pkg/hold/quota/config_test.go
···
··· 1 + package quota 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "testing" 7 + ) 8 + 9 + func TestParseHumanBytes(t *testing.T) { 10 + tests := []struct { 11 + input string 12 + expected int64 13 + wantErr bool 14 + }{ 15 + // Basic units 16 + {"1024", 1024, false}, 17 + {"1024B", 1024, false}, 18 + {"1KB", 1024, false}, 19 + {"1MB", 1024 * 1024, false}, 20 + {"1GB", 1024 * 1024 * 1024, false}, 21 + {"1TB", 1024 * 1024 * 1024 * 1024, false}, 22 + {"1PB", 1024 * 1024 * 1024 * 1024 * 1024, false}, 23 + 24 + // Common sizes 25 + {"5GB", 5 * 1024 * 1024 * 1024, false}, 26 + {"50GB", 50 * 1024 * 1024 * 1024, false}, 27 + {"100GB", 100 * 1024 * 1024 * 1024, false}, 28 + {"500MB", 500 * 1024 * 1024, false}, 29 + 30 + // Case insensitive 31 + {"5gb", 5 * 1024 * 1024 * 1024, false}, 32 + {"5Gb", 5 * 1024 * 1024 * 1024, false}, 33 + 34 + // With whitespace 35 + {" 5GB ", 5 * 1024 * 1024 * 1024, false}, 36 + 37 + // Decimals 38 + {"1.5GB", int64(1.5 * 1024 * 1024 * 1024), false}, 39 + {"2.5TB", int64(2.5 * 1024 * 1024 * 1024 * 1024), false}, 40 + 41 + // Errors 42 + {"", 0, true}, 43 + {"invalid", 0, true}, 44 + {"GB", 0, true}, 45 + {"-5GB", 0, true}, 46 + } 47 + 48 + for _, tt := range tests { 49 + t.Run(tt.input, func(t *testing.T) { 50 + result, err := ParseHumanBytes(tt.input) 51 + if tt.wantErr { 52 + if err == nil { 53 + t.Errorf("expected error for input %q", tt.input) 54 + } 55 + return 56 + } 57 + if err != nil { 58 + t.Errorf("unexpected error: %v", err) 59 + return 60 + } 61 + if result != tt.expected { 62 + t.Errorf("got %d, want %d", result, tt.expected) 63 + } 64 + }) 65 + } 66 + } 67 + 68 + func TestFormatHumanBytes(t *testing.T) { 69 + tests := []struct { 70 + bytes int64 71 + expected string 72 + }{ 73 + {0, "0 B"}, 74 + {512, "512 B"}, 75 + {1024, "1.0 KB"}, 76 + {1024 * 1024, "1.0 MB"}, 77 + {1024 * 1024 * 1024, "1.0 GB"}, 78 + {5 * 1024 * 1024 * 1024, "5.0 GB"}, 79 + {1024 * 1024 * 1024 * 1024, "1.0 TB"}, 80 + } 81 + 82 + for _, tt := range tests { 83 + t.Run(tt.expected, func(t *testing.T) { 84 + result := FormatHumanBytes(tt.bytes) 85 + if result != tt.expected { 86 + t.Errorf("got %q, want %q", result, tt.expected) 87 + } 88 + }) 89 + } 90 + } 91 + 92 + func TestNewManager_NoConfigFile(t *testing.T) { 93 + m, err := NewManager("/nonexistent/quotas.yaml") 94 + if err != nil { 95 + t.Fatalf("expected no error for missing file, got: %v", err) 96 + } 97 + if m.IsEnabled() { 98 + t.Error("expected quotas to be disabled when file missing") 99 + } 100 + if m.GetTierLimit("anything") != nil { 101 + t.Error("expected nil limit when quotas disabled") 102 + } 103 + } 104 + 105 + func TestNewManager_ValidConfig(t *testing.T) { 106 + tmpDir := t.TempDir() 107 + configPath := filepath.Join(tmpDir, "quotas.yaml") 108 + 109 + configContent := ` 110 + tiers: 111 + deckhand: 112 + quota: 5GB 113 + bosun: 114 + quota: 50GB 115 + quartermaster: 116 + quota: 100GB 117 + 118 + defaults: 119 + new_crew_tier: deckhand 120 + ` 121 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 122 + t.Fatalf("failed to write config: %v", err) 123 + } 124 + 125 + m, err := NewManager(configPath) 126 + if err != nil { 127 + t.Fatalf("failed to load config: %v", err) 128 + } 129 + 130 + if !m.IsEnabled() { 131 + t.Error("expected quotas to be enabled") 132 + } 133 + 134 + if m.TierCount() != 3 { 135 + t.Errorf("expected 3 tiers, got %d", m.TierCount()) 136 + } 137 + 138 + // Test default tier (empty string) 139 + limit := m.GetTierLimit("") 140 + if limit == nil { 141 + t.Fatal("expected non-nil limit for default tier") 142 + } 143 + if *limit != 5*1024*1024*1024 { 144 + t.Errorf("expected 5GB limit for default, got %d", *limit) 145 + } 146 + 147 + // Test explicit tier 148 + limit = m.GetTierLimit("bosun") 149 + if limit == nil { 150 + t.Fatal("expected non-nil limit for bosun") 151 + } 152 + if *limit != 50*1024*1024*1024 { 153 + t.Errorf("expected 50GB limit for bosun, got %d", *limit) 154 + } 155 + 156 + // Test tier name resolution 157 + if m.GetTierName("") != "deckhand" { 158 + t.Errorf("expected tier name 'deckhand' for empty key, got %q", m.GetTierName("")) 159 + } 160 + if m.GetTierName("bosun") != "bosun" { 161 + t.Errorf("expected tier name 'bosun', got %q", m.GetTierName("bosun")) 162 + } 163 + } 164 + 165 + func TestNewManager_FallbackToDefault(t *testing.T) { 166 + tmpDir := t.TempDir() 167 + configPath := filepath.Join(tmpDir, "quotas.yaml") 168 + 169 + configContent := ` 170 + tiers: 171 + deckhand: 172 + quota: 5GB 173 + quartermaster: 174 + quota: 50GB 175 + 176 + defaults: 177 + new_crew_tier: deckhand 178 + ` 179 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 180 + t.Fatalf("failed to write config: %v", err) 181 + } 182 + 183 + m, err := NewManager(configPath) 184 + if err != nil { 185 + t.Fatalf("failed to load config: %v", err) 186 + } 187 + 188 + // Unknown tier should fall back to default 189 + limit := m.GetTierLimit("unknown_tier") 190 + if limit == nil { 191 + t.Fatal("expected fallback to default tier") 192 + } 193 + if *limit != 5*1024*1024*1024 { 194 + t.Errorf("expected 5GB limit from default fallback, got %d", *limit) 195 + } 196 + 197 + // Tier name should also fall back 198 + if m.GetTierName("unknown_tier") != "deckhand" { 199 + t.Errorf("expected tier name 'deckhand' for unknown tier, got %q", m.GetTierName("unknown_tier")) 200 + } 201 + } 202 + 203 + func TestNewManager_InvalidConfig(t *testing.T) { 204 + tmpDir := t.TempDir() 205 + configPath := filepath.Join(tmpDir, "quotas.yaml") 206 + 207 + // Invalid YAML 208 + if err := os.WriteFile(configPath, []byte("invalid: [yaml"), 0644); err != nil { 209 + t.Fatalf("failed to write config: %v", err) 210 + } 211 + 212 + _, err := NewManager(configPath) 213 + if err == nil { 214 + t.Error("expected error for invalid YAML") 215 + } 216 + } 217 + 218 + func TestNewManager_InvalidQuotaSize(t *testing.T) { 219 + tmpDir := t.TempDir() 220 + configPath := filepath.Join(tmpDir, "quotas.yaml") 221 + 222 + configContent := ` 223 + tiers: 224 + deckhand: 225 + quota: invalid_size 226 + 227 + defaults: 228 + new_crew_tier: deckhand 229 + ` 230 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 231 + t.Fatalf("failed to write config: %v", err) 232 + } 233 + 234 + _, err := NewManager(configPath) 235 + if err == nil { 236 + t.Error("expected error for invalid quota size") 237 + } 238 + } 239 + 240 + func TestNewManager_NoDefaultTier(t *testing.T) { 241 + tmpDir := t.TempDir() 242 + configPath := filepath.Join(tmpDir, "quotas.yaml") 243 + 244 + configContent := ` 245 + tiers: 246 + quartermaster: 247 + quota: 50GB 248 + 249 + defaults: 250 + new_crew_tier: nonexistent 251 + ` 252 + if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { 253 + t.Fatalf("failed to write config: %v", err) 254 + } 255 + 256 + m, err := NewManager(configPath) 257 + if err != nil { 258 + t.Fatalf("failed to load config: %v", err) 259 + } 260 + 261 + // Empty tier key with nonexistent default should return nil (unlimited) 262 + limit := m.GetTierLimit("") 263 + if limit != nil { 264 + t.Error("expected nil limit when default tier doesn't exist") 265 + } 266 + 267 + // Explicit tier should still work 268 + limit = m.GetTierLimit("quartermaster") 269 + if limit == nil { 270 + t.Fatal("expected non-nil limit for quartermaster tier") 271 + } 272 + if *limit != 50*1024*1024*1024 { 273 + t.Errorf("expected 50GB limit for quartermaster, got %d", *limit) 274 + } 275 + }
+2 -2
pkg/logging/logger_test.go
··· 366 defer slog.SetDefault(originalLogger) 367 368 b.ResetTimer() 369 - for i := 0; i < b.N; i++ { 370 InitLogger("info") 371 } 372 } ··· 376 defer slog.SetDefault(originalLogger) 377 378 b.ResetTimer() 379 - for i := 0; i < b.N; i++ { 380 cleanup := SetupTestLogger() 381 cleanup() 382 }
··· 366 defer slog.SetDefault(originalLogger) 367 368 b.ResetTimer() 369 + for range b.N { 370 InitLogger("info") 371 } 372 } ··· 376 defer slog.SetDefault(originalLogger) 377 378 b.ResetTimer() 379 + for range b.N { 380 cleanup := SetupTestLogger() 381 cleanup() 382 }
+35
quotas.yaml.example
···
··· 1 + # ATCR Hold Service Quota Configuration 2 + # Copy this file to quotas.yaml to enable quota enforcement. 3 + # If quotas.yaml doesn't exist, quotas are disabled (unlimited for all users). 4 + 5 + # Tiers define quota levels using nautical crew ranks. 6 + # Each tier has a quota limit specified in human-readable format. 7 + # Supported units: B, KB, MB, GB, TB, PB (case-insensitive) 8 + tiers: 9 + # Entry-level crew - suitable for new or casual users 10 + deckhand: 11 + quota: 5GB 12 + 13 + # Mid-level crew - for regular contributors 14 + bosun: 15 + quota: 50GB 16 + 17 + # Senior crew - for power users or trusted contributors 18 + quartermaster: 19 + quota: 100GB 20 + 21 + # You can add custom tiers with any name: 22 + # unlimited_crew: 23 + # quota: 1TB 24 + 25 + defaults: 26 + # Default tier assigned to new crew members who don't have an explicit tier. 27 + # This tier must exist in the tiers section above. 28 + new_crew_tier: deckhand 29 + 30 + # Notes: 31 + # - The hold captain (owner) always has unlimited quota regardless of tiers. 32 + # - Crew members can be assigned a specific tier in their crew record. 33 + # - If a crew member's tier doesn't exist in config, they fall back to the default. 34 + # - Quota is calculated per-user by summing unique blob sizes (deduplicated). 35 + # - Quota is checked when pushing manifests (after blobs are already uploaded).