Compare commits

..

83 Commits

Author SHA1 Message Date
Ronald A. Richardson
53a87d6f38 Hotfix: load iam engine for user-form modal when creating driver
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-12-19 23:17:51 +08:00
Ronald A. Richardson
d7f8f87315 Hotfix: onboarding wrapper stylings added back 2025-12-19 22:58:29 +08:00
Ron
36673ef564 Merge pull request #482 from fleetbase/dev-v0.7.23
dev-v0.7.23
2025-12-19 22:41:13 +08:00
Ronald A. Richardson
19341c81e7 Minor tweaks to user model and profile page 2025-12-19 22:39:41 +08:00
Ronald A. Richardson
b4ecf5bda9 bump FLEETBASE_VERSION in Dockerfile 2025-12-19 22:24:38 +08:00
Ronald A. Richardson
1b714a7ef8 Release ready 2025-12-19 22:23:39 +08:00
Ron
e41cd62ea5 Merge pull request #481 from fleetbase/feature/onboarding-wrapper-architecture
feat: Add wrapper component support to onboarding orchestrator
2025-12-19 15:56:49 +08:00
Ronald A. Richardson
1ca1342052 feat: fixed optimization changes for octane, added deprecation workflow 2025-12-19 15:56:03 +08:00
roncodes
a5a5ddb0d5 perf: Optimize FrankenPHP/Octane configuration for high load
**Changes:**

1. **Caddyfile**:
   - Reduced num_threads from 24 to 20
   - Added request timeouts (read_body: 10s, write: 60s, idle: 120s)
   - With 4 containers: 20 × 4 = 80 total workers

2. **Dockerfile**:
   - Added explicit --workers=20 to octane:frankenphp command
   - Increased --max-requests from 250 to 1000
   - Applied to app-dev, app-release, and app stages

3. **Octane config**:
   - Enabled DisconnectFromDatabases listener
   - Enabled CollectGarbage listener
   - Prevents DB connection leaks and memory leaks

**Impact:**
- Better resource management under load
- Prevents connection pool exhaustion
- Requires db.t3.large (591 max connections) or better
- Supports up to 250 concurrent VUs

**Related:**
- Requires RDS upgrade from db.t4g.micro to db.t3.large
- Works with DB_CONNECTION_POOL_SIZE=25 (100 total connections)
- See configuration-analysis.md for details
2025-12-16 20:06:35 -05:00
Ronald A. Richardson
c51f3ca6c8 v0.7.23 2025-12-17 08:57:41 +08:00
roncodes
a9b172081a feat: Add lifecycle hooks support to onboarding orchestrator
- Add onFlowWillStart, onFlowDidStart, onStepWillChange, onStepDidChange, onFlowWillEnd, onFlowDidEnd hooks
- Hooks are optional and backward compatible with existing flows
- Add getCurrentPath() and isStepInPath() helper methods for multi-path flows
- Support dynamic next() functions (already existed, now documented)
- Maintain full backward compatibility with default@v1 flow
2025-12-11 23:10:23 -05:00
Ronald A. Richardson
a29ca0ecb9 feat: update onboarding-registry service to allow set default onboard flow on registration 2025-12-09 09:48:23 +08:00
roncodes
6442644438 feat: Add wrapper component support to onboarding orchestrator
- Add wrapper property to OnboardingOrchestratorService
- Update onboarding/yield component to render wrapper using lazy-engine-component
- Clean up onboard.hbs template to remove styling constraints
- Enable extensions to provide custom wrapper components for onboarding flows
2025-12-08 20:38:59 -05:00
Ron
0238632fdd Merge pull request #477 from fleetbase/dev-v0.7.22
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Organizations can now set their own alpha-numeric sender ID for SMS
2025-12-07 06:37:02 +00:00
Ronald A. Richardson
27652db9c3 Organizations can now set their own alpha-numeric sender ID for SMS 2025-12-07 14:15:31 +08:00
Ronald A. Richardson
5eaf2039d4 Fix: correct version of ember-ui in console/package.json
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-12-06 21:49:14 +08:00
Ron
d8c06ae0be Merge pull request #476 from fleetbase/dev-v0.7.21
v0.7.21 ~ 5x faster css compiling and flawless builds
2025-12-06 13:41:42 +00:00
Ronald A. Richardson
c9011b3ffa *critical* patch to load-extensions initializer 2025-12-06 21:38:02 +08:00
Ronald A. Richardson
520de0f6bc fixed formating on console/package.json 2025-12-06 19:39:24 +08:00
Ronald A. Richardson
ba6ed235e3 v0.7.21 ~ 5x faster css compiling and flawless builds 2025-12-06 19:26:17 +08:00
Ronald A. Richardson
777d84a7fe Hotfix: added fleetbase-extension-generator lib dependency to package.json
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-12-05 23:20:04 +08:00
Ron
18fdfdf506 Merge pull request #475 from fleetbase/feat/remove-console-prebuild
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Remove console prebuild script, bump docker fleetbase version
2025-12-05 14:06:36 +00:00
Ronald A. Richardson
7e898dd54b Remove console prebuild script, bump docker fleetbase version 2025-12-05 22:02:46 +08:00
Ron
761c752a8e Merge pull request #474 from fleetbase/feature/extension-boot-refactor
feat: refactoring extension build pipelines
2025-12-05 13:14:24 +00:00
Ron
74d02efaa0 Merge pull request #470 from fleetbase/docs/add-translation-guide
docs: Add comprehensive translation contribution guide
2025-12-05 12:59:04 +00:00
Ron
aa928b43ba Merge pull request #464 from PicoBaz/add-persian-language
Add Persian (Farsi) Language Support in fa-ir.yaml
2025-12-05 12:58:43 +00:00
Ron
7ed422d893 Merge pull request #471 from johnfelipe/main
latin american spanish translation
2025-12-05 12:58:22 +00:00
Ronald A. Richardson
064fa12a43 Upgraded api dependencies 2025-12-05 20:53:44 +08:00
Ronald A. Richardson
32f0b22ed1 Merge remote-tracking branch 'origin/main' into feature/extension-boot-refactor 2025-12-05 20:49:32 +08:00
Ronald A. Richardson
4f87434911 Upgraded core dependencies 2025-12-05 20:43:53 +08:00
Ronald A. Richardson
498d519c49 Hard reset dependency files to main 2025-12-05 20:39:09 +08:00
Ronald A. Richardson
e4b008093d Fix api/composer.lock 2025-12-05 20:37:47 +08:00
Ronald A. Richardson
c31377b194 Merge branch 'feature/extension-boot-refactor' of github.com:fleetbase/fleetbase into feature/extension-boot-refactor 2025-12-05 20:32:29 +08:00
Ronald A. Richardson
6f397ea3cb fixing dependency merge conflicts 2025-12-05 20:32:04 +08:00
Ron
015c24585b Merge pull request #449 from valentinMERCIER/fix/docker-socket-example
Fix/docker socket example
2025-12-05 12:25:10 +00:00
roncodes
1f4b25faee feat: Clean up app/extensions directory before each build
Added directory cleanup logic to remove ./app/extensions before generating
new extension files. This prevents old/stale/removed extensions from
remaining in subsequent builds.

Changes:
- Added fs.rmSync to remove extensions directory if it exists
- Cleanup happens at the start of generateExtensionFiles()
- Directory is recreated during generation process
- Logs cleanup action for visibility

This ensures each build starts with a clean slate and only includes
currently installed extensions.
2025-12-05 07:20:24 -05:00
Ronald A. Richardson
3a193e414c almost ready for release 2025-12-05 20:15:55 +08:00
Ronald A. Richardson
9719632289 Preparing major release 2025-12-05 10:57:18 +08:00
roncodes
08dabaf138 fix: pass appInstance instead of appInstance.application
- Change window.Fleetbase = appInstance (not appInstance.application)
- Change setApplicationInstance(appInstance) (not appInstance.application)
- appInstance IS the ApplicationInstance, not the Application class
- Remove comment about RegistryService cascade
2025-12-02 21:07:27 -05:00
roncodes
7ae3ea95a2 feat: add set-application-instance initializer to console
- Create app/instance-initializers/set-application-instance.js
- Sets window.Fleetbase = appInstance.application for global access
- Calls universeService.setApplicationInstance() to cascade to RegistryService
- Ensures all services have access to root application container
- Required for UniverseRegistry singleton to work across engines
2025-12-02 21:00:56 -05:00
Ronald A. Richardson
9653cfcaf0 refactor of universe and lazy loading fix contd. 2025-12-03 09:55:12 +08:00
Ronald A. Richardson
cb7a2fb05b minor syntax tweaks 2025-11-28 18:10:24 +08:00
roncodes
fd569cfeaf fix: ensure intl polyfills load before runtime config
- Added name to load-intl-polyfills initializer
- Made load-runtime-config run after load-intl-polyfills
- Fixes race condition where intl service initialized before polyfill data loaded
- Resolves MISSING_DATA errors for all locales (mn-mn, etc.)

Initialization order now:
1. load-intl-polyfills (loads formatjs polyfill data)
2. load-runtime-config (loads fleetbase.config.json)
3. load-socketcluster-client
4. Other initializers
2025-11-28 04:52:07 -05:00
roncodes
0f9cd52bb4 fix: correct locale format from en-us to en-US
- Fixed ember-intl locale format to use proper case (en-US)
- Renamed translation file: en-us.yaml → en-US.yaml
- Updated fallbackLocale in ember-intl config
- Updated default locale in application route
- Fixes MISSING_DATA error in Intl.NumberFormat

The Intl API requires proper locale format (en-US not en-us)
2025-11-28 04:46:06 -05:00
Ronald A. Richardson
72ce000786 Merge branch 'feature/extension-boot-refactor' of github.com:fleetbase/fleetbase into feature/extension-boot-refactor 2025-11-28 17:43:08 +08:00
Ronald A. Richardson
c9477b78f2 feat: updated ember-cli-build.js 2025-11-28 17:42:43 +08:00
roncodes
affa141c9d perf: optimize fleetbase.config.json loading with localStorage caching
- Added 1-hour localStorage cache for runtime config
- Reduces 750ms+ HTTP request to instant cache lookup
- Cache automatically expires and refreshes
- Includes cache clear utility function
- Uses browser cache as fallback
- Performance logging with debug()
- Excluded JSON files from fingerprinting

Expected improvement: 783ms → <5ms (99.4% faster)
2025-11-28 04:37:29 -05:00
roncodes
5726eb974f refactor: Move runtime config and router fix to proper Ember initializers
Refactored app.js boot sequence to follow Ember conventions:

1. Created app/initializers/load-runtime-config.js
   - Loads fleetbase.config.json before application boots
   - Uses deferReadiness/advanceReadiness pattern
   - Runs before all other initializers
   - Added performance timing with debug logging

2. Created app/instance-initializers/apply-router-fix.js
   - Applies router refresh bug fix patch
   - Runs as instance-initializer (needs app instance)
   - Runs before extension loading
   - Added performance timing with debug logging

3. Refactored app/app.js
   - Removed custom ready() hook
   - Removed document.addEventListener('DOMContentLoaded')
   - Removed manual deferReadiness/boot calls
   - Now uses standard Ember boot sequence
   - Clean, minimal implementation

Benefits:
- Follows Ember conventions and best practices
- Proper initialization order guaranteed
- Performance monitoring for boot phases
- Easier to debug and maintain
- No custom boot logic needed

Boot sequence:
1. load-runtime-config initializer (runs first)
2. Other initializers (socketcluster, etc.)
3. apply-router-fix instance-initializer
4. load-extensions instance-initializer
5. Other instance-initializers
6. Application ready
2025-11-27 23:12:14 -05:00
Ronald A. Richardson
ca3050905d feat: preparing for performance tweaking to page load sub 1s 2025-11-28 11:56:38 +08:00
roncodes
cf2ced1512 feat: Add ASCII logo header to generated files and improve console output
- Added Option 1 ASCII logo header to all generated files
- Header includes copyright notice and AGPL-3.0 license
- Improved console logging with [Fleetbase] prefix
- Better formatted output with separators and extension listing
- Cleaner file change notifications
- More professional and readable build output
2025-11-27 06:12:02 -05:00
roncodes
93b7224335 feat: Generate app/extensions/index.js with new format and cleanup
- Changed extension loader generation to create app/extensions/index.js
- New format uses direct imports instead of dynamic imports
- Added getExtensionLoader helper function
- Removed unused plugins directory
- Removed old extension-loaders.generated.js file
- Cleaner and simpler loader structure
2025-11-27 05:48:54 -05:00
roncodes
9a053cfd9f fix: Use exact working logic from prebuild.js in in-repo addon
- Copied all working functions from prebuild.js
- Adapted only for addon context (this.project.root instead of __dirname)
- No logic changes, just direct migration
- Should work exactly like prebuild.js did
2025-11-27 05:30:14 -05:00
roncodes
56897af057 Revert refactor - caused regression in extension discovery 2025-11-27 05:28:25 -05:00
roncodes
b27e485a44 fix: Router mount context issue and refactor into modular utilities
- Fixed 'this' context issue in addConsoleExtensions/addRootExtensions
- Refactored monolithic index.js into separate utility modules:
  - discover-extensions.js - Extension discovery logic
  - generate-extension-shims.js - Shim file generation
  - generate-extension-loaders.js - Loader map generation
  - generate-router.js - Router AST manipulation
  - generate-manifest.js - Manifest generation
  - watch-extensions.js - File watching logic
- Simplified index.js to orchestrate utilities
- Improved code organization and maintainability
2025-11-27 05:23:38 -05:00
Ronald A. Richardson
94c5407387 fix: syntax error in extensions generator lib 2025-11-27 18:15:27 +08:00
roncodes
54ac27b304 fix: Use correct router.map.js path (console/router.map.js not app/router.map.js) 2025-11-27 04:56:56 -05:00
roncodes
4fb596c866 fix: Router generation and add file watching for extension changes
**Router Generation Fixes:**
- Fix router.js parsing (was looking for non-existent router.map.js)
- Parse Router.map(function() {...}) structure correctly
- Fix console route detection (check for path: '/' config)
- Fix function expression location (3rd arg after path config)
- Fix root extensions to use this.mount() not router.mount()

**File Watching:**
- Add chokidar to watch extension.js files in development
- Regenerate shims and loaders when extension.js changes
- Cache discovered extensions for regeneration
- Only watch in development mode

**Issues Fixed:**
1. Router.js now properly contains engine mounts
2. Extension.js changes trigger automatic regeneration
2025-11-27 04:35:16 -05:00
roncodes
3f12e98448 refactor: Simplify extension generator to write directly to app directory
- Remove Broccoli tree complexity, use included() hook instead
- Write files directly to app/ using Node.js fs operations
- Generate extension shims in app/extensions/
- Generate extension loaders in app/utils/extension-loaders.generated.js
- Generate router.js with proper engine mounts using recast AST
- Generate extensions.json manifest in public/
- Add recast dependency for AST manipulation
- Much simpler and more reliable than Broccoli trees
2025-11-27 04:09:59 -05:00
roncodes
a1fc1e4ff8 fix: Correct Funnel srcDir configuration and add improved logging
- Fix BroccoliMergeTrees error by adding srcDir to Funnel calls
- extensionShims: srcDir 'extensions' (files in outputPath/extensions/)
- extensionLoaders: srcDir 'utils' (files in outputPath/utils/)
- routerGen: srcDir '/' (files at outputPath root)
- Add comprehensive debug logging to all plugins
- Add detailed logging to in-repo addon index.js
- Improves debugging and troubleshooting of build process
2025-11-27 03:25:56 -05:00
Ronald A. Richardson
d622b617c3 feat: migrate from plugins to in-app-repo with build hooks 2025-11-27 16:13:44 +08:00
roncodes
edba6c8396 fix: Correct Broccoli plugin wiring to merge into app.trees.app
- Merge generatedAppTree into app.trees.app before app.toTree()
- Fix Funnel srcDir/destDir configuration
- Remove prebuild extension generation (now handled by Broccoli)
- Add extensionManifestTree to expose dist/extensions.json
- Files now treated as real app source for ember-auto-import
2025-11-27 01:12:36 -05:00
roncodes
a0fc1ce402 feat: Implement extension boot refactor with prebuild approach
- Modified prebuild.js to generate extension shims in app/extensions/
- Generate extension-loaders.js with dynamic import map
- Added ember-auto-import allowAppImports configuration
- Extension setup code is now code-split into separate chunks
- Removed FleetbaseExtensionsIndexer in favor of prebuild generation
- Added generated files to .gitignore

Successfully tested: extension code is code-split into separate chunk
(chunk.app_extensions_fleetops_js.*.js)
2025-11-27 00:34:12 -05:00
Ronald A. Richardson
ffab66ac6c feat: refactoring extension build pipelines 2025-11-27 11:20:14 +08:00
roncodes
d3555c7c82 Revert "refactor: Migrate FleetbaseConsole to new Universe architecture"
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
This reverts commit 4b12efef41.
2025-11-26 01:08:33 -05:00
roncodes
4b12efef41 refactor: Migrate FleetbaseConsole to new Universe architecture
## Changes

### Removed Old Instance Initializers
- Deleted `app/instance-initializers/load-extensions.js` (used bootEngines)
- Deleted `app/instance-initializers/initialize-widgets.js` (now in initialize-universe)

### Created New Instance Initializer
- `app/instance-initializers/initialize-universe.js`
  - Creates console-specific registries
  - Registers default dashboard widgets using WidgetService
  - Loads extension.js files from enabled extensions
  - No longer calls bootEngines (enables lazy loading)

### Migrated Application Route
- `app/routes/application.js`
  - Replaced `@service universe` with specialized services
  - Uses `@service('universe/hook-service')` for hook execution
  - Uses `@service('universe/extension-manager')` for boot waiting
  - `universe.callHooks()` → `hookService.execute()`
  - `universe.booting()` → `extensionManager.waitForBoot()`

### Migrated Dashboard Widget Panel
- `app/components/dashboard/widget-panel.js`
  - Replaced `@service universe` with `@service('universe/widget-service')`
  - `universe.getDashboardWidgets()` → `widgetService.getWidgets('dashboard')`

### Migrated Dashboard Model
- `app/models/dashboard.js`
  - `universe.getDashboardRegistry()` → `widgetService.getRegistry()`
  - Looks up `service:universe/widget-service` instead of `service:universe`

### What Stayed the Same
- `app/controllers/console.js` - Event system usage unchanged
- `app/controllers/console/notifications.js` - Event system usage unchanged
- Event system (`on`, `trigger`) remains on Universe facade

## Benefits

-  Enables true lazy loading (engines load on-demand)
-  Separation of concerns via specialized services
-  Clearer service responsibilities
-  Better performance (no bootEngines at startup)
-  Maintains backward compatibility for events
2025-11-26 00:59:39 -05:00
Tu Nombre
c80f507720 latin american spanish translation 2025-11-19 08:02:28 -05:00
roncodes
2da7ee9c19 docs: add comprehensive translation contribution guide 2025-11-18 23:10:09 -05:00
Ronald A. Richardson
658568e4ec bump console/package.json version
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-11-17 19:24:00 +08:00
Ron
8a487b2352 Merge pull request #469 from fleetbase/dev-v0.7.19
v0.7.19
2025-11-17 19:22:41 +08:00
Ronald A. Richardson
bc89218a26 add fleetops-data 2025-11-17 19:17:01 +08:00
Ronald A. Richardson
5a4f7e2ae3 upgraded dependencies 2025-11-17 18:43:28 +08:00
Ronald A. Richardson
9fa1bf54d2 v0.7.19 ~ A major leap forward in scheduling, reporting, and user interface capabilities. 2025-11-17 18:17:51 +08:00
mehdi
13cfe00958 Add Persian (Farsi) lang in api/resource/lang , fa folder 2025-11-10 11:47:50 +03:30
PicoBaz
6cab778f93 Create fa-ir.yaml (Add Persian (Farsi) translations in fa-ir.yaml) 2025-11-10 09:31:39 +03:30
Ronald A. Richardson
b98eb3adf5 Merge branch 'main' of github.com:fleetbase/fleetbase into ron/dev-v0.7.19 2025-11-10 11:30:12 +08:00
Ronald A. Richardson
5473b50c40 development in progress 2025-11-10 11:27:50 +08:00
Ronald A. Richardson
d9f415528e hotfix: update api/composer.json
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-11-10 11:02:06 +08:00
Ron
76b0bfbfcd Merge pull request #463 from fleetbase/dev-v0.7.18
v0.7.18
2025-11-10 10:52:51 +08:00
Ronald A. Richardson
0432003163 latest storefront 2025-11-10 10:52:09 +08:00
Ron
7cb4654c86 Merge pull request #460 from fleetbase/dev-v0.7.17
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.17 ~ hotfix router map
2025-11-06 21:59:37 +08:00
lapin
b9adb92fc1 Add Docker socket configuration examples and platform compatibility
This commit provides proper configuration examples for the SocketCluster
WebSocket service and improves platform compatibility.

Changes:
- Platform: Add linux/amd64 platform specification for Apple Silicon compatibility
- CORS: Remove hardcoded permissive origins and add secure configuration examples
- Examples: Create docker-compose.override.yml.example with proper WebSocket origins

The configuration now supports:
- Development: localhost-only origins (http/https/ws/wss protocols)
- Production: Domain-specific origins with WebSocket protocol support
- Security: Prevents unauthorized cross-origin WebSocket connections

Updated documentation explains how to configure WebSocket origins securely
for different deployment environments.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 10:17:37 +01:00
lapin
d81bd4e900 Merge remote-tracking branch 'upstream/main' 2025-10-28 09:47:55 +01:00
lapin
8a21593d9a Update docker-compose.yml 2025-10-24 15:21:42 +02:00
104 changed files with 5674 additions and 2444 deletions

View File

@@ -8,6 +8,7 @@
http://:8000 {
root * /fleetbase/api/public
encode zstd br gzip
php_server {
resolve_root_symlink
}

View File

@@ -125,10 +125,10 @@ Next copy this value to the `APP_KEY` environment variable in the application co
**Routing:** Fleetbase ships with a default OSRM server hosted by `[router.project-osrm.org](https://router.project-osrm.org)` but youre able to use your own or any other OSRM compatible server. You can modify this in the `console/environments` directory by modifying the .env file of the environment youre deploying and setting the `OSRM_HOST` to the OSRM server for Fleetbase to use.
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If youre deploying with docker then its easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
**Services:** There are a few environment variables which need to be set for Fleetbase to function with full features. If you're deploying with docker then it's easiest to just create a `docker-compose.override.yml` and supply the environment variables in this file.
```yaml
version: 3.8
version: "3.8"
services:
application:
environment:
@@ -141,8 +141,18 @@ services:
TWILIO_SID:
TWILIO_TOKEN:
TWILIO_FROM:
socket:
environment:
# IMPORTANT: Configure WebSocket origins for security
# Development (localhost only - include WebSocket protocols):
SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*,https://localhost:*,ws://localhost:*,wss://localhost:*"}'
# Production (replace with your actual domain):
# SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*,wss://yourdomain.com:*"}'
```
**WebSocket Security:** The `SOCKETCLUSTER_OPTIONS` environment variable controls which domains can connect to the WebSocket server. Always restrict origins to your specific domains in production to prevent security vulnerabilities.
You can learn more about full installation, and configuration in the [official documentation](https://docs.fleetbase.io/getting-started/install).
## 🚀 Deploy on AWS in One Click

View File

@@ -1,23 +1,45 @@
# 🚀 Fleetbase v0.7.18 — 2025-11-10
# 🚀 Fleetbase v0.7.23 — 2025-12-19
> "Hotfix IAM user validation, make online/offline toggle silent"
> "🤯 Insane optimization and performance upgrades + horizontal scaling support 🚀"
---
## ✨ Highlights
- Hotfix validateRequest implementation to not rewrite request params
- Hotfix user validation password optional for creation
- Made online/offline endpoint for drivers silent
- Hotfix QPay payment gateway on Storefront + ebarimt reciept fix
- Major performance and optimization improvements which support horizontal scaling
- Ability to resize images on upload using resize parameters
- Several patches in FleetOps - fixed service rates and missing translations, improvements and patch to scheduler
- Added a new `LanguageService` available in ember-core
- Minor `@fleetbase/ember-ui` improvements
### New Features
- **Improved API performance** with two-layer caching system (Redis + ETag validation) for user and organization data
- **Reduced bandwidth usage** with automatic HTTP 304 Not Modified responses via new ValidateETag middleware
- **Faster page loads** with intelligent cache invalidation that updates immediately when data changes
- **New UserCacheService class** for centralized cache management across the application
- **Image resizing support** for dynamic image dimensions via URL parameters
- Added `ApiModelCache` class - Provides intelligent Redis-based caching for API query results with automatic invalidation
- Added `HasApiModelCache` trait - Enables models to cache query results with a single method call
### Performance Improvements
- Optimized form data syncing to eliminate N+1 query problems, reducing database queries from N to 2 for relationship syncing
- Implemented cache stampede prevention to handle high concurrent load efficiently
- Added cache versioning system for automatic invalidation when data changes
### Developer Experience
- Added `X-Cache-Status` header to API responses for easy cache debugging (HIT/MISS visibility)
- Automatic multi-tenant cache key generation for company-scoped data isolation
- Graceful fallback to direct queries when cache is unavailable
---
## ⚠️ Breaking Changes
- None
- None 🙂
---
## 🔧 Upgrade Steps
```bash
# Pull latest version
git pull origin main --no-rebase
@@ -30,5 +52,7 @@ docker compose down && docker compose up -d
docker compose exec application bash -c "./deploy.sh"
```
---
## Need help?
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)
Join the discussion on [GitHub Discussions](https://github.com/fleetbase/ember-ui/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)

100
TRANSLATING.md Normal file
View File

@@ -0,0 +1,100 @@
# Contributing to Fleetbase Translations
First off, thank you for considering contributing to Fleetbase translations! Your efforts help make Fleetbase accessible to a global audience. This guide will walk you through the process of adding or updating language translations for the Fleetbase platform and its various extensions.
## Understanding the Structure
Fleetbase is a modular system. The main application, known as Fleetbase Console, has its own set of translations. Additionally, each extension (like FleetOps or Storefront) also contains its own translation files. This means that to provide a complete translation for a specific language, you may need to contribute to multiple repositories.
- **Main Application (`fleetbase/fleetbase`)**: Contains the core translation files for the Fleetbase Console.
- **Extensions/Modules**: Each extension has its own repository and its own set of translation files.
## File Format and Location
All translation files are in the **YAML** format (`.yaml` or `.yml`). The base language for all translations is American English (`en-us.yaml`).
- In the main `fleetbase/fleetbase` repository, the translation files are located at `./console/translations/`.
- In each extension repository, the translation files are located at `./translations/`.
Translation files are named using the language and region code, for example:
- `en-us.yaml` (American English)
- `fr-fr.yaml` (French, France)
- `zh-cn.yaml` (Chinese, Simplified)
## How to Contribute Translations
Follow these steps to contribute a new translation or update an existing one.
### Step 1: Fork and Clone the Repository
First, you need to fork the repository you want to contribute to. This could be the main `fleetbase/fleetbase` repository or one of the extension repositories. After forking, clone it to your local machine.
### Step 2: Create or Update a Language File
Navigate to the appropriate translations directory (`./console/translations/` or `./translations/`).
- **To add a new language**: Copy the `en-us.yaml` file and rename it to your target language code (e.g., `es-es.yaml`).
- **To update an existing language**: Open the existing language file. You can compare it with `en-us.yaml` to find missing keys or phrases that need updating.
### Step 3: Translate the Content
Open the YAML file in a text editor. You will see a structure of nested keys and values.
```yaml
# Example from en-us.yaml
common:
new: New
create: Create
delete-selected-count: Delete {count} Selected
```
When translating, you should:
- **Only translate the values**, not the keys. For example, in `new: New`, you would only translate `New`.
- **Keep placeholders intact**. Some phrases contain placeholders like `{count}` or `{resource}`. These should not be translated. They are used by the application to insert dynamic values.
Here is an example of the French translation for the keys above:
```yaml
# Example from fr-fr.yaml
common:
new: Nouveau
create: Créer
delete-selected-count: Supprimer {count} sélectionné(s)
```
### Step 4: Submit a Pull Request
Once you have finished translating, commit your changes and push them to your forked repository. Then, open a pull request to the original Fleetbase repository.
- Make sure your pull request has a clear title and description of the changes you made.
- If you are translating an extension, you may need to submit a pull request to the extension's repository. If your changes also affect the main console, a separate PR to the `fleetbase/fleetbase` repository might be necessary.
Your contribution will be reviewed by the Fleetbase team, and once approved, it will be merged into the project.
## Translation Repositories
Here is a list of the primary repositories that accept translation contributions:
| Repository | Translation Path |
| ---------------------------------------- | ----------------------------- |
| [fleetbase/fleetbase][1] | `./console/translations/` |
| [fleetbase/fleetops][2] | `./translations/` |
| [fleetbase/storefront][3] | `./translations/` |
| [fleetbase/dev-engine][4] | `./translations/` |
| [fleetbase/iam-engine][5] | `./translations/` |
| [fleetbase/pallet][6] | `./translations/` |
| [fleetbase/ledger][7] | `./translations/` |
| [fleetbase/registry-bridge][8] | `./translations/` |
[1]: https://github.com/fleetbase/fleetbase
[2]: https://github.com/fleetbase/fleetops
[3]: https://github.com/fleetbase/storefront
[4]: https://github.com/fleetbase/dev-engine
[5]: https://github.com/fleetbase/iam-engine
[6]: https://github.com/fleetbase/pallet
[7]: https://github.com/fleetbase/ledger
[8]: https://github.com/fleetbase/registry-bridge
Thank you again for your contribution to the Fleetbase community!

View File

@@ -40,7 +40,6 @@ class Kernel extends HttpKernel
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

View File

@@ -18,12 +18,12 @@
}
],
"require": {
"php": "^8.0",
"php": ">=8.0 <=8.2.28",
"appstract/laravel-opcache": "^4.0",
"fleetbase/core-api": "^1.6.23",
"fleetbase/fleetops-api": "^0.6.24",
"fleetbase/registry-bridge": "^0.1.0",
"fleetbase/storefront-api": "^0.4.5",
"fleetbase/core-api": "^1.6.29",
"fleetbase/fleetops-api": "^0.6.31",
"fleetbase/registry-bridge": "^0.1.2",
"fleetbase/storefront-api": "^0.4.10",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",

1147
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -51,7 +51,7 @@ return [
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'channels' => ['single', 'stdout'],
'ignore_exceptions' => false,
],

View File

@@ -105,8 +105,8 @@ return [
OperationTerminated::class => [
FlushOnce::class,
FlushTemporaryContainerInstances::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
DisconnectFromDatabases::class,
CollectGarbage::class,
],
WorkerErrorOccurred::class => [

View File

@@ -0,0 +1,20 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Estas credenciales no coinciden con nuestros registros.',
'password' => 'La contraseña proporcionada es incorrecta.',
'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, intenta de nuevo en :seconds segundos.',
];

View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Anterior',
'next' => 'Siguiente &raquo;',
];

View File

@@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'reset' => '¡Tu contraseña ha sido restablecida!',
'sent' => '¡Te hemos enviado por correo el enlace para restablecer tu contraseña!',
'throttled' => 'Por favor espera antes de volver a intentar.',
'token' => 'Este token de restablecimiento de contraseña es inválido.',
'user' => "No podemos encontrar un usuario con esa dirección de correo electrónico.",
];

View File

@@ -0,0 +1,163 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'El campo :attribute debe ser aceptado.',
'accepted_if' => 'El campo :attribute debe ser aceptado cuando :other sea :value.',
'active_url' => 'El campo :attribute no es una URL válida.',
'after' => 'El campo :attribute debe ser una fecha posterior a :date.',
'after_or_equal' => 'El campo :attribute debe ser una fecha posterior o igual a :date.',
'alpha' => 'El campo :attribute solo debe contener letras.',
'alpha_dash' => 'El campo :attribute solo debe contener letras, números, guiones y guiones bajos.',
'alpha_num' => 'El campo :attribute solo debe contener letras y números.',
'array' => 'El campo :attribute debe ser un arreglo.',
'before' => 'El campo :attribute debe ser una fecha anterior a :date.',
'before_or_equal' => 'El campo :attribute debe ser una fecha anterior o igual a :date.',
'between' => [
'numeric' => 'El campo :attribute debe estar entre :min y :max.',
'file' => 'El campo :attribute debe estar entre :min y :max kilobytes.',
'string' => 'El campo :attribute debe tener entre :min y :max caracteres.',
'array' => 'El campo :attribute debe tener entre :min y :max elementos.',
],
'boolean' => 'El campo :attribute debe ser verdadero o falso.',
'confirmed' => 'La confirmación de :attribute no coincide.',
'current_password' => 'La contraseña es incorrecta.',
'date' => 'El campo :attribute no es una fecha válida.',
'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.',
'date_format' => 'El campo :attribute no coincide con el formato :format.',
'declined' => 'El campo :attribute debe ser rechazado.',
'declined_if' => 'El campo :attribute debe ser rechazado cuando :other sea :value.',
'different' => 'El campo :attribute y :other deben ser diferentes.',
'digits' => 'El campo :attribute debe tener :digits dígitos.',
'digits_between' => 'El campo :attribute debe tener entre :min y :max dígitos.',
'dimensions' => 'El campo :attribute tiene dimensiones de imagen inválidas.',
'distinct' => 'El campo :attribute tiene un valor duplicado.',
'email' => 'El campo :attribute debe ser una dirección de correo electrónico válida.',
'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes: :values.',
'enum' => 'El :attribute seleccionado es inválido.',
'exists' => 'El :attribute seleccionado es inválido.',
'file' => 'El campo :attribute debe ser un archivo.',
'filled' => 'El campo :attribute debe tener un valor.',
'gt' => [
'numeric' => 'El campo :attribute debe ser mayor que :value.',
'file' => 'El campo :attribute debe ser mayor que :value kilobytes.',
'string' => 'El campo :attribute debe ser mayor que :value caracteres.',
'array' => 'El campo :attribute debe tener más de :value elementos.',
],
'gte' => [
'numeric' => 'El campo :attribute debe ser mayor o igual a :value.',
'file' => 'El campo :attribute debe ser mayor o igual a :value kilobytes.',
'string' => 'El campo :attribute debe ser mayor o igual a :value caracteres.',
'array' => 'El campo :attribute debe tener :value elementos o más.',
],
'image' => 'El campo :attribute debe ser una imagen.',
'in' => 'El :attribute seleccionado es inválido.',
'in_array' => 'El campo :attribute no existe en :other.',
'integer' => 'El campo :attribute debe ser un número entero.',
'ip' => 'El campo :attribute debe ser una dirección IP válida.',
'ipv4' => 'El campo :attribute debe ser una dirección IPv4 válida.',
'ipv6' => 'El campo :attribute debe ser una dirección IPv6 válida.',
'json' => 'El campo :attribute debe ser una cadena JSON válida.',
'lt' => [
'numeric' => 'El campo :attribute debe ser menor que :value.',
'file' => 'El campo :attribute debe ser menor que :value kilobytes.',
'string' => 'El campo :attribute debe ser menor que :value caracteres.',
'array' => 'El campo :attribute debe tener menos de :value elementos.',
],
'lte' => [
'numeric' => 'El campo :attribute debe ser menor o igual a :value.',
'file' => 'El campo :attribute debe ser menor o igual a :value kilobytes.',
'string' => 'El campo :attribute debe ser menor o igual a :value caracteres.',
'array' => 'El campo :attribute no debe tener más de :value elementos.',
],
'mac_address' => 'El campo :attribute debe ser una dirección MAC válida.',
'max' => [
'numeric' => 'El campo :attribute no debe ser mayor que :max.',
'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.',
'string' => 'El campo :attribute no debe ser mayor que :max caracteres.',
'array' => 'El campo :attribute no debe tener más de :max elementos.',
],
'mimes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
'mimetypes' => 'El campo :attribute debe ser un archivo de tipo: :values.',
'min' => [
'numeric' => 'El campo :attribute debe ser al menos :min.',
'file' => 'El campo :attribute debe ser al menos :min kilobytes.',
'string' => 'El campo :attribute debe tener al menos :min caracteres.',
'array' => 'El campo :attribute debe tener al menos :min elementos.',
],
'multiple_of' => 'El campo :attribute debe ser un múltiplo de :value.',
'not_in' => 'El :attribute seleccionado es inválido.',
'not_regex' => 'El formato del campo :attribute es inválido.',
'numeric' => 'El campo :attribute debe ser un número.',
'password' => 'La contraseña es incorrecta.',
'present' => 'El campo :attribute debe estar presente.',
'prohibited' => 'El campo :attribute está prohibido.',
'prohibited_if' => 'El campo :attribute está prohibido cuando :other sea :value.',
'prohibited_unless' => 'El campo :attribute está prohibido a menos que :other esté en :values.',
'prohibits' => 'El campo :attribute prohíbe que :other esté presente.',
'regex' => 'El formato del campo :attribute es inválido.',
'required' => 'El campo :attribute es obligatorio.',
'required_array_keys' => 'El campo :attribute debe contener entradas para: :values.',
'required_if' => 'El campo :attribute es obligatorio cuando :other sea :value.',
'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.',
'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.',
'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.',
'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.',
'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values están presentes.',
'same' => 'El campo :attribute y :other deben coincidir.',
'size' => [
'numeric' => 'El campo :attribute debe ser :size.',
'file' => 'El campo :attribute debe ser :size kilobytes.',
'string' => 'El campo :attribute debe tener :size caracteres.',
'array' => 'El campo :attribute debe contener :size elementos.',
],
'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes: :values.',
'string' => 'El campo :attribute debe ser una cadena de texto.',
'timezone' => 'El campo :attribute debe ser una zona horaria válida.',
'unique' => 'El campo :attribute ya ha sido tomado.',
'uploaded' => 'El campo :attribute falló al subir.',
'url' => 'El campo :attribute debe ser una URL válida.',
'uuid' => 'El campo :attribute debe ser un UUID válido.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];

View File

@@ -0,0 +1,15 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان احراز هویت
|--------------------------------------------------------------------------
|
| خطوط زبان زیر در طول احراز هویت برای پیام‌های مختلفی که باید به کاربر نمایش دهیم استفاده می‌شوند.
| شما می‌توانید این خطوط زبان را بر اساس نیازهای برنامه خود تغییر دهید.
|
*/
'failed' => 'این اطلاعات ورود با سوابق ما مطابقت ندارد.',
'password' => 'رمز عبور ارائه‌شده نادرست است.',
'throttle' => 'تعداد تلاش‌های ورود بیش از حد زیاد است. لطفاً پس از :seconds ثانیه دوباره تلاش کنید.',
];

View File

@@ -0,0 +1,18 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان صفحه‌بندی
|--------------------------------------------------------------------------
|
| خطوط زبان زیر توسط کتابخانه صفحه‌بندی برای ساخت لینک‌های صفحه‌بندی ساده استفاده می‌شوند.
| شما می‌توانید این خطوط را به دلخواه تغییر دهید تا با نیازهای برنامه خود سازگار شوند.
|
*/
'previous' => '&laquo; قبلی',
'next' => 'بعدی &raquo;',
];

View File

@@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان بازنشانی رمز عبور
|--------------------------------------------------------------------------
|
| خطوط زبان زیر خطوط پیش‌فرضی هستند که با دلایلی که توسط کارگزار رمز عبور
| برای تلاش‌های ناموفق به‌روزرسانی رمز عبور ارائه می‌شوند، مطابقت دارند،
| مانند توکن نامعتبر یا رمز عبور جدید نامعتبر.
|
*/
'reset' => 'رمز عبور شما بازنشانی شد!',
'sent' => 'لینک بازنشانی رمز عبور به ایمیل شما ارسال شد!',
'throttled' => 'لطفاً قبل از تلاش مجدد صبر کنید.',
'token' => 'این توکن بازنشانی رمز عبور نامعتبر است.',
'user' => 'کاربری با این آدرس ایمیل یافت نشد.',
];

View File

@@ -0,0 +1,163 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| خطوط زبان اعتبارسنجی
|--------------------------------------------------------------------------
|
| خطوط زبان زیر شامل پیام‌های خطای پیش‌فرض استفاده‌شده توسط کلاس اعتبارسنجی هستند.
| برخی از این قوانین نسخه‌های متعددی دارند، مانند قوانین مربوط به اندازه.
| شما می‌توانید این پیام‌ها را در اینجا به دلخواه تنظیم کنید.
|
*/
'accepted' => 'فیلد :attribute باید پذیرفته شود.',
'accepted_if' => 'فیلد :attribute باید پذیرفته شود وقتی :other برابر با :value باشد.',
'active_url' => 'فیلد :attribute یک URL معتبر نیست.',
'after' => 'فیلد :attribute باید تاریخی پس از :date باشد.',
'after_or_equal' => 'فیلد :attribute باید تاریخی پس از یا برابر با :date باشد.',
'alpha' => 'فیلد :attribute فقط باید شامل حروف باشد.',
'alpha_dash' => 'فیلد :attribute فقط باید شامل حروف، اعداد، خط تیره و زیرخط باشد.',
'alpha_num' => 'فیلد :attribute فقط باید شامل حروف و اعداد باشد.',
'array' => 'فیلد :attribute باید یک آرایه باشد.',
'before' => 'فیلد :attribute باید تاریخی قبل از :date باشد.',
'before_or_equal' => 'فیلد :attribute باید تاریخی قبل از یا برابر با :date باشد.',
'between' => [
'numeric' => 'فیلد :attribute باید بین :min و :max باشد.',
'file' => 'فیلد :attribute باید بین :min و :max کیلوبایت باشد.',
'string' => 'فیلد :attribute باید بین :min و :max کاراکتر باشد.',
'array' => 'فیلد :attribute باید بین :min و :max آیتم داشته باشد.',
],
'boolean' => 'فیلد :attribute باید true یا false باشد.',
'confirmed' => 'تأیید فیلد :attribute مطابقت ندارد.',
'current_password' => 'رمز عبور نادرست است.',
'date' => 'فیلد :attribute یک تاریخ معتبر نیست.',
'date_equals' => 'فیلد :attribute باید تاریخی برابر با :date باشد.',
'date_format' => 'فیلد :attribute با فرمت :format مطابقت ندارد.',
'declined' => 'فیلد :attribute باید رد شود.',
'declined_if' => 'فیلد :attribute باید رد شود وقتی :other برابر با :value باشد.',
'different' => 'فیلد :attribute و :other باید متفاوت باشند.',
'digits' => 'فیلد :attribute باید :digits رقم باشد.',
'digits_between' => 'فیلد :attribute باید بین :min و :max رقم باشد.',
'dimensions' => 'فیلد :attribute دارای ابعاد تصویر نامعتبر است.',
'distinct' => 'فیلد :attribute دارای مقدار تکراری است.',
'email' => 'فیلد :attribute باید یک آدرس ایمیل معتبر باشد.',
'ends_with' => 'فیلد :attribute باید با یکی از مقادیر زیر پایان یابد: :values.',
'enum' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'exists' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'file' => 'فیلد :attribute باید یک فایل باشد.',
'filled' => 'فیلد :attribute باید دارای مقدار باشد.',
'gt' => [
'numeric' => 'فیلد :attribute باید بزرگ‌تر از :value باشد.',
'file' => 'فیلد :attribute باید بزرگ‌تر از :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید بیش از :value کاراکتر باشد.',
'array' => 'فیلد :attribute باید بیش از :value آیتم داشته باشد.',
],
'gte' => [
'numeric' => 'فیلد :attribute باید بزرگ‌تر یا برابر با :value باشد.',
'file' => 'فیلد :attribute باید بزرگ‌تر یا برابر با :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید بیش از یا برابر با :value کاراکتر باشد.',
'array' => 'فیلد :attribute باید :value آیتم یا بیشتر داشته باشد.',
],
'image' => 'فیلد :attribute باید یک تصویر باشد.',
'in' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'in_array' => 'فیلد :attribute در :other وجود ندارد.',
'integer' => 'فیلد :attribute باید یک عدد صحیح باشد.',
'ip' => 'فیلد :attribute باید یک آدرس IP معتبر باشد.',
'ipv4' => 'فیلد :attribute باید یک آدرس IPv4 معتبر باشد.',
'ipv6' => 'فیلد :attribute باید یک آدرس IPv6 معتبر باشد.',
'json' => 'فیلد :attribute باید یک رشته JSON معتبر باشد.',
'lt' => [
'numeric' => 'فیلد :attribute باید کمتر از :value باشد.',
'file' => 'فیلد :attribute باید کمتر از :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید کمتر از :value کاراکتر باشد.',
'array' => 'فیلد :attribute باید کمتر از :value آیتم داشته باشد.',
],
'lte' => [
'numeric' => 'فیلد :attribute باید کمتر یا برابر با :value باشد.',
'file' => 'فیلد :attribute باید کمتر یا برابر با :value کیلوبایت باشد.',
'string' => 'فیلد :attribute باید کمتر یا برابر با :value کاراکتر باشد.',
'array' => 'فیلد :attribute نباید بیش از :value آیتم داشته باشد.',
],
'mac_address' => 'فیلد :attribute باید یک آدرس MAC معتبر باشد.',
'max' => [
'numeric' => 'فیلد :attribute نباید بزرگ‌تر از :max باشد.',
'file' => 'فیلد :attribute نباید بزرگ‌تر از :max کیلوبایت باشد.',
'string' => 'فیلد :attribute نباید بیش از :max کاراکتر باشد.',
'array' => 'فیلد :attribute نباید بیش از :max آیتم داشته باشد.',
],
'mimes' => 'فیلد :attribute باید یک فایل از نوع: :values باشد.',
'mimetypes' => 'فیلد :attribute باید یک فایل از نوع: :values باشد.',
'min' => [
'numeric' => 'فیلد :attribute باید حداقل :min باشد.',
'file' => 'فیلد :attribute باید حداقل :min کیلوبایت باشد.',
'string' => 'فیلد :attribute باید حداقل :min کاراکتر باشد.',
'array' => 'فیلد :attribute باید حداقل :min آیتم داشته باشد.',
],
'multiple_of' => 'فیلد :attribute باید مضربی از :value باشد.',
'not_in' => 'مقدار انتخاب‌شده برای :attribute نامعتبر است.',
'not_regex' => 'فرمت فیلد :attribute نامعتبر است.',
'numeric' => 'فیلد :attribute باید یک عدد باشد.',
'password' => 'رمز عبور نادرست است.',
'present' => 'فیلد :attribute باید وجود داشته باشد.',
'prohibited' => 'فیلد :attribute ممنوع است.',
'prohibited_if' => 'فیلد :attribute وقتی :other برابر با :value باشد ممنوع است.',
'prohibited_unless' => 'فیلد :attribute ممنوع است مگر اینکه :other در :values باشد.',
'prohibits' => 'فیلد :attribute مانع حضور :other می‌شود.',
'regex' => 'فرمت فیلد :attribute نامعتبر است.',
'required' => 'فیلد :attribute الزامی است.',
'required_array_keys' => 'فیلد :attribute باید شامل ورودی‌هایی برای: :values باشد.',
'required_if' => 'فیلد :attribute وقتی :other برابر با :value باشد الزامی است.',
'required_unless' => 'فیلد :attribute الزامی است مگر اینکه :other در :values باشد.',
'required_with' => 'فیلد :attribute وقتی :values وجود دارد الزامی است.',
'required_with_all' => 'فیلد :attribute وقتی همه :values وجود دارند الزامی است.',
'required_without' => 'فیلد :attribute وقتی :values وجود ندارد الزامی است.',
'required_without_all' => 'فیلد :attribute وقتی هیچ‌کدام از :values وجود ندارند الزامی است.',
'same' => 'فیلد :attribute و :other باید یکسان باشند.',
'size' => [
'numeric' => 'فیلد :attribute باید :size باشد.',
'file' => 'فیلد :attribute باید :size کیلوبایت باشد.',
'string' => 'فیلد :attribute باید :size کاراکتر باشد.',
'array' => 'فیلد :attribute باید شامل :size آیتم باشد.',
],
'starts_with' => 'فیلد :attribute باید با یکی از مقادیر زیر شروع شود: :values.',
'string' => 'فیلد :attribute باید یک رشته باشد.',
'timezone' => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.',
'unique' => 'فیلد :attribute قبلاً استفاده شده است.',
'uploaded' => 'فیلد :attribute در آپلود ناموفق بود.',
'url' => 'فیلد :attribute باید یک URL معتبر باشد.',
'uuid' => 'فیلد :attribute باید یک UUID معتبر باشد.',
/*
|--------------------------------------------------------------------------
| خطوط زبان اعتبارسنجی سفارشی
|--------------------------------------------------------------------------
|
| در اینجا می‌توانید پیام‌های اعتبارسنجی سفارشی برای ویژگی‌ها را با استفاده از
| قرارداد "attribute.rule" برای نام‌گذاری خطوط مشخص کنید. این کار امکان
| تعیین سریع یک خط زبان سفارشی برای یک قانون خاص ویژگی را فراهم می‌کند.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'پیام سفارشی',
],
],
/*
|--------------------------------------------------------------------------
| ویژگی‌های اعتبارسنجی سفارشی
|--------------------------------------------------------------------------
|
| خطوط زبان زیر برای جایگزینی placeholder ویژگی‌های ما با چیزی کاربرپسندتر
| مانند "آدرس ایمیل" به جای "email" استفاده می‌شوند. این کار به ما کمک می‌کند
| پیام‌هایمان را گویاتر کنیم.
|
*/
'attributes' => [],
];

4
console/.gitignore vendored
View File

@@ -26,3 +26,7 @@
# broccoli-debug
/DEBUG/
# Auto-generated extension files
/app/extensions/
/app/utils/extension-loaders.js

View File

@@ -2,10 +2,7 @@ import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from '@fleetbase/console/config/environment';
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
import mapEngines from '@fleetbase/ember-core/utils/map-engines';
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
import applyRouterFix from './utils/router-refresh-patch';
import './deprecation-workflow';
export default class App extends Application {
modulePrefix = config.modulePrefix;
@@ -13,21 +10,6 @@ export default class App extends Application {
Resolver = Resolver;
extensions = [];
engines = {};
async ready() {
applyRouterFix(this);
const extensions = await loadExtensions();
this.extensions = extensions;
this.engines = mapEngines(extensions);
}
}
document.addEventListener('DOMContentLoaded', async () => {
await loadRuntimeConfig();
loadInitializers(App, config.modulePrefix);
let fleetbase = App.create();
fleetbase.deferReadiness();
fleetbase.boot();
});
loadInitializers(App, config.modulePrefix);

View File

@@ -1,31 +0,0 @@
<Overlay @isOpen={{@isOpen}} @onLoad={{this.setOverlayContext}} @position="right" @noBackdrop={{true}} @fullHeight={{true}} @width={{or this.width @width "400px"}}>
<Overlay::Header @title={{t "component.dashboard-widget-panel.select-widgets"}} @hideStatusDot={{true}} @titleWrapperClass="leading-5">
<div class="flex flex-1 justify-end">
<Button @type="default" @icon="times" @helpText={{t "component.dashboard-widget-panel.close-and-save"}} @onClick={{this.onPressClose}} />
</div>
</Overlay::Header>
<Overlay::Body @wrapperClass="new-service-rate-overlay-body px-4 space-y-4 pt-4">
<div class="grid grid-cols-1 gap-4 text-xs dark:text-gray-100">
{{#each this.availableWidgets as |widget|}}
<div
class="rounded-lg border border-gray-300 bg-white dark:border-gray-700 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all duration-300 ease-in-out shadow-md px-4 py-2 cursor-pointer"
{{on "click" (fn this.addWidgetToDashboard widget)}}
>
<div class="flex flex-row items-center leading-6 mb-2.5">
<div class="w-8 flex items-center justify-start">
<FaIcon @icon={{widget.icon}} class="text-lg text-gray-600 dark:text-gray-300" />
</div>
<p class="text-sm truncate font-semibold dark:text-gray-100 text-gray-800">
{{t "component.dashboard-widget-panel.widget-name" widgetName=widget.name}}
</p>
</div>
<div>
<p class="text-xs dark:text-gray-100 text-gray-800">{{widget.description}}</p>
</div>
</div>
{{/each}}
</div>
<Spacer @height="300px" />
</Overlay::Body>
</Overlay>

View File

@@ -1,60 +0,0 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class DashboardWidgetPanelComponent extends Component {
@service universe;
@tracked availableWidgets = [];
@tracked dashboard;
@tracked isOpen = true;
@service notifications;
/**
* Constructs the component and applies initial state.
*/
constructor(owner, { dashboard }) {
super(...arguments);
this.availableWidgets = this.universe.getDashboardWidgets();
this.dashboard = dashboard;
}
/**
* Sets the overlay context.
*
* @action
* @param {OverlayContextObject} overlayContext
*/
@action setOverlayContext(overlayContext) {
this.context = overlayContext;
if (typeof this.args.onLoad === 'function') {
this.args.onLoad(...arguments);
}
}
@action addWidgetToDashboard(widget) {
// If widget is a component definition/class
if (typeof widget.component === 'function') {
widget.component = widget.component.name;
}
this.args.dashboard.addWidget(widget).catch((error) => {
this.notifications.serverError(error);
});
}
/**
* Handles cancel button press.
*
* @action
*/
@action onPressClose() {
this.isOpen = false;
if (typeof this.args.onClose === 'function') {
this.args.onClose();
}
}
}

View File

@@ -8,7 +8,7 @@
</a>
</div>
<div class="px-4 py-2.5">
{{#if this.isLoading}}
{{#if this.loadBlogPosts.isRunning}}
<Spinner />
{{else}}
<ul class="space-y-2">

View File

@@ -1,28 +1,42 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { storageFor } from 'ember-local-storage';
import { add, isPast } from 'date-fns';
import { task } from 'ember-concurrency';
export default class FleetbaseBlogComponent extends Component {
@storageFor('local-cache') localCache;
@service fetch;
@tracked posts = [];
@tracked isLoading = false;
constructor() {
super(...arguments);
this.loadBlogPosts();
this.loadBlogPosts.perform();
}
@action loadBlogPosts() {
this.isLoading = true;
@task *loadBlogPosts() {
// Check if cached data and expiration are available
const cachedData = this.localCache.get('fleetbase-blog-data');
const expiration = this.localCache.get('fleetbase-blog-data-expiration');
return this.fetch
.get('lookup/fleetbase-blog')
.then((response) => {
this.posts = response;
})
.finally(() => {
this.isLoading = false;
});
// Check if the cached data is still valid
if (cachedData && isArray(cachedData) && expiration && !isPast(new Date(expiration))) {
// Use cached data
this.posts = cachedData;
} else {
// Fetch new data
try {
const data = yield this.fetch.get('lookup/fleetbase-blog');
this.posts = isArray(data) ? data : [];
if (data) {
this.localCache.set('fleetbase-blog-data', data);
this.localCache.set('fleetbase-blog-data-expiration', add(new Date(), { hours: 6 }));
}
} catch (err) {
debug('Failed to load blog: ' + err.message);
}
}
}
}

View File

@@ -52,7 +52,7 @@ export default class GithubCardComponent extends Component {
this.data = cachedData;
} else {
// Fetch new data
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase');
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase', { cache: 'default' });
if (response.ok) {
this.data = yield response.json();
this.localCache.set('fleetbase-github-data', this.data);
@@ -72,7 +72,7 @@ export default class GithubCardComponent extends Component {
this.tags = cachedTags;
} else {
// Fetch new tags
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags');
const response = yield fetch('https://api.github.com/repos/fleetbase/fleetbase/tags', { cache: 'default' });
if (response.ok) {
this.tags = yield response.json();
this.localCache.set('fleetbase-github-tags', this.tags);

View File

@@ -1,42 +1,61 @@
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
<div class="mt-2">
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@brand.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} height="56" class="h-10 object-contain mx-auto" />
<div class="mt-2">
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
</div>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
<form {{on "submit" (perform this.onboard)}}>
{{#if this.error}}
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
{{/if}}
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup
@name={{t "onboard.index.confirm-password"}}
@value={{this.password_confirmation}}
@type="password"
@helpText={{t "onboard.index.confirm-password-help-text"}}
@inputClass="input-lg"
/>
<div class="flex items-center justify-end mt-5">
<Button
@buttonType="submit"
@icon="check"
@iconPrefix="fas"
@type="primary"
@size="lg"
@text={{t "onboard.index.continue-button-text"}}
@isLoading={{this.onboard.isRunning}}
@disabled={{not this.filled}}
/>
</div>
</form>
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
<YieldedComponent @context={{ctx}} />
</RegistryYield>
</div>
</div>
<div class="flex px-3 py-2 mb-4 rounded-md shadow-sm bg-blue-200">
<div>
<FaIcon @icon="hand-spock" @size="lg" class="text-blue-900 mr-4" />
</div>
<p class="flex-1 text-sm text-blue-900 dark:text-blue-900">
{{t "onboard.index.welcome-title" htmlSafe=true companyName=(t "app.name")}}
{{t "onboard.index.welcome-text"}}
</p>
</div>
<form {{on "submit" (perform this.onboard)}}>
{{#if this.error}}
<InfoBlock @icon="exclamation-triangle" @text={{this.error}} class="mb-6 px-3 py-2 bg-red-300 text-red-900" @textClass="text-red-900" />
{{/if}}
<InputGroup @name={{t "onboard.index.full-name"}} @value={{this.name}} @helpText={{t "onboard.index.full-name-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.your-email"}} @type="email" @value={{this.email}} @helpText={{t "onboard.index.your-email-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.phone"}} @helpText={{t "onboard.index.phone-help-text"}}>
<PhoneInput @onInput={{fn (mut this.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "onboard.index.organization-name"}} @value={{this.organization_name}} @helpText={{t "onboard.index.organization-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.password"}} @value={{this.password}} @type="password" @helpText={{t "onboard.index.password-help-text"}} @inputClass="input-lg" />
<InputGroup @name={{t "onboard.index.confirm-password"}} @value={{this.password_confirmation}} @type="password" @helpText={{t "onboard.index.confirm-password-help-text"}} @inputClass="input-lg" />
<div class="flex items-center justify-end mt-5">
<Button @buttonType="submit" @icon="check" @iconPrefix="fas" @type="primary" @size="lg" @text={{t "onboard.index.continue-button-text"}} @isLoading={{this.onboard.isRunning}} @disabled={{not this.filled}} />
</div>
</form>
<RegistryYield @registry="onboard" as |YieldedComponent ctx|>
<YieldedComponent @context={{ctx}} />
</RegistryYield>
</div>

View File

@@ -1,78 +1,82 @@
{{page-title (t "onboard.verify-email.header-title")}}
{{#if this.initialized}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.verify-email.title"}}
</h2>
</div>
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
{{#if this.initialized}}
<div class="bg-white dark:bg-gray-800 py-8 px-4 shadow rounded-lg w-full">
<div class="mb-6">
<LinkTo @route="console" class="flex items-center justify-center">
<LogoIcon @size="12" class="rounded-md" />
</LinkTo>
<h2 class="mt-6 text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.verify-email.title"}}
</h2>
</div>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</InfoBlock>
<InfoBlock @type="info" @icon="shield-halved" @iconSize="lg">
{{t "onboard.verify-email.message-text" htmlSafe=true}}
</InfoBlock>
<form class="mt-8 space-y-6" {{on "submit" (perform this.verify)}}>
<InputGroup
@type="tel"
@name={{t "onboard.verify-email.verification-input-label"}}
@value={{this.code}}
@helpText={{t "onboard.verify-email.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.verification.validateInput}}
{{did-insert this.verification.validateInput}}
/>
<form class="mt-8 space-y-6" {{on "submit" (perform this.verify)}}>
<InputGroup
@type="tel"
@name={{t "onboard.verify-email.verification-input-label"}}
@value={{this.code}}
@helpText={{t "onboard.verify-email.verification-code-text"}}
@inputClass="input-lg"
{{on "input" this.verification.validateInput}}
{{did-insert this.verification.validateInput}}
/>
<div class="flex flex-row items-center space-x-4">
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.verify.isRunning}}
@disabled={{not this.verification.ready}}
/>
<a href="#" {{on "click" this.verification.didntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
</div>
<div class="flex flex-row items-center space-x-4">
<Button
@icon="check"
@iconPrefix="fas"
@buttonType="submit"
@type="primary"
@size="lg"
@text="Verify & Continue"
@isLoading={{this.verify.isRunning}}
@disabled={{not this.verification.ready}}
/>
<a href="#" {{on "click" this.verification.didntReceiveCode}} class="text-sm text-blue-400 hover:text-blue-300">{{t "onboard.verify-email.didnt-receive-a-code"}}</a>
</div>
{{#if this.verification.waiting}}
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
{{#if this.verification.waiting}}
<div class="flex flex-col flex-grow-0 flex-shrink-0 text-sm bg-yellow-800 border border-yellow-600 px-2 py-2 rounded-md text-yellow-100 my-4 transition-all">
<div class="flex flex-row items-start mb-2">
<div class="w-8 flex-grow-0 flex-shrink-0">
<FaIcon @icon="triangle-exclamation" @size="xl" class="pt-1" />
</div>
<div class="flex-1">
<div class="flex-1 text-sm text-yellow-100">
<div>{{t "auth.verification.didnt-receive-a-code" htmlSafe=true}}</div>
<div>{{t "auth.verification.not-sent.alternative-choice" htmlSafe=true}}</div>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendBySms}}
/>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<Button
@text={{t "auth.verification.not-sent.resend-email"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendEmail}}
/>
<Button
@text={{t "auth.verification.not-sent.send-by-sms"}}
@buttonType="button"
@type="link"
class="text-yellow-100"
@wrapperClass="px-4 py-2 bg-gray-900 bg-opacity-25 hover:opacity-50"
@onClick={{this.verification.resendBySms}}
/>
</div>
</div>
{{/if}}
</form>
{{/if}}
</form>
</div>
{{/if}}
</div>
{{/if}}
</div>

View File

@@ -1,7 +1,9 @@
<section class="onboarding step-host">
{{#if this.initialized}}
{{#if this.currentComponent}}
{{component this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{#if this.orchestrator.wrapper}}
{{component (lazy-engine-component this.orchestrator.wrapper) currentStepComponent=this.currentComponent context=this.context orchestrator=this.orchestrator brand=@brand}}
{{else if this.currentComponent}}
{{component (lazy-engine-component this.currentComponent) context=this.context orchestrator=this.orchestrator brand=@brand}}
{{/if}}
{{else}}
<div class="flex items-center justify-center min-h-24">

View File

@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ConsoleAccountController extends Controller {
/**
* Inject the `universe` service.
*
* @memberof ConsoleAdminController
*/
@service('universe/menu-service') menuService;
@service universe;
}

View File

@@ -68,6 +68,7 @@ export default class ConsoleAccountIndexController extends Controller {
subject_uuid: this.user.id,
subject_type: 'user',
type: 'user_avatar',
resize: 'md'
},
(uploadedFile) => {
this.user.setProperties({

View File

@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ConsoleAdminController extends Controller {
/**
* Inject the `universe` service.
*
* @memberof ConsoleAdminController
*/
@service('universe/menu-service') menuService;
@service universe;
}

View File

@@ -2,10 +2,6 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ConsoleSettingsController extends Controller {
/**
* INject the `universe` service
*
* @memberof ConsoleSettingsController
*/
@service('universe/menu-service') menuService;
@service universe;
}

View File

@@ -6,35 +6,13 @@ import createNotificationKey from '@fleetbase/ember-core/utils/create-notificati
import { task } from 'ember-concurrency';
export default class ConsoleSettingsNotificationsController extends Controller {
/**
* Inject the notifications service.
*
* @memberof ConsoleSettingsNotificationsController
*/
@service notifications;
/**
* Inject the fetch service.
*
* @memberof ConsoleSettingsNotificationsController
*/
@service fetch;
/**
* The notification settings value JSON.
*
* @memberof ConsoleSettingsNotificationsController
* @var {Object}
*/
@service store;
@service currentUser;
@tracked notificationSettings = {};
/**
* Notification transport methods enabled.
*
* @memberof ConsoleSettingsNotificationsController
* @var {Array}
*/
@tracked notificationTransportMethods = ['email', 'sms'];
@tracked company;
/**
* Creates an instance of ConsoleSettingsNotificationsController.
@@ -45,6 +23,40 @@ export default class ConsoleSettingsNotificationsController extends Controller {
this.getSettings.perform();
}
/**
* Toggles the "Alphanumeric Sender ID" feature for the current company.
*
* Updates the company's `options` object by setting the
* `alpha_numeric_sender_id_enabled` flag. This controls whether the
* organization uses a custom alphanumeric sender ID when sending SMS.
*
* @action
* @param {boolean} enabled - Whether the feature should be enabled or disabled.
* @returns {void}
*/
@action toggleAlphaNumericSenderId(enabled) {
const currentOptions = this.company.options ?? {};
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id_enabled: enabled });
}
/**
* Sets the Alphanumeric Sender ID string for the current company.
*
* Reads the input's value from the event and updates the company's `options`
* object by setting the `alpha_numeric_sender_id` field. This value represents
* the sender name that will appear in outbound SMS messages (subject to carrier
* support and restrictions).
*
* @action
* @param {Event} event - Input event containing the alphanumeric sender ID value.
* @returns {void}
*/
@action setAlphaNumericSenderId(event) {
const value = event.target.value;
const currentOptions = this.company.options ?? {};
this.company.set('options', { ...currentOptions, alpha_numeric_sender_id: value });
}
/**
* Selectes notifiables for settings.
*
@@ -94,7 +106,8 @@ export default class ConsoleSettingsNotificationsController extends Controller {
const { notificationSettings } = this;
try {
yield this.fetch.post('notifications/save-settings', { notificationSettings });
yield this.fetch.post('notifications/save-settings', { notificationSettings: notificationSettings ?? {} });
yield this.saveCompanyOptions.perform();
this.notifications.success('Notification settings successfully saved.');
} catch (error) {
this.notifications.serverError(error);
@@ -114,4 +127,26 @@ export default class ConsoleSettingsNotificationsController extends Controller {
this.notifications.serverError(error);
}
}
/**
* Saves the updated company options to the backend.
*
* This ember-concurrency task attempts to persist the company's modified
* `options` object by calling `company.save()`. If the request fails, a server
* error notification is displayed. No action is taken if no company is loaded.
*
* @task
* @generator
* @yields {Promise} Resolves when the save request completes.
* @returns {Promise<void>} Task completion state.
*/
@task *saveCompanyOptions() {
if (!this.company) return;
try {
yield this.company.save();
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -0,0 +1,7 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
export default class VirtualController extends Controller {
@tracked view;
queryParams = ['view'];
}

View File

@@ -0,0 +1,9 @@
import setupDeprecationWorkflow from 'ember-cli-deprecation-workflow';
setupDeprecationWorkflow({
workflow: [
{ handler: 'silence', matchId: 'ember-concurrency.deprecate-decorator-task' },
{ handler: 'silence', matchId: 'new-helper-names' },
{ handler: 'silence', matchId: 'ember-data:deprecate-non-strict-relationships' },
],
});

View File

@@ -10,9 +10,11 @@
{{content-for "head"}}
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="256x256" href="/favicon/android-chrome-256x256.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">

View File

@@ -38,4 +38,7 @@ export function initialize(application) {
})();
}
export default { initialize };
export default {
name: 'load-intl-polyfills',
initialize,
};

View File

@@ -0,0 +1,41 @@
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
import { debug } from '@ember/debug';
/**
* Load Runtime Config Initializer
*
* Loads runtime configuration from fleetbase.config.json before the application boots.
* This must run first to ensure all config is available for other initializers.
*
* Uses `before` to ensure it runs before any other initializers.
*
* @export
* @param {Application} application
*/
export function initialize(application) {
const startTime = performance.now();
debug('[Runtime Config] Loading runtime configuration...');
// Defer readiness until config is loaded
application.deferReadiness();
(async () => {
try {
await loadRuntimeConfig();
const endTime = performance.now();
debug(`[Runtime Config] Runtime config loaded in ${(endTime - startTime).toFixed(2)}ms`);
application.advanceReadiness();
} catch (error) {
console.error('[Runtime Config] Failed to load runtime config:', error);
// Still advance readiness to prevent hanging
application.advanceReadiness();
}
})();
}
export default {
name: 'load-runtime-config',
initialize,
// Run after intl polyfills are loaded, before socketcluster
after: 'load-intl-polyfills',
before: 'load-socketcluster-client',
};

View File

@@ -0,0 +1,38 @@
import applyRouterFix from '@fleetbase/console/utils/router-refresh-patch';
import { debug } from '@ember/debug';
/**
* Apply Router Fix Instance Initializer
*
* Applies the Fleetbase router refresh bug fix patch.
* This patches the Ember router to handle dynamic segments correctly
* when refreshing routes with query parameters.
*
* Runs as an instance-initializer because it needs access to the
* application instance and router service.
*
* Bug: https://github.com/emberjs/ember.js/issues/19260
*
* @export
* @param {ApplicationInstance} appInstance
*/
export function initialize(appInstance) {
const startTime = performance.now();
debug('[Initializing Router Patch] Applying router refresh bug fix...');
try {
applyRouterFix(appInstance);
const endTime = performance.now();
debug(`[Initializing Router Patch] Router fix applied in ${(endTime - startTime).toFixed(2)}ms`);
} catch (error) {
console.error('[Initializing Router Patch] Failed to apply router fix:', error);
}
}
export default {
name: 'apply-router-fix',
initialize,
// Run before extension loading to ensure router is patched early
before: 'load-extensions',
};

View File

@@ -0,0 +1,20 @@
import { debug } from '@ember/debug';
/**
* Create console-specific registries
* Runs after extensions are loaded
*/
export function initialize(appInstance) {
const registryService = appInstance.lookup('service:universe/registry-service');
debug('[Initializing Registries] Creating console registries...');
// Create console-specific registries
registryService.createRegistries(['@fleetbase/console', 'auth:login']);
}
export default {
name: 'initialize-registries',
after: 'load-extensions',
initialize,
};

View File

@@ -1,36 +1,47 @@
import { Widget } from '@fleetbase/ember-core/contracts';
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { debug } from '@ember/debug';
export function initialize(application) {
const universe = application.lookup('service:universe');
const defaultWidgets = [
{
widgetId: 'fleetbase-blog',
/**
* Register dashboard and widgets for FleetbaseConsole
* Runs after extensions are loaded
*/
export function initialize(appInstance) {
const widgetService = appInstance.lookup('service:universe/widget-service');
debug('[Initializing Widgets] Registering console dashboard and widgets...');
// Register the console dashboard
widgetService.registerDashboard('dashboard');
// Create widget definitions
const widgets = [
new Widget({
id: 'fleetbase-blog',
name: 'Fleetbase Blog',
description: 'Lists latest news and events from the Fleetbase official team.',
icon: 'newspaper',
component: 'fleetbase-blog',
grid_options: { w: 8, h: 9, minW: 8, minH: 9 },
options: {
title: 'Fleetbase Blog',
},
},
{
widgetId: 'fleetbase-github-card',
default: true,
}),
new Widget({
id: 'fleetbase-github-card',
name: 'Github Card',
description: 'Displays current Github stats from the official Fleetbase repo.',
icon: faGithub,
component: 'github-card',
grid_options: { w: 4, h: 8, minW: 4, minH: 8 },
options: {
title: 'Github Card',
},
},
default: true,
}),
];
universe.registerDefaultDashboardWidgets(defaultWidgets);
universe.registerDashboardWidgets(defaultWidgets);
// Register widgets
widgetService.registerWidgets('dashboard', widgets);
}
export default {
name: 'initialize-widgets',
after: 'load-extensions',
initialize,
};

View File

@@ -1,11 +1,19 @@
export function initialize(application) {
const universe = application.lookup('service:universe');
if (universe) {
universe.createRegistries(['@fleetbase/console', 'auth:login']);
universe.bootEngines(application);
/**
* Load extensions from the API using ExtensionManager
* This must run before other initializers that depend on extensions
*/
export async function initialize(appInstance) {
const application = appInstance.application;
const extensionManager = appInstance.lookup('service:universe/extension-manager');
try {
await extensionManager.loadExtensions(application);
} catch (error) {
console.error('[load-extensions] Error:', error);
}
}
export default {
name: 'load-extensions',
initialize,
};

View File

@@ -0,0 +1,17 @@
export function initialize(appInstance) {
// Set window.Fleetbase to the application instance for global access
// This is used by services and engines to access the root application instance
if (typeof window !== 'undefined') {
window.Fleetbase = appInstance;
}
// Look up UniverseService and set the application instance
const universeService = appInstance.lookup('service:universe');
if (universeService) {
universeService.setApplicationInstance(appInstance);
}
}
export default {
initialize
};

View File

@@ -0,0 +1,16 @@
/**
* Setup extensions by loading and executing their extension.js files
* Runs after extensions are loaded from API
*/
export async function initialize(appInstance) {
const universe = appInstance.lookup('service:universe');
const extensionManager = appInstance.lookup('service:universe/extension-manager');
await extensionManager.setupExtensions(appInstance, universe);
}
export default {
name: 'setup-extensions',
after: ['load-extensions', 'initialize-registries', 'initialize-widgets'],
initialize,
};

View File

@@ -24,7 +24,7 @@ export default class Company extends Model {
@attr('string') logo_url;
@attr('string') backdrop_url;
@attr('string') description;
@attr('raw') options;
@attr('object') options;
@attr('number') users_count;
@attr('string') type;
@attr('string') currency;

View File

@@ -0,0 +1,18 @@
import Model, { attr } from '@ember-data/model';
export default class ScheduleAvailabilityModel extends Model {
@attr('string') subject_uuid;
@attr('string') subject_type;
@attr('date') start_at;
@attr('date') end_at;
@attr('boolean', { defaultValue: true }) is_available;
@attr('number') preference_level;
@attr('string') rrule;
@attr('string') reason;
@attr('string') notes;
@attr('object') meta;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,23 @@
import Model, { attr, belongsTo } from '@ember-data/model';
export default class ScheduleConstraintModel extends Model {
@attr('string') company_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
@attr('string') name;
@attr('string') description;
@attr('string') type;
@attr('string') category;
@attr('string') constraint_key;
@attr('string') constraint_value;
@attr('string') jurisdiction;
@attr('number', { defaultValue: 0 }) priority;
@attr('boolean', { defaultValue: true }) is_active;
@attr('object') meta;
@belongsTo('company') company;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,23 @@
import Model, { attr, belongsTo } from '@ember-data/model';
export default class ScheduleItemModel extends Model {
@attr('string') public_id;
@attr('string') schedule_uuid;
@attr('string') assignee_uuid;
@attr('string') assignee_type;
@attr('string') resource_uuid;
@attr('string') resource_type;
@attr('date') start_at;
@attr('date') end_at;
@attr('number') duration;
@attr('date') break_start_at;
@attr('date') break_end_at;
@attr('string', { defaultValue: 'pending' }) status;
@attr('object') meta;
@belongsTo('schedule') schedule;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,22 @@
import Model, { attr, belongsTo } from '@ember-data/model';
export default class ScheduleTemplateModel extends Model {
@attr('string') public_id;
@attr('string') company_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
@attr('string') name;
@attr('string') description;
@attr('string') start_time;
@attr('string') end_time;
@attr('number') duration;
@attr('number') break_duration;
@attr('string') rrule;
@attr('object') meta;
@belongsTo('company') company;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -0,0 +1,27 @@
import Model, { attr, hasMany, belongsTo } from '@ember-data/model';
export default class ScheduleModel extends Model {
/** @ids */
@attr('string') public_id;
@attr('string') company_uuid;
@attr('string') subject_uuid;
@attr('string') subject_type;
/** @attributes */
@attr('string') name;
@attr('string') description;
@attr('date') start_date;
@attr('date') end_date;
@attr('string') timezone;
@attr('string', { defaultValue: 'draft' }) status;
@attr('object') meta;
/** @relationships */
@hasMany('schedule-item') items;
@belongsTo('company') company;
/** @dates */
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
}

View File

@@ -7,6 +7,8 @@ import pathToRoute from '@fleetbase/ember-core/utils/path-to-route';
import removeBootLoader from '../utils/remove-boot-loader';
export default class ApplicationRoute extends Route {
@service('universe/hook-service') hookService;
@service('universe/extension-manager') extensionManager;
@service session;
@service theme;
@service fetch;
@@ -15,7 +17,6 @@ export default class ApplicationRoute extends Route {
@service intl;
@service currentUser;
@service router;
@service universe;
@tracked defaultTheme;
/**
@@ -24,7 +25,7 @@ export default class ApplicationRoute extends Route {
* @memberof ApplicationRoute
*/
@action willTransition(transition) {
this.universe.callHooks('application:will-transition', this.session, this.router, transition);
this.hookService.execute('application:will-transition', this.session, this.router, transition);
}
/**
@@ -45,7 +46,7 @@ export default class ApplicationRoute extends Route {
* @memberof ApplicationRoute
*/
@action loading(transition) {
this.universe.callHooks('application:loading', this.session, this.router, transition);
this.hookService.execute('application:loading', this.session, this.router, transition);
}
/**
@@ -79,9 +80,9 @@ export default class ApplicationRoute extends Route {
*/
async beforeModel(transition) {
await this.session.setup();
await this.universe.booting();
await this.extensionManager.waitForBoot();
this.universe.callHooks('application:before-model', this.session, this.router, transition);
this.hookService.execute('application:before-model', this.session, this.router, transition);
const shift = this.urlSearchParams.get('shift');
if (this.session.isAuthenticated && shift) {
@@ -95,9 +96,7 @@ export default class ApplicationRoute extends Route {
* @memberof ApplicationRoute
*/
afterModel() {
if (!this.session.isAuthenticated) {
removeBootLoader();
}
if (!this.session.isAuthenticated) removeBootLoader();
}
/**
@@ -122,11 +121,11 @@ export default class ApplicationRoute extends Route {
* Initializes the application's locale settings based on the current user's preferences.
*
* This method retrieves the user's preferred locale using the `getOption` method from the `currentUser` service.
* If no locale is set by the user, it defaults to `'en-us'`. It then sets the application's locale by calling
* If no locale is set by the user, it defaults to `'en-US'`. It then sets the application's locale by calling
* the `setLocale` method of the `intl` service with the retrieved locale.
*/
initializeLocale() {
const locale = this.currentUser.getOption('locale', 'en-us');
const locale = this.currentUser.getOption('locale', 'en-US');
this.intl.setLocale([locale]);
}

View File

@@ -5,9 +5,9 @@ import removeBootLoader from '../utils/remove-boot-loader';
import '@fleetbase/leaflet-routing-machine';
export default class ConsoleRoute extends Route {
@service('universe/hook-service') hookService;
@service store;
@service session;
@service universe;
@service router;
@service currentUser;
@service intl;
@@ -22,7 +22,7 @@ export default class ConsoleRoute extends Route {
async beforeModel(transition) {
await this.session.requireAuthentication(transition, 'auth.login');
this.universe.callHooks('console:before-model', this.session, this.router, transition);
this.hookService.execute('console:before-model', this.session, this.router, transition);
if (this.session.isAuthenticated) {
return this.session.promiseCurrentUser(transition);
@@ -37,7 +37,7 @@ export default class ConsoleRoute extends Route {
* @memberof ConsoleRoute
*/
async afterModel(model, transition) {
this.universe.callHooks('console:after-model', this.session, this.router, model, transition);
this.hookService.execute('console:after-model', this.session, this.router, model, transition);
removeBootLoader();
}
@@ -47,7 +47,7 @@ export default class ConsoleRoute extends Route {
* @memberof ConsoleRoute
*/
@action didTransition() {
this.universe.callHooks('console:did-transition', this.session, this.router);
this.hookService.execute('console:did-transition', this.session, this.router);
}
/**

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleAccountVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleAccountVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:account', slug, view);
return this.menuService.lookupMenuItem('console:account', slug, view);
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleAdminVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleAdminVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:admin', slug, view);
return this.menuService.lookupMenuItem('console:admin', slug, view);
}
}

View File

@@ -5,6 +5,7 @@ import groupBy from '@fleetbase/ember-core/utils/group-by';
export default class ConsoleSettingsNotificationsRoute extends Route {
@service fetch;
@service currentUser;
model() {
return hash({
@@ -13,10 +14,11 @@ export default class ConsoleSettingsNotificationsRoute extends Route {
});
}
setupController(controller, { registry, notifiables }) {
async setupController(controller, { registry, notifiables }) {
super.setupController(...arguments);
controller.groupedNotifications = groupBy(registry, 'package');
controller.notifiables = notifiables;
controller.company = await this.currentUser.loadCompany();
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleSettingsVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleSettingsVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console:settings', slug, view);
return this.menuService.lookupMenuItem('console:settings', slug, view);
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ConsoleVirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class ConsoleVirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('console', slug, view);
return this.menuService.lookupMenuItem('console', slug, view);
}
}

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class VirtualRoute extends Route {
@service('universe/menu-service') menuService;
@service universe;
queryParams = {
@@ -12,6 +13,6 @@ export default class VirtualRoute extends Route {
model({ slug }, transition) {
const view = this.universe.getViewFromTransition(transition);
return this.universe.lookupMenuItemFromRegistry('auth:login', slug, view);
return this.menuService.lookupMenuItem('auth:login', slug, view);
}
}

View File

@@ -4,8 +4,6 @@ import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
export default class UserSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
/**
* Embedded relationship attributes
*
* @var {Object}
*/
get attrs() {
return {
@@ -16,22 +14,45 @@ export default class UserSerializer extends ApplicationSerializer.extend(Embedde
}
/**
* Customize serializer so that the password is never sent to the server via Ember Data
* Prevent partial payloads from overwriting fully-loaded
* user records in the store.
*
* @param {Snapshot} snapshot
* @param {Object} options
* @return {Object} json
* This runs ONLY on incoming data.
*/
normalize(modelClass, resourceHash, prop) {
let normalized = super.normalize(modelClass, resourceHash, prop);
// Existing user already loaded in the store?
let existing = this.store.peekRecord(normalized.data.type, normalized.data.id);
if (existing) {
let attrs = normalized.data.attributes || {};
for (let key in attrs) {
if (attrs[key] === null || attrs[key] === undefined || key === 'avatar_url') {
delete attrs[key];
}
}
}
return normalized;
}
/**
* Customize serializer so that sensitive or server-managed
* fields are never sent to the backend.
*/
serialize() {
const json = super.serialize(...arguments);
// delete the password always
// Never send password
delete json.password;
// delete verification attributes
// Verification flags
delete json.email_verified_at;
delete json.phone_verified_at;
// delete server managed dates
// Server-managed timestamps
delete json.deleted_at;
delete json.created_at;
delete json.updated_at;

View File

@@ -7,17 +7,31 @@ export default class OnboardingOrchestratorService extends Service {
@service onboardingContext;
@tracked flow = null;
@tracked wrapper = null;
@tracked current = null;
@tracked history = [];
@tracked sessionId = null;
start(flowId = null, opts = {}) {
async start(flowId = null, opts = {}) {
const flow = this.onboardingRegistry.getFlow(flowId ?? this.onboardingRegistry.defaultFlow);
if (!flow) throw new Error(`Onboarding flow '${flowId}' not found`);
this.flow = flow;
this.wrapper = flow.wrapper || null;
this.sessionId = opts.sessionId || null;
this.history = [];
this.goto(flow.entry);
// Execute onFlowWillStart hook if defined
if (typeof this.flow.onFlowWillStart === 'function') {
await this.flow.onFlowWillStart(this.flow, this);
}
await this.goto(flow.entry);
// Execute onFlowDidStart hook if defined
if (typeof this.flow.onFlowDidStart === 'function') {
await this.flow.onFlowDidStart(this.flow, this);
}
}
async goto(stepId) {
@@ -25,27 +39,43 @@ export default class OnboardingOrchestratorService extends Service {
const step = this.flow.steps.find((s) => s.id === stepId);
if (!step) throw new Error(`Step '${stepId}' not found`);
// Execute onStepWillChange hook if defined
const previousStep = this.current;
if (typeof this.flow.onStepWillChange === 'function') {
await this.flow.onStepWillChange(step, previousStep, this);
}
// Guard function - skip step if guard returns false
if (typeof step.guard === 'function' && !step.guard(this.onboardingContext)) {
return this.next();
}
// beforeEnter lifecycle hook
if (typeof step.beforeEnter === 'function') {
await step.beforeEnter(this.onboardingContext);
}
this.current = step;
// Execute onStepDidChange hook if defined
if (typeof this.flow.onStepDidChange === 'function') {
await this.flow.onStepDidChange(this.current, previousStep, this);
}
}
async next() {
if (!this.flow || !this.current) return;
const leaving = this.current;
// afterLeave lifecycle hook
if (typeof leaving.afterLeave === 'function') {
await leaving.afterLeave(this.onboardingContext);
}
if (!this.history.includes(leaving)) this.history.push(leaving);
// Support both string and function for next property
let nextId;
if (typeof leaving.next === 'function') {
nextId = leaving.next(this.onboardingContext);
@@ -53,8 +83,20 @@ export default class OnboardingOrchestratorService extends Service {
nextId = leaving.next;
}
// If no next step, flow is complete
if (!nextId) {
// Execute onFlowWillEnd hook if defined
if (typeof this.flow.onFlowWillEnd === 'function') {
await this.flow.onFlowWillEnd(leaving, this);
}
this.current = null; // finished
// Execute onFlowDidEnd hook if defined
if (typeof this.flow.onFlowDidEnd === 'function') {
await this.flow.onFlowDidEnd(leaving, this);
}
return;
}
@@ -68,4 +110,31 @@ export default class OnboardingOrchestratorService extends Service {
this.history = this.history.slice(0, -1);
await this.goto(prev.id);
}
/**
* Get the current path (for flows with multiple paths)
* This is a helper method that can be used by flows to determine the current path
*/
getCurrentPath() {
if (!this.flow || !this.flow.paths) return null;
// Determine path based on context or current step
for (const [pathId, pathDef] of Object.entries(this.flow.paths)) {
if (pathDef.steps && pathDef.steps.some(s => s.id === this.current?.id)) {
return pathDef;
}
}
return null;
}
/**
* Check if a step is in the current path
*/
isStepInPath(stepId) {
const currentPath = this.getCurrentPath();
if (!currentPath) return true; // If no paths defined, all steps are valid
return currentPath.steps?.some(s => s.id === stepId) ?? false;
}
}

View File

@@ -9,7 +9,7 @@ export default class OnboardingRegistryService extends Service {
this.defaultFlow = flowId;
}
registerFlow(flow) {
registerFlow(flow, options = {}) {
if (!flow || !flow.id || !flow.entry || !Array.isArray(flow.steps)) {
throw new Error('Invalid FlowDef: id, entry, steps are required');
}
@@ -23,6 +23,11 @@ export default class OnboardingRegistryService extends Service {
}
}
this.flows.set(flow.id, flow);
// If specified, set as default flow
if (options.default) {
this.defaultFlow = flow.id;
}
}
getFlow(id) {

View File

@@ -107,8 +107,12 @@ export default class UserVerificationService extends Service {
}
#wait(timeout = 75000) {
return later(this, () => {
this.waiting = true;
}, timeout);
return later(
this,
() => {
this.waiting = true;
},
timeout
);
}
}

View File

@@ -4,11 +4,11 @@
<Layout::Sidebar::Item @route="console.account.index" @icon="user">Profile</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.account.auth" @icon="key">Auth</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.account.organizations" @icon="building">Organizations</Layout::Sidebar::Item>
{{#each this.universe.accountMenuItems as |menuItem|}}
{{#each this.menuService.accountMenuItems as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
</Layout::Sidebar::Panel>
{{#each this.universe.accountMenuPanels as |menuPanel|}}
{{#each this.menuService.accountMenuPanels as |menuPanel|}}
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
{{#each menuPanel.items as |menuItem|}}
<Layout::Sidebar::Item @onClick={{fn this.universe.transitionMenuItem "console.account.virtual" menuItem}} @item={{menuItem}} @icon={{menuItem.icon}}>{{menuItem.title}}</Layout::Sidebar::Item>

View File

@@ -4,21 +4,27 @@
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto">
<ContentPanel @title={{t "common.your-profile"}} @open={{true}} @wrapperClass="bordered-classic">
<form class="flex flex-col md:flex-row" {{on "submit" (perform this.saveProfile)}}>
<form class="flex flex-col items-start md:flex-row" {{on "submit" (perform this.saveProfile)}}>
<div class="w-32 flex flex-col justify-center mb-6 mr-6">
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md" />
<FileUpload @name={{t "console.account.index.photos"}} @accept="image/*" @onFileAdded={{this.uploadNewPhoto}} @labelClass="flex flex-row items-center justify-center" as |queue|>
<Image src={{this.user.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.user.name}} class="w-32 h-32 rounded-md mt-1" />
<FileUpload
@name={{t "console.account.index.photos"}}
@accept="image/*"
@onFileAdded={{this.uploadNewPhoto}}
@labelClass="flex flex-row items-center justify-center"
as |queue|
>
<a tabindex={{0}} class="flex items-center px-0 mt-2 text-xs no-underline truncate btn btn-sm btn-default" disabled={{queue.files.length}}>
{{#if queue.files.length}}
<div class="mr-1.5">
<Spinner />
</div>
<span>
{{t "common.uploading"}}
{{t "common.uploading"}}
</span>
{{else}}
<FaIcon @icon="image" class="mr-1.5" />
<span>
<span>
{{t "console.account.index.upload-new"}}
</span>
{{/if}}
@@ -34,11 +40,31 @@
</InputGroup>
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.account.index.timezone"}}>
<Select @value={{this.user.timezone}} @options={{this.timezones}} @onSelect={{fn (mut this.user.timezone)}} @placeholder={{t "console.account.index.timezone"}} />
<div class="fleetbase-model-select fleetbase-power-select ember-model-select">
<PowerSelect
@options={{this.timezones}}
@selected={{this.user.timezone}}
@onChange={{fn (mut this.user.timezone)}}
@placeholder={{t "console.account.index.timezone"}}
@triggerClass="form-select form-input"
@searchEnabled={{true}}
as |option|
>
<div>{{option}}</div>
</PowerSelect>
</div>
</InputGroup>
</div>
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-changes"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />
<Button
@buttonType="submit"
@type="primary"
@size="lg"
@icon="save"
@text={{t "common.save-changes"}}
@onClick={{perform this.saveProfile}}
@isLoading={{not this.saveProfile.isIdle}}
/>
</div>
</div>
</form>

View File

@@ -4,7 +4,7 @@
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
</div>
</div>
<Spacer @height="300px" />

View File

@@ -6,14 +6,16 @@
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "console.admin.menu.branding"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "console.admin.menu.2fa-config"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.schedule-monitor" @icon="calendar-check">{{t "console.admin.schedule-monitor.schedule-monitor"}}</Layout::Sidebar::Item>
{{#each this.universe.adminMenuItems as |menuItem|}}
{{#each this.menuService.adminMenuItems as |menuItem|}}
<Layout::Sidebar::Item
@onClick={{fn this.universe.transitionMenuItem "console.admin.virtual" menuItem}}
@item={{menuItem}}
@icon={{menuItem.icon}}
>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
{{#each this.universe.adminMenuPanels as |menuPanel|}}
{{#each this.menuService.adminMenuPanels as |menuPanel|}}
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
{{#each menuPanel.items as |menuItem|}}
<Layout::Sidebar::Item

View File

@@ -4,7 +4,7 @@
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
</div>
</div>
<Spacer @height="300px" />

View File

@@ -4,7 +4,7 @@
<Layout::Sidebar::Item @route="console.settings.index" @icon="cog">{{t "common.organization"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.settings.two-fa" @icon="shield-halved">{{t "common.two-factor"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.settings.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
{{#each this.universe.settingsMenuItems as |menuItem|}}
{{#each this.menuService.settingsMenuItems as |menuItem|}}
<Layout::Sidebar::Item
@onClick={{fn this.universe.transitionMenuItem "console.settings.virtual" menuItem}}
@item={{menuItem}}
@@ -12,7 +12,7 @@
>{{menuItem.title}}</Layout::Sidebar::Item>
{{/each}}
</Layout::Sidebar::Panel>
{{#each this.universe.settingsMenuPanels as |menuPanel|}}
{{#each this.menuService.settingsMenuPanels as |menuPanel|}}
<Layout::Sidebar::Panel @open={{menuPanel.open}} @title={{menuPanel.title}}>
{{#each menuPanel.items as |menuItem|}}
<Layout::Sidebar::Item

View File

@@ -17,7 +17,7 @@
@selected={{get this.notificationSettings (concat (get-notification-key notification.definition notification.name) ".notifiables")}}
@onChange={{fn this.onSelectNotifiable notification}}
@placeholder="Select notifiables..."
@triggerClass="form-select form-input form-input-sm flex-1"
@triggerClass="form-select form-input flex-1"
as |notifiable|
>
{{notifiable.label}}
@@ -27,6 +27,21 @@
{{/each}}
</ContentPanel>
{{/each-in}}
<ContentPanel @title="SMS Notification Settings" @open={{true}} @wrapperClass="bordered-classic">
<Toggle @isToggled={{this.company.options.alpha_numeric_sender_id_enabled}} @onToggle={{this.toggleAlphaNumericSenderId}} @label="Enable Alpha-Numeric Sender ID" @wrapperClass="mb-4" />
<InputGroup @name="Alpha-Numeric Sender ID" @value={{this.company.options.alpha_numeric_sender_id}} @helpText="Set the custom alphanumeric name that will appear as the sender for all SMS sent by your organization. Up to 11 letters or numbers. Not supported in all countries." @disabled={{not this.company.options.alpha_numeric_sender_id_enabled}} />
<div class="space-y-2 mb-3">
<InfoBlock>
<p>Alphanumeric Sender IDs allow your organization to replace a traditional phone number with a custom text-based sender name when sending SMS notifications (e.g., Fleetbase, MyStore, DispatchHQ). This can improve brand recognition, increase message trust, and enhance deliverability in regions where numeric senders are restricted by local carriers.</p>
<p>When enabled, Fleetbase will use this sender ID for all outbound SMS messages sent on behalf of your organization, including order updates, verification codes, driver notifications, and other automated alerts. Sender IDs can contain up to 11 characters using letters and numbers.</p>
<p>Some countries require or enforce specific messaging rules, and certain carriers may only support alphanumeric senders. Using a Sender ID can significantly improve message delivery in these regions.</p>
</InfoBlock>
<InfoBlock @type="warning">
<p>Delivery of SMS using Alphanumeric Sender IDs depends on local carrier policies. Some regions may restrict or block numeric senders or require the use of alphanumeric senders for successful delivery. While Fleetbase will attempt to deliver all messages using your configured Sender ID, message delivery cannot be guaranteed in countries with carrier-level filtering or regulatory restrictions. Your organization is responsible for ensuring compliance with local messaging regulations in the countries you send SMS to.</p>
</InfoBlock>
</div>
</ContentPanel>
</div>
</div>
<Spacer @height="300px" />

View File

@@ -4,7 +4,7 @@
<Layout::Section::Body class="overflow-y-scroll h-full">
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
</div>
</div>
<Spacer @height="300px" />

View File

@@ -1,6 +1,6 @@
{{page-title @model.title}}
<Layout::Section::Body class="overflow-y-scroll h-full">
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />
<Spacer @height="300px" />
</Layout::Section::Body>

View File

@@ -1,6 +1,3 @@
<div class="flex items-center justify-center h-screen min-h-screen px-4 py-12 bg-gray-50 dark:bg-gray-900 sm:px-6 lg:px-8 overflow-y-scroll">
<div class="w-full max-w-md h-screen flex items-center justify-center py-4">
{{outlet}}
</div>
<Spacer @height="300px" />
<div class="onboard-route-wrapper">
{{outlet}}
</div>

View File

@@ -1,2 +1,2 @@
{{page-title @model.title}}
{{component @model.component params=@model.componentParams}}
<LazyEngineComponent @component={{@model.component}} @params={{@model.componentParams}} />

View File

@@ -107,8 +107,8 @@ export function suppressRouterRefreshErrors(application) {
// Global error handler for unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
const error = event.reason;
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
if (typeof error?.message === 'string' && error?.message.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug: ' + error.message);
event.preventDefault(); // Prevent the error from being logged
}
});
@@ -118,8 +118,8 @@ export function suppressRouterRefreshErrors(application) {
const originalEmberError = window.Ember.onerror;
window.Ember.onerror = function (error) {
if (error?.message?.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug:', error.message);
if (typeof error?.message === 'string' && error?.message.includes("You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments")) {
debug('[Fleetbase Router Patch] Suppressed known Ember route refresh bug: ' + error.message);
return; // Suppress the error
}

View File

@@ -17,6 +17,13 @@ const RUNTIME_CONFIG_MAP = {
EXTENSIONS: 'APP.extensions',
};
/**
* Cache key for localStorage
*/
const CACHE_KEY = 'fleetbase_runtime_config';
const CACHE_VERSION_KEY = 'fleetbase_runtime_config_version';
const CACHE_TTL = 1000 * 60 * 60; // 1 hour
/**
* Coerce and sanitize runtime config values based on key.
*
@@ -53,32 +60,130 @@ export function applyRuntimeConfig(rawConfig = {}) {
const coercedValue = coerceValue(key, value);
set(config, configPath, coercedValue);
} else {
debug(`[runtime-config] Ignored unknown key: ${key}`);
debug(`[Runtime Config] Ignored unknown key: ${key}`);
}
});
}
/**
* Load and apply runtime config.
* Get cached config from localStorage
*
* @returns {Object|null} Cached config or null
*/
function getCachedConfig() {
try {
const cached = localStorage.getItem(CACHE_KEY);
const cachedVersion = localStorage.getItem(CACHE_VERSION_KEY);
if (!cached || !cachedVersion) {
return null;
}
// Application version has changed
if (cachedVersion !== config.APP.version) {
debug(`[Runtime Config] Version mismatch (cached: ${cachedVersion}, current: ${config.APP.version})`);
return null;
}
const cacheData = JSON.parse(cached);
const cacheAge = Date.now() - cacheData.timestamp;
// Check if cache is still valid (within TTL)
if (cacheAge > CACHE_TTL) {
debug('[Runtime Config] Cache expired');
return null;
}
debug(`[Runtime Config] Using cached config (age: ${Math.round(cacheAge / 1000)}s)`);
return cacheData.config;
} catch (e) {
debug(`[Runtime Config] Failed to read cache: ${e.message}`);
return null;
}
}
/**
* Save config to localStorage cache
*
* @param {Object} config Config object
*/
function setCachedConfig(runtimeConfig) {
try {
const cacheData = {
config: runtimeConfig,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
localStorage.setItem(CACHE_VERSION_KEY, config.APP.version);
debug('[Runtime Config] Config cached to localStorage');
} catch (e) {
debug(`[Runtime Config] Failed to cache config: ${e.message}`);
}
}
/**
* Clear cached config
*
* @export
* @return {void}
*/
export function clearRuntimeConfigCache() {
try {
localStorage.removeItem(CACHE_KEY);
localStorage.removeItem(CACHE_VERSION_KEY);
debug('[Runtime Config] Cache cleared');
} catch (e) {
debug(`[Runtime Config] Failed to clear cache: ${e.message}`);
}
}
/**
* Load and apply runtime config with localStorage caching.
*
* Strategy:
* 1. Check localStorage cache first (instant, no HTTP request)
* 2. If cache hit and valid, use it immediately
* 3. If cache miss, fetch from server and cache the result
* 4. Cache is valid for 1 hour
*
* @export
* @return {Promise<void>}
*/
export default async function loadRuntimeConfig() {
if (config.APP.disableRuntimeConfig) {
return;
}
const isProduction = config?.environment === 'production';
if (isProduction) {
// Try cache first
const cachedConfig = getCachedConfig();
if (cachedConfig) {
applyRuntimeConfig(cachedConfig);
return;
}
}
// Cache miss - fetch from server
try {
const response = await fetch(`/fleetbase.config.json?_t=${Date.now()}`, { cache: 'no-cache' });
const startTime = performance.now();
const response = await fetch('/fleetbase.config.json', {
cache: 'default', // Use browser cache if available
});
if (!response.ok) {
debug('No fleetbase.config.json found, using built-in config defaults');
debug('[Runtime Config] No fleetbase.config.json found, using built-in config defaults');
return;
}
const runtimeConfig = await response.json();
const endTime = performance.now();
debug(`[Runtime Config] Fetched from server in ${(endTime - startTime).toFixed(2)}ms`);
// Apply and cache
applyRuntimeConfig(runtimeConfig);
setCachedConfig(runtimeConfig);
} catch (e) {
debug(`Failed to load runtime config : ${e.message}`);
debug(`[Runtime Config] Failed to load runtime config: ${e.message}`);
}
}

View File

@@ -14,7 +14,7 @@ module.exports = function (/* environment */) {
* @type {String?}
* @default "null"
*/
fallbackLocale: 'en-us',
fallbackLocale: 'en-US',
/**
* Path where translations are stored. This is relative to the project root.

View File

@@ -21,9 +21,9 @@ module.exports = function (environment) {
},
APP: {
autoboot: false,
autoboot: true,
extensions: asArray(getenv('EXTENSIONS')),
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG')),
disableRuntimeConfig: toBoolean(getenv('DISABLE_RUNTIME_CONFIG', environment === 'production')),
},
API: {

View File

@@ -2,26 +2,17 @@
/** eslint-disable node/no-unpublished-require */
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const FleetbaseExtensionsIndexer = require('fleetbase-extensions-indexer');
const Funnel = require('broccoli-funnel');
const writeFile = require('broccoli-file-creator');
const postcssImport = require('postcss-import');
const postcssPresetEnv = require('postcss-preset-env');
const postcssEach = require('postcss-each');
const postcssMixins = require('postcss-mixins');
const postcssConditionals = require('postcss-conditionals-renewed');
const postcssAtRulesVariables = require('postcss-at-rules-variables');
const autoprefixer = require('autoprefixer');
const tailwind = require('tailwindcss');
const mergeTrees = require('broccoli-merge-trees');
const toBoolean = require('./config/utils/to-boolean');
const environment = process.env.EMBER_ENV;
module.exports = function (defaults) {
const app = new EmberApp(defaults, {
storeConfigInMeta: false,
fingerprint: {
exclude: ['leaflet/', 'leaflet-images/', 'socketcluster-client.min.js'],
exclude: ['leaflet/', 'leaflet-images/', 'socketcluster-client.min.js', 'fleetbase.config.json', 'extensions.json'],
},
liveReload: {
@@ -30,31 +21,12 @@ module.exports = function (defaults) {
},
},
'ember-simple-auth': {
useSessionSetupMethod: true,
intl: {
silent: true,
},
postcssOptions: {
compile: {
enabled: true,
cacheInclude: [/.*\.(css|scss|hbs)$/, /.*\/tailwind\/config\.js$/, /.*tailwind\.js$/],
plugins: [
postcssAtRulesVariables,
postcssImport({
path: ['node_modules'],
plugins: [postcssAtRulesVariables, postcssImport],
}),
postcssMixins,
postcssPresetEnv({ stage: 1 }),
postcssEach,
tailwind('./tailwind.config.js'),
autoprefixer,
],
},
filter: {
enabled: true,
plugins: [postcssAtRulesVariables, postcssMixins, postcssEach, postcssConditionals, tailwind('./tailwind.config.js')],
},
'ember-simple-auth': {
useSessionSetupMethod: true,
},
babel: {
@@ -62,7 +34,6 @@ module.exports = function (defaults) {
},
});
let extensions = new FleetbaseExtensionsIndexer();
let runtimeConfigTree;
if (toBoolean(process.env.DISABLE_RUNTIME_CONFIG)) {
runtimeConfigTree = writeFile('fleetbase.config.json', '{}');
@@ -73,5 +44,5 @@ module.exports = function (defaults) {
});
}
return app.toTree([extensions, runtimeConfigTree].filter(Boolean));
return app.toTree([runtimeConfigTree].filter(Boolean));
};

View File

@@ -0,0 +1,376 @@
/* eslint-env node */
'use strict';
const fg = require('fast-glob');
const fs = require('fs');
const path = require('path');
const recast = require('recast');
const babelParser = require('recast/parsers/babel');
const builders = recast.types.builders;
const chokidar = require('chokidar');
module.exports = {
name: require('./package').name,
getGeneratedFileHeader() {
const year = new Date().getFullYear();
return `/**
* ███████╗██╗ ███████╗███████╗████████╗██████╗ █████╗ ███████╗███████╗
* ██╔════╝██║ ██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝
* █████╗ ██║ █████╗ █████╗ ██║ ██████╔╝███████║███████╗█████╗
* ██╔══╝ ██║ ██╔══╝ ██╔══╝ ██║ ██╔══██╗██╔══██║╚════██║██╔══╝
* ██║ ███████╗███████╗███████╗ ██║ ██████╔╝██║ ██║███████║███████╗
* ╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝
*
* AUTO-GENERATED FILE - DO NOT EDIT
*
* This file is automatically generated by the Fleetbase extension build system.
* Any manual changes will be overwritten on the next build.
*
* @generated
* @copyright © ${year} Fleetbase Pte Ltd. All rights reserved.
* @license AGPL-3.0-or-later
*/
`;
},
included(app) {
this._super.included.apply(this, arguments);
console.log('\n' + '/'.repeat(70));
console.log('[Fleetbase] Extension Build System');
console.log('/'.repeat(70));
// Generate files on startup
this.generateExtensionFiles();
// Watch for changes in development
this.watchExtensionFiles();
},
async generateExtensionFiles() {
// Clean up old/stale extensions directory before generating new files
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
if (fs.existsSync(extensionsDir)) {
console.log('[Fleetbase] Cleaning up old extensions directory...');
fs.rmSync(extensionsDir, { recursive: true, force: true });
}
const extensions = await this.getExtensions();
if (extensions.length === 0) {
console.log('[Fleetbase] No extensions found');
return;
}
console.log(`[Fleetbase] Discovered ${extensions.length} extension(s)`);
extensions.forEach((ext) => {
console.log(`[Fleetbase] - ${ext.name} (v${ext.version})`);
});
console.log('');
// Generate extension shims
this.generateExtensionShims(extensions);
// Generate extension loaders
this.generateExtensionLoaders(extensions);
// Generate router
this.generateRouter(extensions);
// Generate manifest
this.generateExtensionsManifest(extensions);
},
getExtensions() {
return new Promise((resolve, reject) => {
const extensions = [];
const seenPackages = new Set();
const cwd = this.project.root;
return fg(['node_modules/*/package.json', 'node_modules/*/*/package.json'], { cwd })
.then((results) => {
for (let i = 0; i < results.length; i++) {
const packagePath = path.join(cwd, results[i]);
const packageJson = fs.readFileSync(packagePath);
let packageData = null;
try {
packageData = JSON.parse(packageJson);
} catch (e) {
console.warn(`Could not parse package.json at ${packagePath}:`, e);
continue;
}
if (!packageData || !packageData.keywords || !packageData.keywords.includes('fleetbase-extension') || !packageData.keywords.includes('ember-engine')) {
continue;
}
// If we've seen this package before, skip it
if (seenPackages.has(packageData.name)) {
continue;
}
seenPackages.add(packageData.name);
extensions.push(this.only(packageData, ['name', 'description', 'version', 'fleetbase', 'keywords', 'license', 'repository']));
}
resolve(extensions);
})
.catch(reject);
});
},
only(subject, props = []) {
const keys = Object.keys(subject);
const result = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (props.includes(key)) {
result[key] = subject[key];
}
}
return result;
},
getExtensionMountPath(extensionName) {
let extensionNameSegments = extensionName.split('/');
let mountName = extensionNameSegments[1];
if (typeof mountName !== 'string') {
mountName = extensionNameSegments[0];
}
return mountName.replace('-engine', '');
},
generateExtensionShims(extensions) {
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
if (!fs.existsSync(extensionsDir)) {
fs.mkdirSync(extensionsDir, { recursive: true });
}
extensions.forEach((extension) => {
const extensionPath = path.join(this.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
if (!fs.existsSync(extensionPath)) {
return;
}
const extensionContent = fs.readFileSync(extensionPath, 'utf8');
const mountPath = extension.fleetbase?.route || this.getExtensionMountPath(extension.name);
const shimFile = path.join(extensionsDir, `${mountPath}.js`);
const fileContent = this.getGeneratedFileHeader() + extensionContent;
fs.writeFileSync(shimFile, fileContent, 'utf8');
console.log(`[Fleetbase] ✓ Generated app/extensions/${mountPath}.js`);
});
},
generateExtensionLoaders(extensions) {
const extensionsDir = path.join(this.project.root, 'app', 'extensions');
if (!fs.existsSync(extensionsDir)) {
fs.mkdirSync(extensionsDir, { recursive: true });
}
const imports = [];
const loaders = {};
extensions.forEach((extension) => {
const extensionPath = path.join(this.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
if (!fs.existsSync(extensionPath)) {
return;
}
const mountPath = extension.fleetbase?.route || this.getExtensionMountPath(extension.name);
const camelCaseName = mountPath.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
imports.push(`import ${camelCaseName} from './${mountPath}';`);
loaders[extension.name] = `() => ${camelCaseName}`;
});
const loadersContent =
this.getGeneratedFileHeader() +
`${imports.join('\n')}
export const EXTENSION_LOADERS = {
${Object.entries(loaders)
.map(([name, loader]) => ` '${name}': ${loader}`)
.join(',\n')}
};
export const getExtensionLoader = (packageName) => {
return EXTENSION_LOADERS[packageName];
};
export default getExtensionLoader;
`;
const loadersFile = path.join(extensionsDir, 'index.js');
fs.writeFileSync(loadersFile, loadersContent, 'utf8');
console.log(`[Fleetbase] ✓ Generated app/extensions/index.js`);
},
generateRouter(extensions) {
const consoleExtensions = extensions.filter((extension) => !extension.fleetbase || extension.fleetbase.mount !== 'root');
const rootExtensions = extensions.filter((extension) => extension.fleetbase && extension.fleetbase.mount === 'root');
const routerMapPath = path.join(this.project.root, 'router.map.js');
const routerFileContents = fs.readFileSync(routerMapPath, 'utf-8');
const ast = recast.parse(routerFileContents, { parser: babelParser });
recast.visit(ast, {
visitCallExpression(path) {
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'route' && path.value.arguments[0].value === 'console') {
let functionExpression;
// Find the function expression
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
// Check and add the new engine mounts
consoleExtensions.forEach((extension) => {
const mountPath = module.exports.getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
// Check if engine is already mounted
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
// If not mounted, append to the function body
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
let functionExpression;
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
rootExtensions.forEach((extension) => {
const mountPath = module.exports.getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}
this.traverse(path);
},
});
const output = recast.print(ast, { quote: 'single' }).code;
const routerFile = path.join(this.project.root, 'app/router.js');
const fileContent = this.getGeneratedFileHeader() + output;
fs.writeFileSync(routerFile, fileContent);
console.log(`[Fleetbase] ✓ Generated app/router.js`);
},
generateExtensionsManifest(extensions) {
const publicDir = path.join(this.project.root, 'public');
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
const manifest = extensions.map((ext) => ({
name: ext.name,
version: ext.version,
route: ext.fleetbase?.route,
}));
const manifestFile = path.join(publicDir, 'extensions.json');
fs.writeFileSync(manifestFile, JSON.stringify(manifest, null, 2), 'utf8');
console.log(`[Fleetbase] ✓ Generated public/extensions.json`);
},
watchExtensionFiles() {
const isDevelopment = process.env.EMBER_ENV !== 'production';
if (!isDevelopment) {
return;
}
const self = this;
this.getExtensions().then((extensions) => {
const extensionFiles = [];
extensions.forEach((extension) => {
const extensionPath = path.join(self.project.root, 'node_modules', extension.name, 'addon', 'extension.js');
if (fs.existsSync(extensionPath)) {
extensionFiles.push(extensionPath);
}
});
if (extensionFiles.length === 0) {
return;
}
const watcher = chokidar.watch(extensionFiles, {
persistent: true,
ignoreInitial: true,
});
watcher.on('change', (filePath) => {
console.log(`\n[Fleetbase] Extension file changed: ${path.basename(filePath)}`);
console.log('[Fleetbase] Regenerating extension files...\n');
self.generateExtensionFiles();
});
});
},
};

View File

@@ -0,0 +1,15 @@
{
"name": "fleetbase-extensions-generator",
"version": "0.0.0",
"private": true,
"keywords": [
"ember-addon"
],
"ember-addon": {
"configPath": "tests/dummy/config"
},
"dependencies": {
"broccoli-funnel": "^5.0.2",
"broccoli-merge-trees": "^5.2.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.7.18",
"version": "0.7.23",
"private": true,
"description": "Modular logistics and supply chain operating system (LSOS)",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -11,8 +11,7 @@
"test": "tests"
},
"scripts": {
"prebuild": "node prebuild.js",
"build": "pnpm run prebuild && ember build",
"build": "ember build",
"lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"",
"lint:css": "stylelint \"**/*.css\"",
"lint:css:fix": "concurrently \"npm:lint:css -- --fix\"",
@@ -22,22 +21,27 @@
"lint:js": "eslint . --cache",
"lint:js:fix": "eslint . --fix",
"lint:intl": "fleetbase-intl-lint",
"start": "pnpm run prebuild && ember serve",
"start:dev": "pnpm run prebuild && ember serve --environment development",
"start": "ember serve",
"start:dev": "ember serve --environment development",
"test": "concurrently \"npm:lint\" \"npm:test:*\" --names \"lint,test:\"",
"test:ember": "ember test"
},
"ember-addon": {
"paths": [
"lib/fleetbase-extensions-generator"
]
},
"dependencies": {
"@ember/legacy-built-in-components": "^0.4.2",
"@fleetbase/dev-engine": "^0.2.10",
"@fleetbase/ember-core": "latest",
"@fleetbase/ember-ui": "latest",
"@fleetbase/fleetops-data": "latest",
"@fleetbase/fleetops-engine": "^0.6.25",
"@fleetbase/iam-engine": "^0.1.4",
"@fleetbase/dev-engine": "^0.2.12",
"@fleetbase/ember-core": "^0.3.9",
"@fleetbase/ember-ui": "^0.3.15",
"@fleetbase/fleetops-data": "^0.1.24",
"@fleetbase/fleetops-engine": "^0.6.31",
"@fleetbase/iam-engine": "^0.1.6",
"@fleetbase/leaflet-routing-machine": "^3.2.17",
"@fleetbase/registry-bridge-engine": "^0.1.0",
"@fleetbase/storefront-engine": "^0.4.6",
"@fleetbase/registry-bridge-engine": "^0.1.2",
"@fleetbase/storefront-engine": "^0.4.10",
"@formatjs/intl-datetimeformat": "^6.18.2",
"@formatjs/intl-numberformat": "^8.15.6",
"@formatjs/intl-pluralrules": "^5.4.6",
@@ -78,6 +82,8 @@
"broccoli-asset-rev": "^3.0.0",
"broccoli-file-creator": "^2.1.1",
"broccoli-funnel": "^3.0.8",
"broccoli-merge-trees": "^4.2.0",
"chokidar": "4.0.3",
"concurrently": "^8.2.2",
"date-fns": "^2.30.0",
"dragula": "^3.7.3",
@@ -87,6 +93,7 @@
"ember-cli-babel": "^8.2.0",
"ember-cli-clean-css": "^3.0.0",
"ember-cli-dependency-checker": "^3.3.2",
"ember-cli-deprecation-workflow": "^4.0.0",
"ember-cli-dotenv": "^3.1.0",
"ember-cli-htmlbars": "^6.3.0",
"ember-cli-inject-live-reload": "^2.1.0",
@@ -143,9 +150,9 @@
},
"pnpm": {
"overrides": {
"@fleetbase/ember-core": "latest",
"@fleetbase/ember-ui": "latest",
"@fleetbase/fleetops-data": "latest"
"@fleetbase/ember-core": "^0.3.9",
"@fleetbase/ember-ui": "^0.3.15",
"@fleetbase/fleetops-data": "^0.1.24"
}
},
"prettier": {
@@ -162,5 +169,6 @@
}
}
]
}
},
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903"
}

2672
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
const fg = require('fast-glob');
const fs = require('fs');
const path = require('path');
const recast = require('recast');
const babelParser = require('recast/parsers/babel');
const builders = recast.types.builders;
function getExtensionMountPath(extensionName) {
let extensionNameSegments = extensionName.split('/');
let mountName = extensionNameSegments[1];
if (typeof mountName !== 'string') {
mountName = extensionNameSegments[0];
}
return mountName.replace('-engine', '');
}
function only(subject, props = []) {
const keys = Object.keys(subject);
const result = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (props.includes(key)) {
result[key] = subject[key];
}
}
return result;
}
function getExtensions() {
return new Promise((resolve, reject) => {
const extensions = [];
const seenPackages = new Set();
return fg(['node_modules/*/package.json', 'node_modules/*/*/package.json'])
.then((results) => {
for (let i = 0; i < results.length; i++) {
const packagePath = results[i];
const packageJson = fs.readFileSync(packagePath);
let packageData = null;
try {
packageData = JSON.parse(packageJson);
} catch (e) {
console.warn(`Could not parse package.json at ${packagePath}:`, e);
continue;
}
if (!packageData || !packageData.keywords || !packageData.keywords.includes('fleetbase-extension') || !packageData.keywords.includes('ember-engine')) {
continue;
}
// If we've seen this package before, skip it
if (seenPackages.has(packageData.name)) {
continue;
}
seenPackages.add(packageData.name);
extensions.push(only(packageData, ['name', 'description', 'version', 'fleetbase', 'keywords', 'license', 'repository']));
}
resolve(extensions);
})
.catch(reject);
});
}
function getRouterFileContents() {
const routerFilePath = path.join(__dirname, 'router.map.js');
const routerFileContents = fs.readFileSync(routerFilePath, 'utf-8');
return routerFileContents;
}
(async () => {
const extensions = await getExtensions();
const consoleExtensions = extensions.filter((extension) => !extension.fleetbase || extension.fleetbase.mount !== 'root');
const rootExtensions = extensions.filter((extension) => extension.fleetbase && extension.fleetbase.mount === 'root');
const routerFileContents = getRouterFileContents();
const ast = recast.parse(routerFileContents, { parser: babelParser });
recast.visit(ast, {
visitCallExpression(path) {
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'route' && path.value.arguments[0].value === 'console') {
let functionExpression;
// Find the function expression
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
// Check and add the new engine mounts
consoleExtensions.forEach((extension) => {
const mountPath = getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
// Check if engine is already mounted
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
// If not mounted, append to the function body
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}
// console.log(path.value.callee.property.name);
if (path.value.type === 'CallExpression' && path.value.callee.property.name === 'map') {
let functionExpression;
path.value.arguments.forEach((arg) => {
if (arg.type === 'FunctionExpression') {
functionExpression = arg;
}
});
if (functionExpression) {
rootExtensions.forEach((extension) => {
const mountPath = getExtensionMountPath(extension.name);
let route = mountPath;
if (extension.fleetbase && extension.fleetbase.route) {
route = extension.fleetbase.route;
}
const isMounted = functionExpression.body.body.some((expressionStatement) => {
return expressionStatement.expression.arguments[0].value === extension.name;
});
if (!isMounted) {
functionExpression.body.body.push(
builders.expressionStatement(
builders.callExpression(builders.memberExpression(builders.thisExpression(), builders.identifier('mount')), [
builders.literal(extension.name),
builders.objectExpression([
builders.property('init', builders.identifier('as'), builders.literal(route)),
builders.property('init', builders.identifier('path'), builders.literal(route)),
]),
])
)
);
}
});
}
}
this.traverse(path);
},
});
const output = recast.print(ast, { quote: 'single' }).code;
fs.writeFileSync(path.join(__dirname, 'app/router.js'), output);
})();

View File

@@ -1,18 +1,6 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicon/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"name": "Fleetbase Console",
"short_name": "Fleetbase",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"

View File

@@ -8,6 +8,7 @@ export default class Router extends EmberRouter {
Router.map(function () {
this.route('virtual', { path: '/:slug' });
this.route('install');
this.route('onboard', function () {
this.route('index', { path: '/' });
});

View File

@@ -1,26 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | dashboard/widget-panel', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<Dashboard::WidgetPanel />`);
assert.dom().hasText('');
// Template block usage:
await render(hbs`
<Dashboard::WidgetPanel>
template block text
</Dashboard::WidgetPanel>
`);
assert.dom().hasText('template block text');
});
});

View File

@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Controller | virtual', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:virtual');
assert.ok(controller);
});
});

View File

@@ -0,0 +1,39 @@
import Application from '@ember/application';
import config from '@fleetbase/console/config/environment';
import { initialize } from '@fleetbase/console/instance-initializers/initialize-registries';
import { module, test } from 'qunit';
import Resolver from 'ember-resolver';
import { run } from '@ember/runloop';
module('Unit | Instance Initializer | initialize-registries', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
this.TestApplication.instanceInitializer({
name: 'initializer under test',
initialize,
});
this.application = this.TestApplication.create({
autoboot: false,
});
this.instance = this.application.buildInstance();
});
hooks.afterEach(function () {
run(this.instance, 'destroy');
run(this.application, 'destroy');
});
// TODO: Replace this with your real tests.
test('it works', async function (assert) {
await this.instance.boot();
assert.ok(true);
});
});

View File

@@ -0,0 +1,39 @@
import Application from '@ember/application';
import config from '@fleetbase/console/config/environment';
import { initialize } from '@fleetbase/console/instance-initializers/initialize-widgets';
import { module, test } from 'qunit';
import Resolver from 'ember-resolver';
import { run } from '@ember/runloop';
module('Unit | Instance Initializer | initialize-widgets', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
this.TestApplication.instanceInitializer({
name: 'initializer under test',
initialize,
});
this.application = this.TestApplication.create({
autoboot: false,
});
this.instance = this.application.buildInstance();
});
hooks.afterEach(function () {
run(this.instance, 'destroy');
run(this.application, 'destroy');
});
// TODO: Replace this with your real tests.
test('it works', async function (assert) {
await this.instance.boot();
assert.ok(true);
});
});

View File

@@ -0,0 +1,39 @@
import Application from '@ember/application';
import config from '@fleetbase/console/config/environment';
import { initialize } from '@fleetbase/console/instance-initializers/setup-extensions';
import { module, test } from 'qunit';
import Resolver from 'ember-resolver';
import { run } from '@ember/runloop';
module('Unit | Instance Initializer | setup-extensions', function (hooks) {
hooks.beforeEach(function () {
this.TestApplication = class TestApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
this.TestApplication.instanceInitializer({
name: 'initializer under test',
initialize,
});
this.application = this.TestApplication.create({
autoboot: false,
});
this.instance = this.application.buildInstance();
});
hooks.afterEach(function () {
run(this.instance, 'destroy');
run(this.application, 'destroy');
});
// TODO: Replace this with your real tests.
test('it works', async function (assert) {
await this.instance.boot();
assert.ok(true);
});
});

View File

@@ -454,6 +454,7 @@ dashboard-widget-panel:
{widgetName} Widget
select-widgets: Select Widgets
close-and-save: Close and Save
filter-widgets: Filter widgets by keyword
filters-picker:
filters: Filters

View File

@@ -0,0 +1,821 @@
app:
name: Fleetbase
common:
new: Nuevo
create: Crear
add: Agregar
edit: Editar
update: Actualizar
save: Guardar
save-changes: Guardar Cambios
delete: Eliminar
delete-selected: Eliminar Seleccionados
delete-selected-count: Eliminar {count} Seleccionados
your-profile: Tu Perfil
date-of-birth: Fecha de Nacimiento
organization: Organización
two-factor: Dos Factores
remove: Remover
cancel: Cancelar
confirm: Confirmar
close: Cerrar
open: Abrir
view: Ver
preview: Vista previa
upload: Subir
download: Descargar
import: Importar
export: Exportar
print: Imprimir
duplicate: Duplicar
copy: Copiar
paste: Pegar
share: Compartir
refresh: Actualizar
reset: Restablecer
retry: Reintentar
back: Atrás
next: Siguiente
previous: Anterior
submit: Enviar
apply: Aplicar
continue: Continuar
proceed: Proceder
select: Seleccionar
deselect: Deseleccionar
search: Buscar
filter: Filtrar
sort: Ordenar
view-all: Ver Todo
clear: Limpiar
done: Hecho
finish: Finalizar
skip: Omitir
method: Método
bulk-delete: Eliminación Masiva
bulk-delete-resource: Eliminación Masiva de {resource}
bulk-cancel: Cancelación masiva
bulk-cancel-resource: Cancelación masiva de {resource}
bulk-actions: Acciones masivas
column: Columna
row: Fila
table: Tabla
list: Lista
grid: Cuadrícula
form: Formulario
field: Campo
section: Sección
panel: Panel
card: Tarjeta
tab: Pestaña
modal: Modal
dialog: Diálogo
menu: Menú
dropdown: Desplegable
tooltip: Información sobre herramientas
sidebar: Barra lateral
toolbar: Barra de herramientas
footer: Pie de página
header: Encabezado
title: Título
subtitle: Subtítulo
description: Descripción
placeholder: Marcador de posición
label: Etiqueta
button: Botón
icon: Ícono
avatar: Avatar
link: Enlace
badge: Insignia
tag: Etiqueta
banner: Banner
step: Paso
progress: Progreso
map: Mapa
board: Tablero
loading: Cargando
loading-resource: Cargando {resource}
saving: Guardando
processing: Procesando
fetching: Obteniendo
updating: Actualizando
uploading: Subiendo
completed: Completado
success: Éxito
failed: Fallido
error: Error
warning: Advertencia
info: Información
ready: Listo
activity: Actividad
active: Activo
inactive: Inactivo
enabled: Habilitado
disabled: Deshabilitado
online: En línea
offline: Desconectado
pending: Pendiente
archived: Archivado
hidden: Oculto
visible: Visible
empty: Vacío
not-found: No encontrado
no-results: Sin resultados
try-again: Intentar de nuevo
are-you-sure: ¿Estás seguro?
changes-saved: Cambios guardados correctamente.
saved-successfully: Cambios guardados correctamente.
field-saved: '{field} guardado correctamente.'
changes-discarded: Cambios descartados.
delete-confirm: ¿Estás seguro de que quieres eliminar este elemento?
action-successful: Acción completada con éxito.
action-failed: La acción ha fallado. Por favor, inténtalo de nuevo.
something-went-wrong: Algo salió mal.
please-wait: Por favor espera...
sign-in: Iniciar sesión
sign-out: Cerrar sesión
sign-up: Registrarse
log-in: Iniciar sesión
log-out: Cerrar sesión
register: Registrar
forgot-password: ¿Olvidaste tu contraseña?
reset-password: Restablecer contraseña
change-password: Cambiar contraseña
password: Contraseña
confirm-password: Confirmar contraseña
email: Correo electrónico
username: Nombre de usuario
remember-me: Recuérdame
welcome: Bienvenido
welcome-back: Bienvenido de nuevo
profile: Perfil
account: Cuenta
settings: Configuración
preferences: Preferencias
record: Registro
records: Registros
item: Elemento
items: Elementos
entry: Entrada
entries: Entradas
id: ID
name: Nombre
type: Tipo
category: Categoría
overview: Resumen
value: Valor
amount: Cantidad
price: Precio
quantity: Cantidad
status: Estado
date: Fecha
date-created: Fecha de creación
date-updated: Fecha de actualización
time: Hora
created-at: Creado en
updated-at: Actualizado en
expired-at: Expirado en
last-seen-at: Última vez visto en
last-modified: Última modificación
last-modified-data: 'Última modificación: {date}'
actions: Acciones
details: Detalles
notes: Notas
reference: Referencia
filter-by: Filtrar por
filter-by-field: Filtrar por {field}
sort-by: Ordenar por
ascending: Ascendente
descending: Descendente
all: Todos
none: Ninguno
select-all: Seleccionar todo
deselect-all: Deseleccionar todo
show-more: Mostrar más
show-less: Mostrar menos
page: Página
of: de
total: Total
items-per-page: Elementos por página
showing: Mostrando
to: a
results: Resultados
load-more: Cargar más
no-more-results: No hay más resultados
today: Hoy
yesterday: Ayer
tomorrow: Mañana
day: Día
week: Semana
month: Mes
year: Año
date-range: Rango de fechas
start-date: Fecha de inicio
end-date: Fecha de fin
time-zone: Zona horaria
system: Sistema
dashboard: Panel
home: Inicio
analytics: Analíticas
reports: Informes
logs: Registros
help: Ayuda
support: Soporte
contact: Contacto
documentation: Documentación
language: Idioma
timezone: Zona horaria
version: Versión
theme: Tema
light-mode: Modo claro
dark-mode: Modo oscuro
update-available: Actualización disponible
install-update: Instalar actualización
maintenance-mode: Modo mantenimiento
notification: Notificación
notifications: Notificaciones
mark-as-read: Marcar como leído
mark-all-as-read: Marcar todo como leído
clear-notifications: Limpiar notificaciones
company: Empresa
companies: Empresas
user: Usuario
users: Usuarios
role: Rol
roles: Roles
permission: Permiso
permissions: Permisos
group: Grupo
groups: Grupos
unauthorized: No autorizado
forbidden: Prohibido
resource-not-found: Recurso no encontrado
server-error: Error del servidor
validation-error: Error de validación
timeout-error: Tiempo de espera agotado
network-error: Error de red
unknown-error: Error desconocido
file: Archivo
files: Archivos
folder: Carpeta
folders: Carpetas
upload-file: Subir archivo
upload-files: Subir archivos
upload-image: Subir imagen
upload-image-supported: Soporta PNG, JPEG y GIF
choose-file: Elegir archivo
choose-files: Elegir archivos
drag-and-drop: Arrastrar y soltar
download-file: Descargar archivo
file-size: Tamaño del archivo
file-type: Tipo de archivo
confirm-delete: Confirmar eliminación
confirm-action: Confirmar acción
confirm-exit: Confirmar salida
confirm-and-save-changes: Confirmar y guardar cambios
are-you-sure-exit: ¿Estás seguro de que quieres salir?
unsaved-changes-warning: Tienes cambios sin guardar.
connected: Conectado
disconnected: Desconectado
reconnecting: Reconectando
connection-lost: Conexión perdida
connection-restored: Conexión restaurada
show: Mostrar
hide: Ocultar
expand: Expandir
collapse: Colapsar
enable: Habilitar
disable: Deshabilitar
minimize: Minimizar
maximize: Maximizar
restore: Restaurar
zoom-in: Acercar
zoom-out: Alejar
fullscreen: Pantalla completa
exit-fullscreen: Salir de pantalla completa
yes:
no: No
ok: Aceptar
none-available: Ninguno disponible
default: Predeterminado
custom: Personalizado
general: General
advanced: Avanzado
placeholder-text: Introduce texto aquí...
learn-more: Aprender más
view-resource: Ver {resource}
view-resource-details: Ver detalles de {resource}
create-a-new-resource: Crear un nuevo {resource}
create-new-resource: Crear nuevo {resource}
search-resource: Buscar {resource}
new-resource: Nuevo {resource}
update-resource: Actualizar {resource}
save-resource-changes: Guardar cambios de {resource}
creating-resource: Creando {resource}
cancel-resource: Cancelar {resource}
delete-resource: Eliminar {resource}
delete-resource-name: 'Eliminar: {resourceName}'
delete-resource-named: Eliminar {resource} ({resourceName})
delete-resource-prompt: Esta acción no se puede deshacer. Una vez eliminado, el
registro será eliminado permanentemente.
delete-cannot-be-undone: Esta acción no se puede deshacer. Una vez eliminado, el
registro será eliminado permanentemente.
create-resource: Crear {resource}
edit-resource: Editar {resource}
edit-resource-details: Editar detalles de {resource}
edit-resource-type-name: 'Editar {resource}: {resourceName}'
edit-resource-name: 'Editar: {resourceName}'
config: Configuración
select-field: Seleccionar {field}
columns: Columnas
metadata: Metadatos
meta: Meta
resource-created-success: Nuevo {resource} creado con éxito.
resource-created-success-name: Nuevo {resource} ({resourceName}) creado con éxito.
resource-updated-success: '{resource} ({resourceName}) actualizado con éxito.'
resource-action-success: '{resource} ({resourceName}) {action} con éxito.'
resource-deleted-success: '{resource} ({resourceName}) eliminado con éxito.'
resource-deleted: '{resource} ({resourceName}) eliminado.'
continue-without-saving: ¿Continuar sin guardar?
continue-without-saving-prompt: Tienes cambios sin guardar en este {resource}. Continuar
descartará esos cambios. Haz clic en Continuar para proceder.
resource:
alert: Alerta
alerts: Alertas
brand: Marca
brands: Marcas
category: Categoría
categories: Categorías
chat-attachment: Archivo adjunto de chat
chat-attachments: Archivos adjuntos de chat
chat-channel: Canal de chat
chat-channels: Canales de chat
chat-log: Registro de chat
chat-logs: Registros de chat
chat-message: Mensaje de chat
chat-messages: Mensajes de chat
chat-participant: Participante del chat
chat-participants: Participantes del chat
chat-receipt: Recibo de chat
chat-receipts: Recibos de chat
comment: Comentario
comments: Comentarios
company: Empresa
companies: Empresas
custom-field-value: Valor de campo personalizado
custom-field-values: Valores de campo personalizado
custom-field: Campo personalizado
custom-fields: Campos personalizados
dashboard-widget: Widget del panel
dashboard-widgets: Widgets del panel
dashboard: Panel
dashboards: Paneles
extension: Extensión
extensions: Extensiones
file: Archivo
files: Archivos
group: Grupo
groups: Grupos
notification: Notificación
notifications: Notificaciones
permission: Permiso
permissions: Permisos
policy: Política
policies: Políticas
report: Informe
reports: Informes
role: Rol
roles: Roles
setting: Configuración
settings: Configuraciones
transaction: Transacción
transactions: Transacciones
user-device: Dispositivo del usuario
user-devices: Dispositivos del usuario
user: Usuario
users: Usuarios
dropzone:
file: archivo
drop-to-upload: Suelta para subir
invalid: Inválido
files-ready-for-upload: '{numOfFiles} listo(s) para subir.'
upload-images-videos: Subir imágenes y videos
upload-documents: Subir documentos
upload-documents-files: Subir documentos y archivos
upload-avatar-files: Subir avatares personalizados
dropzone-supported-images-videos: Arrastra y suelta archivos de imagen y video en
esta zona
dropzone-supported-avatars: Arrastra y suelta archivos SVG o PNG
dropzone-supported-files: Arrastra y suelta archivos en esta zona
or-select-button-text: o selecciona archivos para subir.
upload-queue: Cola de subida
uploading: Subiendo...
two-fa-enforcement-alert:
message: Para mejorar la seguridad de tu cuenta, tu organización requiere Autenticación
de Dos Factores (2FA). Activa 2FA en la configuración de tu cuenta para una
capa adicional de protección.
button-text: Configurar 2FA
comment-thread:
publish-comment-button-text: Publicar comentario
publish-reply-button-text: Publicar respuesta
reply-comment-button-text: Responder
edit-comment-button-text: Editar
delete-comment-button-text: Eliminar
comment-published-ago: hace {createdAgo}
comment-input-placeholder: Escribe un nuevo comentario...
comment-reply-placeholder: Escribe tu respuesta...
comment-input-empty-notification: No puedes publicar comentarios vacíos...
comment-min-length-notification: El comentario debe tener al menos 2 caracteres
dashboard:
select-dashboard: Seleccionar Panel
create-new-dashboard: Crear nuevo Panel
create-a-new-dashboard: Crear un nuevo Panel
confirm-create-dashboard: ¡Crear Panel!
edit-layout: Editar diseño
add-widgets: Agregar widgets
delete-dashboard: Eliminar panel
save-dashboard: Guardar Panel
you-cannot-delete-this-dashboard: No puedes eliminar este panel.
are-you-sure-you-want-delete-dashboard: ¿Estás seguro de que quieres eliminar este
{dashboardName}?
dashboard-widget-panel:
widget-name: Widget {widgetName}
select-widgets: Seleccionar Widgets
close-and-save: Cerrar y Guardar
filters-picker:
filters: Filtros
filter-data: Filtrar Datos
visible-column-picker:
select-viewable-columns: Seleccionar columnas visibles
customize-columns: Personalizar Columnas
component:
file:
dropdown-label: Acciones de archivo
import-modal:
loading-message: Procesando importación...
drop-upload: Suelta para subir
invalid: Inválido
ready-upload: listo para subir.
upload-spreadsheets: Subir Hojas de Cálculo
drag-drop: Arrastra y suelta archivos de hojas de cálculo en esta zona
button-text: o selecciona hojas de cálculo para subir
spreadsheets: hojas de cálculo
upload-queue: Cola de Subida
dropzone:
file: archivo
drop-to-upload: Suelta para subir
invalid: Inválido
files-ready-for-upload: '{numOfFiles} listo(s) para subir.'
upload-images-videos: Subir Imágenes y Videos
upload-documents: Subir Documentos
upload-documents-files: Subir Documentos y Archivos
upload-avatar-files: Subir Avatares Personalizados
dropzone-supported-images-videos: Arrastra y suelta archivos de imagen y video
en esta zona
dropzone-supported-avatars: Arrastra y suelta archivos SVG o PNG
dropzone-supported-files: Arrastra y suelta archivos en esta zona
or-select-button-text: o selecciona archivos para subir.
upload-queue: Cola de Subida
uploading: Subiendo...
two-fa-enforcement-alert:
message: Para mejorar la seguridad de tu cuenta, tu organización requiere Autenticación
de Dos Factores (2FA). Activa 2FA en la configuración de tu cuenta para una
capa adicional de protección.
button-text: Configurar 2FA
comment-thread:
publish-comment-button-text: Publicar comentario
publish-reply-button-text: Publicar respuesta
reply-comment-button-text: Responder
edit-comment-button-text: Editar
delete-comment-button-text: Eliminar
comment-published-ago: hace {createdAgo}
comment-input-placeholder: Escribe un nuevo comentario...
comment-reply-placeholder: Escribe tu respuesta...
comment-input-empty-notification: No puedes publicar comentarios vacíos...
comment-min-length-notification: El comentario debe tener al menos 2 caracteres
dashboard:
select-dashboard: Seleccionar panel
create-new-dashboard: Crear nuevo panel
create-a-new-dashboard: Crear un nuevo panel
confirm-create-dashboard: ¡Crear panel!
edit-layout: Editar diseño
add-widgets: Agregar widgets
delete-dashboard: Eliminar panel
save-dashboard: Guardar panel
you-cannot-delete-this-dashboard: No puedes eliminar este panel.
are-you-sure-you-want-delete-dashboard: ¿Estás seguro de que quieres eliminar
{dashboardName}?
dashboard-widget-panel:
widget-name: Widget {widgetName}
select-widgets: Seleccionar widgets
close-and-save: Cerrar y guardar
services:
dashboard-service:
create-dashboard-success-notification: Nuevo panel `{dashboardName}` creado con
éxito.
delete-dashboard-success-notification: El panel `{dashboardName}` fue eliminado.
auth:
verification:
header-title: Verificación de cuenta
title: Verifica tu dirección de correo electrónico
message-text: <strong>¡Casi terminado!</strong><br> Revisa tu correo para un código
de verificación.
verification-code-text: Introduce el código de verificación que recibiste por
correo.
verification-input-label: Código de verificación
verify-button-text: Verificar y continuar
didnt-receive-a-code: ¿No has recibido un código?
not-sent:
message: ¿No has recibido un código?
alternative-choice: Usa las opciones alternativas abajo para verificar tu cuenta.
resend-email: Reenviar correo
send-by-sms: Enviar por SMS
two-fa:
verify-code:
verification-code: Código de verificación
check-title: Revisa tu correo o teléfono
check-subtitle: Te hemos enviado un código de verificación. Introduce el código
abajo para completar el proceso de inicio de sesión.
expired-help-text: Tu código de autenticación 2FA ha expirado. Puedes solicitar
otro código si necesitas más tiempo.
resend-code: Reenviar código
verify-code: Verificar código
cancel-two-factor: Cancelar autenticación de dos factores
invalid-session-error-notification: Sesión inválida. Por favor, inténtalo de
nuevo.
verification-successful-notification: ¡Verificación exitosa!
verification-code-expired-notification: El código de verificación ha expirado.
Por favor, solicita uno nuevo.
verification-code-failed-notification: La verificación falló. Por favor, inténtalo
de nuevo.
resend-code:
verification-code-resent-notification: Nuevo código de verificación enviado.
verification-code-resent-error-notification: Error al reenviar el código de
verificación. Por favor, inténtalo de nuevo.
forgot-password:
success-message: ¡Revisa tu correo para continuar!
is-sent:
title: ¡Casi listo!
message: <strong>¡Revisa tu correo electrónico!</strong><br> Te hemos enviado
un enlace mágico a tu correo que te permitirá restablecer tu contraseña. El
enlace expira en 15 minutos.
not-sent:
title: ¿Olvidaste tu contraseña?
message: <strong>No te preocupes, te respaldamos.</strong><br> Introduce el
correo electrónico que usas para iniciar sesión en {appName} y te enviaremos
un enlace seguro para restablecer tu contraseña.
form:
email-label: Tu dirección de correo electrónico
submit-button: ¡OK, envíame un enlace mágico!
nevermind-button: No importa
login:
title: Inicia sesión en tu cuenta
no-identity-notification: ¿Olvidaste ingresar tu correo electrónico?
no-password-notification: ¿Olvidaste ingresar tu contraseña?
unverified-notification: Tu cuenta necesita ser verificada para continuar.
password-reset-required: Se requiere un restablecimiento de contraseña para continuar.
failed-attempt:
message: <strong>¿Olvidaste tu contraseña?</strong><br> Haz clic en el botón
de abajo para restablecer tu contraseña.
button-text: ¡Ok, ayúdame a restablecerla!
form:
email-label: Correo electrónico
password-label: Contraseña
remember-me-label: Recuérdame
forgot-password-label: ¿Olvidaste tu contraseña?
sign-in-button: Iniciar sesión
create-account-button: Crear una nueva cuenta
slow-connection-message: Experimentando problemas de conectividad.
reset-password:
success-message: ¡Tu contraseña ha sido restablecida! Inicia sesión para continuar.
invalid-verification-code: Este enlace para restablecer la contraseña es inválido
o ha expirado.
title: Restablece tu contraseña
form:
code:
label: Tu código de restablecimiento
help-text: El código de verificación que recibiste en tu correo electrónico.
password:
label: Nueva contraseña
help-text: Introduce una contraseña de al menos 6 caracteres para continuar.
confirm-password:
label: Confirma la nueva contraseña
help-text: Introduce una contraseña de al menos 6 caracteres para continuar.
submit-button: Restablecer contraseña
back-button: Volver
console:
create-or-join-organization:
modal-title: Crear o unirse a una organización
join-success-notification: ¡Te has unido a una nueva organización!
create-success-notification: ¡Has creado una nueva organización!
switch-organization:
modal-title: ¿Estás seguro de que quieres cambiar la organización a {organizationName}?
modal-body: Al confirmar, tu cuenta permanecerá iniciada, pero tu organización
principal será cambiada.
modal-accept-button-text: Sí, quiero cambiar de organización
success-notification: Has cambiado de organización
account:
index:
upload-new: Subir nuevo
phone: Tu número de teléfono.
photos: fotos
timezone: Selecciona tu zona horaria.
admin:
menu:
overview: Resumen
organizations: Organizaciones
branding: Marca
2fa-config: Configuración 2FA
schedule-monitor: Monitor de programación
services: Servicios
mail: Correo
filesystem: Sistema de archivos
queue: Cola
socket: Socket
push-notifications: Notificaciones push
schedule-monitor:
schedule-monitor: Monitor de programación
task-logs-for: 'Registros de tareas para: '
showing-last-count: Mostrando los últimos {count} registros
name: Nombre
type: Tipo
timezone: Zona horaria
last-started: Último inicio
last-finished: Última finalización
last-failure: Último fallo
date: Fecha
memory: Memoria
runtime: Tiempo de ejecución
output: Salida
no-output: Sin salida
config:
database:
title: Configuración de la base de datos
filesystem:
title: Configuración del sistema de archivos
mail:
title: Configuración de correo
notification-channels:
title: Configuración de notificaciones push
queue:
title: Configuración de la cola
services:
title: Configuración de servicios
socket:
title: Configuración de socket
branding:
title: Marca
icon-text: Ícono
upload-new: Subir nuevo
reset-default: Restablecer a predeterminado
logo-text: Logo
theme: Tema predeterminado
index:
total-users: Total de usuarios
total-organizations: Total de organizaciones
total-transactions: Total de transacciones
notifications:
title: Notificaciones
notification-settings: Configuración de notificaciones
organizations:
index:
title: Organizaciones
owner-name-column: Propietario
owner-phone-column: Teléfono del propietario
owner-email-column: Correo electrónico del propietario
users-count-column: Usuarios
phone-column: Teléfono
email-column: Correo electrónico
users:
title: Usuarios
settings:
index:
title: Configuración de la organización
organization-name: Nombre de la organización
organization-description: Descripción de la organización
organization-phone: Número de teléfono de la organización
organization-currency: Moneda de la organización
organization-id: ID de la organización
organization-branding: Marca de la organización
logo: Logo
logo-help-text: Logo para tu organización.
upload-new-logo: Subir nuevo logo
backdrop: Fondo
backdrop-help-text: Banner o imagen de fondo opcional para tu organización.
upload-new-backdrop: Subir nuevo fondo
organization-timezone: Selecciona la zona horaria predeterminada para tu organización.
select-timezone: Selecciona zona horaria.
extensions:
title: ¡Las extensiones llegarán pronto!
message: Por favor, vuelve a consultar en las próximas versiones mientras preparamos
el lanzamiento del repositorio y mercado de Extensiones.
notifications:
select-all: Seleccionar todo
mark-as-read: Marcar como leído
received: 'Recibido:'
message: No hay notificaciones para mostrar.
invite:
for-users:
invitation-message: Has sido invitado a unirte a {companyName}
invitation-sent-message: Has sido invitado a unirte a la organización {companyName}
en {appName}. Para aceptar esta invitación, introduce tu código de invitación
recibido por correo electrónico y haz clic en continuar.
invitation-code-sent-text: Tu código de invitación
accept-invitation-text: Aceptar invitación
onboard:
index:
title: Crea tu cuenta
welcome-title: <strong>¡Bienvenido a {companyName}!</strong><br />
welcome-text: Completa los detalles requeridos a continuación para comenzar.
full-name: Nombre completo
full-name-help-text: Tu nombre completo
your-email: Dirección de correo electrónico
your-email-help-text: Tu dirección de correo electrónico
phone: Número de teléfono
phone-help-text: Tu número de teléfono
organization-name: Nombre de la organización
organization-help-text: El nombre de tu organización, todos tus servicios y recursos
serán gestionados bajo esta organización, más adelante podrás crear tantas organizaciones
como quieras o necesites.
password: Introduce una contraseña
password-help-text: Tu contraseña, asegúrate de que sea buena.
confirm-password: Confirma tu contraseña
confirm-password-help-text: Solo para confirmar la contraseña que introdujiste
arriba.
continue-button-text: Continuar
verify-email:
header-title: Verificación de cuenta
title: Verifica tu dirección de correo electrónico
message-text: <strong>¡Casi terminado!</strong><br> Revisa tu correo electrónico
para un código de verificación.
verification-code-text: Introduce el código de verificación que recibiste por
correo electrónico.
verification-input-label: Código de verificación
verify-button-text: Verificar y continuar
didnt-receive-a-code: ¿No has recibido un código todavía?
not-sent:
message: ¿No has recibido un código todavía?
alternative-choice: Usa las opciones alternativas a continuación para verificar
tu cuenta.
resend-email: Reenviar correo electrónico
send-by-sms: Enviar por SMS
install:
installer-header: Instalador
failed-message-sent: ¡La instalación falló! Haz clic en el botón de abajo para reintentar
la instalación.
retry-install: Reintentar instalación
start-install: Iniciar instalación
layout:
header:
menus:
organization:
settings: Configuración de la organización
create-or-join: Crear o unirse a organizaciones
explore-extensions: Explorar extensiones
user:
view-profile: Ver perfil
keyboard-shortcuts: Mostrar atajos de teclado
changelog: Registro de cambios

View File

@@ -0,0 +1,741 @@
app:
name: فلیت‌بیس
common:
new: جدید
create: ایجاد
add: افزودن
edit: ویرایش
update: به‌روزرسانی
save: ذخیره
save-changes: ذخیره تغییرات
delete: حذف
delete-selected: حذف انتخاب‌شده‌ها
delete-selected-count: حذف {count} مورد انتخاب‌شده
your-profile: پروفایل شما
date-of-birth: تاریخ تولد
organization: سازمان
two-factor: احراز هویت دو مرحله‌ای
remove: حذف
cancel: لغو
confirm: تأیید
close: بستن
open: باز کردن
view: مشاهده
preview: پیش‌نمایش
upload: بارگذاری
download: دانلود
import: وارد کردن
export: صادر کردن
print: چاپ
duplicate: تکثیر
copy: کپی
paste: جای‌گذاری
share: اشتراک‌گذاری
refresh: تازه‌سازی
reset: بازنشانی
retry: تلاش مجدد
back: بازگشت
next: بعدی
previous: قبلی
submit: ارسال
apply: اعمال
continue: ادامه
proceed: ادامه دادن
select: انتخاب
deselect: لغو انتخاب
search: جستجو
filter: فیلتر
sort: مرتب‌سازی
view-all: مشاهده همه
clear: پاک کردن
done: انجام شد
finish: پایان
skip: رد کردن
method: روش
bulk-delete: حذف گروهی
bulk-delete-resource: حذف گروهی {resource}
bulk-cancel: لغو گروهی
bulk-cancel-resource: لغو گروهی {resource}
bulk-actions: اقدامات گروهی
column: ستون
row: ردیف
table: جدول
list: لیست
grid: شبکه
form: فرم
field: فیلد
section: بخش
panel: پنل
card: کارت
tab: زبانه
modal: مودال
dialog: دیالوگ
menu: منو
dropdown: کشویی
tooltip: راهنمای ابزار
sidebar: نوار کناری
toolbar: نوار ابزار
footer: پاورقی
header: سربرگ
title: عنوان
subtitle: زیرعنوان
description: توضیحات
placeholder: مکان‌نما
label: برچسب
button: دکمه
icon: آیکون
avatar: آواتار
link: لینک
badge: نشان
tag: برچسب
banner: بنر
step: مرحله
progress: پیشرفت
map: نقشه
board: تابلو
loading: در حال بارگذاری
loading-resource: در حال بارگذاری {resource}
saving: در حال ذخیره
processing: در حال پردازش
fetching: در حال دریافت
updating: در حال به‌روزرسانی
uploading: در حال بارگذاری
completed: تکمیل شد
success: موفقیت
failed: ناموفق
error: خطا
warning: هشدار
info: اطلاعات
ready: آماده
activity: فعالیت
active: فعال
inactive: غیرفعال
enabled: فعال‌شده
disabled: غیرفعال‌شده
online: آنلاین
offline: آفلاین
pending: در انتظار
archived: بایگانی‌شده
hidden: مخفی
visible: قابل مشاهده
empty: خالی
not-found: پیدا نشد
no-results: بدون نتیجه
try-again: دوباره امتحان کنید
are-you-sure: آیا مطمئن هستید؟
changes-saved: تغییرات با موفقیت ذخیره شد
saved-successfully: تغییرات با موفقیت ذخیره شد
field-saved: "{field} با موفقیت ذخیره شد"
changes-discarded: تغییرات لغو شد
delete-confirm: آیا مطمئن هستید که می‌خواهید این مورد را حذف کنید؟
action-successful: اقدام با موفقیت انجام شد
action-failed: اقدام ناموفق بود. لطفاً دوباره امتحان کنید
something-went-wrong: مشکلی پیش آمد
please-wait: لطفاً منتظر بمانید...
sign-in: ورود
sign-out: خروج
sign-up: ثبت‌نام
log-in: ورود
log-out: خروج
register: ثبت
forgot-password: رمز عبور را فراموش کرده‌اید؟
reset-password: بازنشانی رمز عبور
change-password: تغییر رمز عبور
password: رمز عبور
confirm-password: تأیید رمز عبور
email: ایمیل
username: نام کاربری
remember-me: مرا به خاطر بسپار
welcome: خوش آمدید
welcome-back: خوش آمدید دوباره
profile: پروفایل
account: حساب کاربری
settings: تنظیمات
preferences: ترجیحات
record: رکورد
records: رکوردها
item: مورد
items: موارد
entry: ورودی
entries: ورودی‌ها
id: شناسه
name: نام
type: نوع
category: دسته‌بندی
overview: مرور
value: مقدار
amount: مبلغ
price: قیمت
quantity: تعداد
status: وضعیت
date: تاریخ
date-created: تاریخ ایجاد
date-updated: تاریخ به‌روزرسانی
time: زمان
created-at: ایجاد شده در
updated-at: به‌روزرسانی شده در
expired-at: منقضی شده در
last-seen-at: آخرین بازدید در
last-modified: آخرین ویرایش
last-modified-data: "آخرین ویرایش: {date}"
actions: اقدامات
details: جزئیات
notes: یادداشت‌ها
reference: مرجع
filter-by: فیلتر بر اساس
filter-by-field: فیلتر بر اساس {field}
sort-by: مرتب‌سازی بر اساس
ascending: صعودی
descending: نزولی
all: همه
none: هیچ‌کدام
select-all: انتخاب همه
deselect-all: لغو انتخاب همه
show-more: نمایش بیشتر
show-less: نمایش کمتر
page: صفحه
of: از
total: مجموع
items-per-page: موارد در هر صفحه
showing: در حال نمایش
to: تا
results: نتایج
load-more: بارگذاری بیشتر
no-more-results: نتیجه دیگری وجود ندارد
today: امروز
yesterday: دیروز
tomorrow: فردا
day: روز
week: هفته
month: ماه
year: سال
date-range: بازه زمانی
start-date: تاریخ شروع
end-date: تاریخ پایان
time-zone: منطقه زمانی
system: سیستم
dashboard: داشبورد
home: خانه
analytics: تحلیل‌ها
reports: گزارش‌ها
logs: لاگ‌ها
help: کمک
support: پشتیبانی
contact: تماس
documentation: مستندات
language: زبان
timezone: منطقه زمانی
version: نسخه
theme: تم
light-mode: حالت روشن
dark-mode: حالت تیره
update-available: به‌روزرسانی در دسترس است
install-update: نصب به‌روزرسانی
maintenance-mode: حالت نگهداری
notification: اعلان
notifications: اعلان‌ها
mark-as-read: علامت‌گذاری به عنوان خوانده‌شده
mark-all-as-read: علامت‌گذاری همه به عنوان خوانده‌شده
clear-notifications: پاک کردن اعلان‌ها
company: شرکت
companies: شرکت‌ها
user: کاربر
users: کاربران
role: نقش
roles: نقش‌ها
permission: مجوز
permissions: مجوزها
group: گروه
groups: گروه‌ها
unauthorized: غیرمجاز
forbidden: ممنوع
resource-not-found: منبع پیدا نشد
server-error: خطای سرور
validation-error: خطای اعتبارسنجی
timeout-error: درخواست منقضی شد
network-error: خطای شبکه
unknown-error: خطای ناشناخته
file: فایل
files: فایل‌ها
folder: پوشه
folders: پوشه‌ها
upload-file: بارگذاری فایل
upload-files: بارگذاری فایل‌ها
upload-image: بارگذاری تصویر
upload-image-supported: پشتیبانی از PNG، JPEG و GIF
choose-file: انتخاب فایل
choose-files: انتخاب فایل‌ها
drag-and-drop: کشیدن و رها کردن
download-file: دانلود فایل
file-size: اندازه فایل
file-type: نوع فایل
confirm-delete: تأیید حذف
confirm-action: تأیید اقدام
confirm-exit: تأیید خروج
confirm-and-save-changes: تأیید و ذخیره تغییرات
are-you-sure-exit: آیا مطمئن هستید که می‌خواهید خارج شوید؟
unsaved-changes-warning: شما تغییرات ذخیره‌نشده دارید
connected: متصل
disconnected: قطع‌شده
reconnecting: در حال اتصال مجدد
connection-lost: اتصال قطع شد
connection-restored: اتصال برقرار شد
show: نمایش
hide: مخفی کردن
expand: گسترش
collapse: جمع کردن
enable: فعال کردن
disable: غیرفعال کردن
minimize: کوچک کردن
maximize: بزرگ کردن
restore: بازیابی
zoom-in: بزرگ‌نمایی
zoom-out: کوچک‌نمایی
fullscreen: تمام صفحه
exit-fullscreen: خروج از حالت تمام صفحه
yes: بله
no: خیر
ok: تأیید
none-available: هیچ‌کدام در دسترس نیست
default: پیش‌فرض
custom: سفارشی
general: عمومی
advanced: پیشرفته
placeholder-text: اینجا متن وارد کنید...
learn-more: اطلاعات بیشتر
view-resource: مشاهده {resource}
view-resource-details: مشاهده جزئیات {resource}
create-a-new-resource: ایجاد یک {resource} جدید
create-new-resource: ایجاد {resource} جدید
search-resource: جستجوی {resource}
new-resource: "{resource} جدید"
update-resource: به‌روزرسانی {resource}
save-resource-changes: ذخیره تغییرات {resource}
creating-resource: در حال ایجاد {resource}
cancel-resource: لغو {resource}
delete-resource: حذف {resource}
delete-resource-name: "حذف: {resourceName}"
delete-resource-named: حذف {resource} ({resourceName})
delete-resource-prompt: این اقدام قابل بازگشت نیست. پس از حذف، رکورد برای همیشه حذف خواهد شد
delete-cannot-be-undone: این اقدام قابل بازگشت نیست. پس از حذف، رکورد برای همیشه حذف خواهد شد
create-resource: ایجاد {resource}
edit-resource: ویرایش {resource}
edit-resource-details: ویرایش جزئیات {resource}
edit-resource-type-name: "ویرایش {resource}: {resourceName}"
edit-resource-name: "ویرایش: {resourceName}"
config: پیکربندی
select-field: انتخاب {field}
columns: ستون‌ها
metadata: متادیتا
meta: متا
resource-created-success: "{resource} جدید با موفقیت ایجاد شد"
resource-created-success-name: "{resource} جدید important! ({resourceName}) با موفقیت ایجاد شد"
resource-updated-success: "{resource} ({resourceName}) با موفقیت به‌روزرسانی شد"
resource-action-success: "{resource} ({resourceName}) با موفقیت {action} شد"
resource-deleted-success: "{resource} ({resourceName}) با موفقیت حذف شد"
resource-deleted: "{resource} ({resourceName}) حذف شد"
continue-without-saving: ادامه بدون ذخیره؟
continue-without-saving-prompt: شما تغییرات ذخیره‌نشده‌ای در این {resource} دارید. ادامه دادن باعث لغو آنها خواهد شد. برای ادامه کلیک کنید
resource:
alert: هشدار
alerts: هشدارها
brand: برند
brands: برندها
category: دسته‌بندی
categories: دسته‌بندی‌ها
chat-attachment: پیوست چت
chat-attachments: پیوست‌های چت
chat-channel: کانال چت
chat-channels: کانال‌های چت
chat-log: لاگ چت
chat-logs: لاگ‌های چت
chat-message: پیام چت
chat-messages: پیام‌های چت
chat-participant: شرکت‌کننده چت
chat-participants: شرکت‌کنندگان چت
chat-receipt: رسید چت
chat-receipts: رسیدهای چت
comment: نظر
comments: نظرات
company: شرکت
companies: شرکت‌ها
custom-field-value: مقدار فیلد سفارشی
custom-field-values: مقادیر فیلد سفارشی
custom-field: فیلد سفارشی
custom-fields: فیلدهای سفارشی
dashboard-widget: ویجت داشبورد
dashboard-widgets: ویجت‌های داشبورد
dashboard: داشبورد
dashboards: داشبوردها
extension: افزونه
extensions: افزونه‌ها
file: فایل
files: فایل‌ها
group: گروه
groups: گروه‌ها
notification: اعلان
notifications: اعلان‌ها
permission: مجوز
permissions: مجوزها
policy: سیاست
policies: سیاست‌ها
report: گزارش
reports: گزارش‌ها
role: نقش
roles: نقش‌ها
setting: تنظیم
settings: تنظیمات
transaction: تراکنش
transactions: تراکنش‌ها
user-device: دستگاه کاربر
user-devices: دستگاه‌های کاربر
user: کاربر
users: کاربران
dropzone:
file: فایل
drop-to-upload: برای بارگذاری رها کنید
invalid: نامعتبر
files-ready-for-upload: "{numOfFiles} آماده برای بارگذاری"
upload-images-videos: بارگذاری تصاویر و ویدئوها
upload-documents: بارگذاری اسناد
upload-documents-files: بارگذاری اسناد و فایل‌ها
upload-avatar-files: بارگذاری آواتارهای سفارشی
dropzone-supported-images-videos: فایل‌های تصویر و ویدئو را به این منطقه رها کنید
dropzone-supported-avatars: فایل‌های SVG یا PNG را رها کنید
dropzone-supported-files: فایل‌ها را به این منطقه رها کنید
or-select-button-text: یا فایل‌ها را برای بارگذاری انتخاب کنید
upload-queue: صف بارگذاری
uploading: در حال بارگذاری...
two-fa-enforcement-alert:
message: برای افزایش امنیت حساب کاربری خود، سازمان شما احراز هویت دو مرحله‌ای (2FA) را الزامی کرده است. 2FA را در تنظیمات حساب خود فعال کنید تا لایه امنیتی بیشتری داشته باشید
button-text: تنظیم 2FA
comment-thread:
publish-comment-button-text: انتشار نظر
publish-reply-button-text: انتشار پاسخ
reply-comment-button-text: پاسخ
edit-comment-button-text: ویرایش
delete-comment-button-text: حذف
comment-published-ago: "{createdAgo} پیش"
comment-input-placeholder: یک نظر جدید وارد کنید...
comment-reply-placeholder: پاسخ خود را وارد کنید...
comment-input-empty-notification: نمی‌توانید نظرات خالی منتشر کنید...
comment-min-length-notification: نظر باید حداقل 2 کاراکتر باشد
dashboard:
select-dashboard: انتخاب داشبورد
create-new-dashboard: ایجاد داشبورد جدید
create-a-new-dashboard: ایجاد یک داشبورد جدید
confirm-create-dashboard: ایجاد داشبورد!
edit-layout: ویرایش چیدمان
add-widgets: افزودن ویجت‌ها
delete-dashboard: حذف داشبورد
save-dashboard: ذخیره داشبورد
you-cannot-delete-this-dashboard: نمی‌توانید این داشبورد را حذف کنید
are-you-sure-you-want-delete-dashboard: آیا مطمئن هستید که می‌خواهید {dashboardName} را حذف کنید؟
dashboard-widget-panel:
widget-name: "ویجت {widgetName}"
select-widgets: انتخاب ویجت‌ها
close-and-save: بستن و ذخیره
filters-picker:
filters: فیلترها
filter-data: فیلتر داده‌ها
visible-column-picker:
select-viewable-columns: انتخاب ستون‌های قابل مشاهده
customize-columns: سفارشی‌سازی ستون‌ها
component:
file:
dropdown-label: اقدامات فایل
import-modal:
loading-message: در حال پردازش وارد کردن...
drop-upload: برای بارگذاری رها کنید
invalid: نامعتبر
ready-upload: آماده برای بارگذاری
upload-spreadsheets: بارگذاری صفحات گسترده
drag-drop: فایل‌های صفحه گسترده را به این منطقه رها کنید
button-text: یا صفحات گسترده را برای بارگذاری انتخاب کنید
spreadsheets: صفحات گسترده
upload-queue: صف بارگذاری
dropzone:
file: فایل
drop-to-upload: برای بارگذاری رها کنید
invalid: نامعتبر
files-ready-for-upload: "{numOfFiles} آماده برای بارگذاری"
upload-images-videos: بارگذاری تصاویر و ویدئوها
upload-documents: بارگذاری اسناد
upload-documents-files: بارگذاری اسناد و فایل‌ها
upload-avatar-files: بارگذاری آواتارهای سفارشی
dropzone-supported-images-videos: فایل‌های تصویر و ویدئو را به این منطقه رها کنید
dropzone-supported-avatars: فایل‌های SVG یا PNG را رها کنید
dropzone-supported-files: فایل‌ها را به این منطقه رها کنید
or-select-button-text: یا فایل‌ها را برای بارگذاری انتخاب کنید
upload-queue: صف بارگذاری
uploading: در حال بارگذاری...
two-fa-enforcement-alert:
message: برای افزایش امنیت حساب کاربری خود، سازمان شما احراز هویت دو مرحله‌ای (2FA) را الزامی کرده است. 2FA را در تنظیمات حساب خود فعال کنید تا لایه امنیتی بیشتری داشته باشید
button-text: تنظیم 2FA
comment-thread:
publish-comment-button-text: انتشار نظر
publish-reply-button-text: انتشار پاسخ
reply-comment-button-text: پاسخ
edit-comment-button-text: ویرایش
delete-comment-button-text: حذف
comment-published-ago: "{createdAgo} پیش"
comment-input-placeholder: یک نظر جدید وارد کنید...
comment-reply-placeholder: پاسخ خود را وارد کنید...
comment-input-empty-notification: نمی‌توانید نظرات خالی منتشر کنید...
comment-min-length-notification: نظر باید حداقل 2 کاراکتر باشد
dashboard:
select-dashboard: انتخاب داشبورد
create-new-dashboard: ایجاد داشبورد جدید
create-a-new-dashboard: ایجاد یک داشبورد جدید
confirm-create-dashboard: ایجاد داشبورد!
edit-layout: ویرایش چیدمان
add-widgets: افزودن ویجت‌ها
delete-dashboard: حذف داشبورد
save-dashboard: ذخیره داشبورد
you-cannot-delete-this-dashboard: نمی‌توانید این داشبورد را حذف کنید
are-you-sure-you-want-delete-dashboard: آیا مطمئن هستید که می‌خواهید {dashboardName} را حذف کنید؟
dashboard-widget-panel:
widget-name: "ویجت {widgetName}"
select-widgets: انتخاب ویجت‌ها
close-and-save: بستن و ذخیره
services:
dashboard-service:
create-dashboard-success-notification: داشبورد جدید `{dashboardName}` با موفقیت ایجاد شد
delete-dashboard-success-notification: داشبورد `{dashboardName}` حذف شد
auth:
verification:
header-title: تأیید حساب
title: ایمیل خود را تأیید کنید
message-text: "<strong>تقریباً تمام شد!</strong><br> ایمیل خود را برای دریافت کد تأیید بررسی کنید"
verification-code-text: کد تأییدی که از طریق ایمیل دریافت کرده‌اید را وارد کنید
verification-input-label: کد تأیید
verify-button-text: تأیید و ادامه
didnt-receive-a-code: هنوز کدی دریافت نکرده‌اید؟
not-sent:
message: هنوز کدی دریافت نکرده‌اید؟
alternative-choice: از گزینه‌های جایگزین زیر برای تأیید حساب خود استفاده کنید
resend-email: ارسال مجدد ایمیل
send-by-sms: ارسال از طریق پیامک
two-fa:
verify-code:
verification-code: کد تأیید
check-title: ایمیل یا تلفن خود را بررسی کنید
check-subtitle: ما یک کد تأیید برای شما ارسال کرده‌ایم. کد را در زیر وارد کنید تا فرآیند ورود کامل شود
expired-help-text: کد احراز هویت دو مرحله‌ای شما منقضی شده است. در صورت نیاز می‌توانید کد دیگری درخواست کنید
resend-code: ارسال مجدد کد
verify-code: تأیید کد
cancel-two-factor: لغو احراز هویت دو مرحله‌ای
invalid-session-error-notification: جلسه نامعتبر است. لطفاً دوباره امتحان کنید
verification-successful-notification: تأیید با موفقیت انجام شد!
verification-code-expired-notification: کد تأیید منقضی شده است. لطفاً کد جدیدی درخواست کنید
verification-code-failed-notification: تأیید ناموفق بود. لطفاً دوباره امتحان کنید
resend-code:
verification-code-resent-notification: کد تأیید جدید ارسال شد
verification-code-resent-error-notification: خطا در ارسال مجدد کد تأیید. لطفاً دوباره امتحان کنید
forgot-password:
success-message: ایمیل خود را برای ادامه بررسی کنید!
is-sent:
title: تقریباً تمام شد!
message: "<strong>ایمیل خود را بررسی کنید!</strong><br> ما یک لینک جادویی به ایمیل شما ارسال کرده‌ایم که به شما امکان بازنشانی رمز عبور را می‌دهد. این لینک در 15 دقیقه منقضی می‌شود"
not-sent:
title: رمز عبور خود را فراموش کرده‌اید؟
message: "<strong>نگران نباشید، ما از شما پشتیبانی می‌کنیم.</strong><br> ایمیلی که برای ورود به {appName} استفاده می‌کنید را وارد کنید و ما یک لینک امن برای بازنشانی رمز عبور برای شما ارسال خواهیم کرد"
form:
email-label: آدرس ایمیل شما
submit-button: خوب، لینک جادویی را برایم بفرست!
nevermind-button: بی‌خیال
login:
title: به حساب کاربری خود وارد شوید
no-identity-notification: آیا فراموش کردید ایمیل خود را وارد کنید؟
no-password-notification: آیا فراموش کردید رمز عبور خود را وارد کنید؟
unverified-notification: حساب شما برای ادامه باید تأیید شود
password-reset-required: برای ادامه، بازنشانی رمز عبور لازم است
failed-attempt:
message: "<strong>رمز عبور خود را فراموش کرده‌اید؟</strong><br> برای بازنشانی رمز عبور خود روی دکمه زیر کلیک کنید"
button-text: خوب، به من کمک کن تا بازنشانی کنم!
form:
email-label: آدرس ایمیل
password-label: رمز عبور
remember-me-label: مرا به خاطر بسپار
forgot-password-label: رمز عبور خود را فراموش کرده‌اید؟
sign-in-button: ورود
create-account-button: ایجاد حساب جدید
slow-connection-message: در حال تجربه مشکلات اتصال
reset-password:
success-message: رمز عبور شما بازنشانی شد! برای ادامه وارد شوید
invalid-verification-code: این لینک بازنشانی رمز عبور نامعتبر یا منقضی شده است
title: بازنشانی رمز عبور
form:
code:
label: کد بازنشانی شما
help-text: کد تأییدی که در ایمیل خود دریافت کرده‌اید
password:
label: رمز عبور جدید
help-text: برای ادامه، رمزی با حداقل 6 کاراکتر وارد کنید
confirm-password:
label: تأیید رمز عبور جدید
help-text: برای ادامه، رمزی با حداقل 6 کاراکتر وارد کنید
submit-button: بازنشانی رمز عبور
back-button: بازگشت
console:
create-or-join-organization:
modal-title: ایجاد یا پیوستن به یک سازمان
join-success-notification: شما به یک سازمان جدید پیوستید!
create-success-notification: شما یک سازمان جدید ایجاد کردید!
switch-organization:
modal-title: آیا مطمئن هستید که می‌خواهید سازمان را به {organizationName} تغییر دهید؟
modal-body: با تأیید، حساب شما همچنان وارد خواهد ماند، اما سازمان اصلی شما تغییر خواهد کرد
modal-accept-button-text: بله، می‌خواهم سازمان را تغییر دهم
success-notification: شما سازمان‌ها را تغییر دادید
account:
index:
upload-new: بارگذاری جدید
phone: شماره تلفن شما
photos: عکس‌ها
timezone: منطقه زمانی خود را انتخاب کنید
admin:
menu:
overview: مرور
organizations: سازمان‌ها
branding: برندینگ
2fa-config: پیکربندی 2FA
schedule-monitor: مانیتور برنامه
services: خدمات
mail: ایمیل
filesystem: سیستم فایل
queue: صف
socket: سوکت
push-notifications: اعلان‌های فشاری
schedule-monitor:
schedule-monitor: مانیتور برنامه
task-logs-for: "لاگ‌های وظایف برای:"
showing-last-count: نمایش آخرین {count} لاگ
name: نام
type: نوع
timezone: منطقه زمانی
last-started: آخرین شروع
last-finished: آخرین پایان
last-failure: آخرین شکست
date: تاریخ
memory: حافظه
runtime: زمان اجرا
output: خروجی
no-output: بدون خروجی
config:
database:
title: پیکربندی پایگاه داده
filesystem:
title: پیکربندی سیستم فایل
mail:
title: پیکربندی ایمیل
notification-channels:
title: پیکربندی اعلان‌های فشاری
queue:
title: پیکربندی صف
services:
title: پیکربندی خدمات
socket:
title: پیکربندی سوکت
branding:
title: برندینگ
icon-text: آیکون
upload-new: بارگذاری جدید
reset-default: بازنشانی به پیش‌فرض
logo-text: لوگو
theme: تم پیش‌فرض
index:
total-users: مجموع کاربران
total-organizations: مجموع سازمان‌ها
total-transactions: مجموع تراکنش‌ها
notifications:
title: اعلان‌ها
notification-settings: تنظیمات اعلان
organizations:
index:
title: سازمان‌ها
owner-name-column: مالک
owner-phone-column: تلفن مالک
owner-email-column: ایمیل مالک
users-count-column: کاربران
phone-column: تلفن
email-column: ایمیل
users:
title: کاربران
settings:
index:
title: تنظیمات سازمان
organization-name: نام سازمان
organization-description: توضیحات سازمان
organization-phone: شماره تلفن سازمان
organization-currency: ارز سازمان
organization-id: شناسه سازمان
organization-branding: برندینگ سازمان
logo: لوگو
logo-help-text: لوگو برای سازمان شما
upload-new-logo: بارگذاری لوگوی جدید
backdrop: پس‌زمینه
backdrop-help-text: بنر یا تصویر پس‌زمینه اختیاری برای سازمان شما
upload-new-backdrop: بارگذاری پس‌زمینه جدید
organization-timezone: منطقه زمانی پیش‌فرض برای سازمان خود را انتخاب کنید
select-timezone: انتخاب منطقه زمانی
extensions:
title: افزونه‌ها به زودی می‌آیند!
message: لطفاً در نسخه‌های آینده بررسی کنید، زیرا ما در حال آماده‌سازی برای راه‌اندازی مخزن و بازار افزونه‌ها هستیم
notifications:
select-all: انتخاب همه
mark-as-read: علامت‌گذاری به عنوان خوانده‌شده
received: "دریافت‌شده:"
message: هیچ اعلانی برای نمایش وجود ندارد
invite:
for-users:
invitation-message: شما به پیوستن به {companyName} دعوت شده‌اید
invitation-sent-message: شما به پیوستن به سازمان {companyName} در {appName} دعوت شده‌اید. برای پذیرش این دعوت، کد دعوت دریافت‌شده از طریق ایمیل را وارد کنید و روی ادامه کلیک کنید
invitation-code-sent-text: کد دعوت شما
accept-invitation-text: پذیرش دعوت
onboard:
index:
title: ایجاد حساب کاربری شما
welcome-title: "<strong>به {companyName} خوش آمدید!</strong><br />"
welcome-text: جزئیات مورد نیاز زیر را تکمیل کنید تا شروع کنید
full-name: نام کامل
full-name-help-text: نام کامل شما
your-email: آدرس ایمیل
your-email-help-text: آدرس ایمیل شما
phone: شماره تلفن
phone-help-text: شماره تلفن شما
organization-name: نام سازمان
organization-help-text: نام سازمان شما، تمام خدمات و منابع شما تحت این سازمان مدیریت خواهند شد، بعداً می‌توانید به تعداد دلخواه سازمان ایجاد کنید
password: یک رمز عبور وارد کنید
password-help-text: رمز عبور شما، مطمئن شوید که قوی است
confirm-password: رمز عبور خود را تأیید کنید
confirm-password-help-text: فقط برای تأیید رمز عبوری که در بالا وارد کرده‌اید
continue-button-text: ادامه
verify-email:
header-title: تأیید حساب
title: آدرس ایمیل خود را تأیید کنید
message-text: "<strong>تقریباً تمام شد!</strong><br> ایمیل خود را برای دریافت کد تأیید بررسی کنید"
verification-code-text: کد تأییدی که از طریق ایمیل دریافت کرده‌اید را وارد کنید
verification-input-label: کد تأیید
verify-button-text: تأیید و ادامه
didnt-receive-a-code: هنوز کدی دریافت نکرده‌اید؟
not-sent:
message: هنوز کدی دریافت نکرده‌اید؟
alternative-choice: از گزینه‌های جایگزین زیر برای تأیید حساب خود استفاده کنید
resend-email: ارسال مجدد ایمیل
send-by-sms: ارسال از طریق پیامک
install:
installer-header: نصب‌کننده
failed-message-sent: نصب ناموفق بود! برای تلاش مجدد روی دکمه زیر کلیک کنید
retry-install: تلاش مجدد برای نصب
start-install: شروع نصب
layout:
header:
menus:
organization:
settings: تنظیمات سازمان
create-or-join: ایجاد یا پیوستن به سازمان‌ها
explore-extensions: کاوش در افزونه‌ها
user:
view-profile: مشاهده پروفایل
keyboard-shortcuts: نمایش میانبرهای صفحه‌کلید
changelog: تغییرات

View File

@@ -0,0 +1,25 @@
# Docker Compose Override Example
# Copy this file to docker-compose.override.yml and customize for your environment
version: "3.8"
services:
application:
environment:
CONSOLE_HOST: http://localhost:4200
# Add your environment-specific variables here
MAIL_MAILER: smtp # or ses, mailgun, postmark, sendgrid
OSRM_HOST: https://router.project-osrm.org
# IPINFO_API_KEY: your_api_key
# GOOGLE_MAPS_API_KEY: your_api_key
# GOOGLE_MAPS_LOCALE: us
# TWILIO_SID: your_twilio_sid
# TWILIO_TOKEN: your_twilio_token
# TWILIO_FROM: your_twilio_phone
socket:
environment:
# DEVELOPMENT: Allow localhost connections (HTTP, HTTPS, and WebSocket protocols)
SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*,https://localhost:*,ws://localhost:*,wss://localhost:*"}'
# PRODUCTION: Replace with your actual domain(s) - include all protocols
# SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*,wss://yourdomain.com:*,https://app.yourdomain.com:*,wss://app.yourdomain.com:*"}'

View File

@@ -27,12 +27,16 @@ services:
socket:
image: socketcluster/socketcluster:v17.4.0
platform: linux/amd64
restart: unless-stopped
ports:
- "38000:8000"
environment:
SOCKETCLUSTER_WORKERS: 10
SOCKETCLUSTER_BROKERS: 10
# SOCKETCLUSTER_OPTIONS can be set via docker-compose.override.yml for specific environments
# For production, use: SOCKETCLUSTER_OPTIONS: '{"origins":"https://yourdomain.com:*"}'
# For development, use: SOCKETCLUSTER_OPTIONS: '{"origins":"http://localhost:*"}'
scheduler:
image: fleetbase/fleetbase-api:latest

View File

@@ -75,7 +75,7 @@ ENV QUEUE_CONNECTION=redis
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
ENV CONSOLE_PATH=/fleetbase/console
ENV OCTANE_SERVER=frankenphp
ENV FLEETBASE_VERSION=0.7.18
ENV FLEETBASE_VERSION=0.7.23
# Set environment
ARG ENVIRONMENT=production
@@ -158,14 +158,14 @@ CMD ["php", "artisan", "queue:work"]
# Application dev stage
FROM base AS app-dev
ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0 --watch"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0 --watch"]
# Application release stage
FROM base AS app-release
ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0"]
# Application stage
FROM base AS app
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=1000 --port=8000 --host=0.0.0.0"]

Some files were not shown because too many files have changed in this diff Show More