Compare commits

..

213 Commits

Author SHA1 Message Date
Ron
947565bcf0 Merge pull request #483 from fleetbase/dev-v0.7.24
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Fix: Critical cache key collision bug in ApiModelCache
2025-12-21 12:05:16 +08:00
Ronald A. Richardson
2d4cc5cf66 Fix: Critical cache key collision bug in ApiModelCache 2025-12-21 12:02:53 +08:00
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
Ronald A. Richardson
da420f0b4a ready for release 2025-11-10 10:49:29 +08:00
Ronald A. Richardson
e923a89719 v0.7.18 2025-11-10 10:35:47 +08:00
Ronald A. Richardson
0742603b43 added storefront release 2025-11-10 10:31:02 +08:00
Ronald A. Richardson
e1788a4ad6 v0.7.17 2025-11-10 10:29:26 +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
Ronald A. Richardson
a17aa3f5cc hotfix router map 2025-11-06 21:58:40 +08:00
Ron
0bf1a7fadd Merge pull request #459 from fleetbase/dev-v0.7.16
v0.7.16
2025-11-06 21:17:49 +08:00
Ronald A. Richardson
aa1ea2de89 Merge branch 'main' of github.com:fleetbase/fleetbase into dev-v0.7.16 2025-11-06 21:10:23 +08:00
Ronald A. Richardson
235f1ce80c upgraded dependencies 2025-11-06 20:45:42 +08:00
Ronald A. Richardson
5aa50504a4 updated RELEASE.md 2025-11-06 20:34:56 +08:00
Ronald A. Richardson
5d1b2e1939 - Made the LogApiRequests middleware more robust
- Fixed controller validation handling
- Added microsoft365/graph mail driver
- Improved password requirements (including breached password check)
- Patched creating duplicate users by email in IAM
- Patch env mapper
- Vehicle/driver tracking API doesnt fire resource lifecycle events or log requests - only tracking events
- Patched `<ModelCoordinatesInput />` component
- Security patch on Storefront customers API
- Styling updates on Storefront
2025-11-06 20:33:23 +08:00
Ron
fc5d90189c Merge pull request #456 from fleetbase/dev-v0.7.15
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.15
2025-11-01 14:08:41 +08:00
Ronald A. Richardson
2fee78e534 performed upgrades 2025-11-01 14:07:55 +08:00
Ronald A. Richardson
83fc794702 v0.7.15 2025-11-01 14:00:59 +08:00
Ron
66f669ad80 Merge pull request #454 from fleetbase/dev-v0.7.14
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.14
2025-10-30 18:53:33 +08:00
Ronald A. Richardson
a11b77592c fix console/package.json 2025-10-30 18:23:44 +08:00
Ronald A. Richardson
e5156829dc update release.md 2025-10-30 18:22:01 +08:00
Ronald A. Richardson
6cd7ddffcb v0.7.14 2025-10-30 18:20:04 +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
Ron
cbdf1d489b Merge pull request #447 from fleetbase/dev-v0.7.13
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.13 ~ Connectivity Module + Positions Playback + Positions & Device Events Drawer
2025-10-28 05:48:35 +08:00
Ronald A. Richardson
785bc55bb7 v0.7.13 ~ Connectivity Module + Positions Playback + Positions & Device Events Drawer 2025-10-28 05:39:54 +08:00
lapin
8a21593d9a Update docker-compose.yml 2025-10-24 15:21:42 +02:00
Ron
d171d02aac Merge pull request #446 from fleetbase/dev-v0.7.12
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.12 ~ Patches and Order Board Improvement
2025-10-22 12:38:56 +08:00
Ronald A. Richardson
dfd4ee37df pin linux build version 2025-10-22 12:38:24 +08:00
Ronald A. Richardson
27c063fbfb v0.7.12 ~ Patches and Order Board Improvement 2025-10-22 12:21:58 +08:00
Ron
8e85dcff83 Merge pull request #443 from fleetbase/dev-v0.7.11
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.11 ~ Maintenance, Reports, Telematics Upgrade/Features
2025-10-15 23:13:21 +08:00
Ronald A. Richardson
e38923c461 added RELEASE and updated README 2025-10-15 23:09:04 +08:00
Ronald A. Richardson
9911c96c09 All packages upgraded 2025-10-15 22:15:33 +08:00
Ronald A. Richardson
284c62cd06 v0.7.11 ~ Maintenance, Reports, Telematics Upgrade/Features 2025-09-30 13:49:34 +08:00
Ron
f8fd9f76fa Merge pull request #429 from fleetbase/dev-v0.7.10
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.10 - Maintenance capability overhaul, Storefront critical patches and improvements
2025-09-02 15:33:10 +08:00
Ronald A. Richardson
67aa793537 v0.7.10 - Maintenance capability overhaul, Storefront critical patches and improvements 2025-09-02 14:32:39 +08:00
Ron
5d0ae16cfd Merge pull request #424 from fleetbase/dev-v0.7.9
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.9 ~ shorter default data retention + template variable resolver + patched order vehicle update via api
2025-08-13 17:47:01 +08:00
Ronald A. Richardson
1d003ee31e v0.7.9 ~ shorter default data retention + template variable resolver + patched order vehicle update via api 2025-08-13 17:32:46 +08:00
Ron
9c9f3a994e Added one-click aws deploy details
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
2025-08-13 14:21:18 +08:00
Ron
b0ae302e81 Merge pull request #423 from fleetbase/dev-v0.7.8
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
v0.7.8 - Fix OSX build script, removed awsmp ECR publish
2025-08-12 19:01:15 +08:00
Ronald A. Richardson
205fcf1480 Optimized maintenance script, added linux/arm64 to docker image platforms 2025-08-12 18:53:23 +08:00
Ronald A. Richardson
23bf7c5ac8 v0.7.8 - Fix OSX build script, removed awsmp ECR publish 2025-08-11 13:37:00 +08:00
Ron
ada7e0df92 Merge pull request #422 from fleetbase/dev-v0.7.7
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.7 - Configurable rate limiting and maintenance patches
2025-08-09 19:14:21 +08:00
Ronald A. Richardson
f3bc42ace5 upgraded dependencies 2025-08-09 18:56:38 +08:00
Ronald A. Richardson
b91cbed080 v0.7.7 - Configurable rate limiting and maintenance patches 2025-08-09 16:40:59 +08:00
Ron
9870b11a71 Merge pull request #418 from fleetbase/feature/aws-marketplace-ecr-publish-job
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
fix ecr authentication step
2025-07-25 16:13:39 +08:00
Ronald A. Richardson
1d62dbca6b fix ecr authentication step 2025-07-25 16:11:26 +08:00
Ron
db3bf46a02 Merge pull request #417 from fleetbase/feature/aws-marketplace-ecr-publish-job
fix ecr publish add registry env variable
2025-07-25 15:25:47 +08:00
Ronald A. Richardson
ec053f1d13 fix ecr publish add registry env variable 2025-07-25 15:24:18 +08:00
Ron
030ec2494d Merge pull request #416 from fleetbase/feature/aws-marketplace-ecr-publish-job
added workflow job to publish to aws ecr for marketplace distribution
2025-07-25 15:07:13 +08:00
Ronald A. Richardson
fe56bcac85 formatted action workflow template 2025-07-25 15:06:40 +08:00
Ronald A. Richardson
8b118d1ad9 minor workflow tweak 2025-07-25 15:05:43 +08:00
Ronald A. Richardson
724c1b49ab added workflow job to publish to aws ecr for marketplace distribution 2025-07-25 15:04:06 +08:00
Ron
8e5b2e1ae3 Merge pull request #402 from fleetbase/dev-v0.7.6
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.6 ~ Minor patches, WIP multi-order route optimization
2025-06-04 12:46:10 +08:00
Ronald A. Richardson
e141d4d3a3 update release title 2025-06-04 12:32:47 +08:00
Ronald A. Richardson
ab2e102e28 v0.7.6 ~ Minor patches, WIP multi-order route optimization 2025-06-04 12:30:02 +08:00
Ron
723deff398 Merge pull request #401 from fleetbase/dev-v0.7.5
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.5 ~ Added route optimization and routing control registry and se…
2025-05-30 17:07:44 +08:00
Ronald A. Richardson
fd9adc3961 update composer.json 2025-05-30 17:07:03 +08:00
Ronald A. Richardson
4244a04052 upgraded fleetops 2025-05-30 16:57:31 +08:00
Ronald A. Richardson
e3c60a2232 fix release md typo 2025-05-30 16:15:44 +08:00
Ronald A. Richardson
1eaeb2c46e updated release file 2025-05-30 16:14:26 +08:00
Ronald A. Richardson
1d64d18b8b v0.7.5 ~ Added route optimization and routing control registry and settings & optimized environment/settings mapper 2025-05-30 16:10:56 +08:00
Ron
1124ecb56c Merge pull request #400 from fleetbase/dev-v0.7.4
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.4 ~ new docker install script, added logic condition property sh…
2025-05-26 15:54:09 +08:00
Ronald A. Richardson
672f3d51ca docker installer: added 12 sec delay before deploy script run 2025-05-26 15:46:05 +08:00
Ronald A. Richardson
cd5af8dfc8 added feature to wait database running in docker install script 2025-05-26 15:32:49 +08:00
Ronald A. Richardson
1a0073eae0 few tweaks to readme and install script 2025-05-26 15:20:48 +08:00
Ronald A. Richardson
d24b1d6fbe update release and readme 2025-05-26 14:59:20 +08:00
Ronald A. Richardson
ebbc4b2bf8 v0.7.4 ~ new docker install script, added logic condition property shortcut keys 2025-05-26 14:52:32 +08:00
Ron
b531c18d65 Merge pull request #399 from fleetbase/hotfix/ci-macos-binary-build
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
attempt to patch macos binary build ci
2025-05-24 14:03:54 +08:00
Ronald A. Richardson
fded8b24df attempt to patch macos binary build ci 2025-05-24 13:59:57 +08:00
Ron
98d082c780 Merge pull request #398 from fleetbase/dev-v0.7.3
v0.7.3 ~ hotfix: route optimization w/ no driver, seeder command
2025-05-24 13:26:23 +08:00
Ronald A. Richardson
d905943511 bump fleetops api version 2025-05-24 13:19:52 +08:00
Ronald A. Richardson
5c73b6e76d v0.7.3 ~ hotfix: route optimization w/ no driver, seeder command 2025-05-24 13:16:47 +08:00
Ron
cedf96fc97 Merge pull request #397 from fleetbase/dev-v0.7.2
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
v0.7.2 ~ route optimization patch, telemetry patch, network store management patch
2025-05-23 20:07:15 +08:00
Ronald A. Richardson
854fa2e680 fixed release.md date 2025-05-23 20:00:58 +08:00
Ronald A. Richardson
91b01c8a17 updated release info 2025-05-23 19:59:24 +08:00
Ronald A. Richardson
a4033db36c fixed fleetops route optimization, fixed network store management 2025-05-23 19:57:38 +08:00
Ronald A. Richardson
c54ef7fb30 v0.7.2 ~ Telemetry tweak patch 2025-05-22 15:19:01 +08:00
Ronald A. Richardson
b5ec15f0bb fix discord announcement workflow, attempt 4
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-05-22 12:47:27 +08:00
Ronald A. Richardson
1f609dd882 fix discord announcement workflow, attempt 3 2025-05-22 12:35:12 +08:00
Ronald A. Richardson
01883da5a2 attempt #2 to fix the discord announcement 2025-05-22 12:29:27 +08:00
Ronald A. Richardson
d2ab5b8a94 fix discord announcement workflow 2025-05-22 12:23:54 +08:00
Ronald A. Richardson
dca23f7e3f fix gh action workflows 2025-05-22 12:18:48 +08:00
Ron
d94dff7fbb Merge pull request #394 from fleetbase/dev-v0.7.1
v0.7.1 ~ Fleetbase console can now read in a runtime config
2025-05-22 12:09:47 +08:00
Ronald A. Richardson
e1ab6a3b11 use release.md for discord announcement 2025-05-22 12:06:58 +08:00
Ronald A. Richardson
c79fe67e44 ready to release, if macos doesnt build fix later 2025-05-22 11:59:09 +08:00
Ronald A. Richardson
d8adf42b24 revert back to previous curl patch 2025-05-22 11:51:05 +08:00
Ronald A. Richardson
80da5fe013 add SPC_OPT_DOWNLOAD_ARGS to attempt to fix osx build on gh runner 2025-05-22 11:25:45 +08:00
Ronald A. Richardson
06fd5e20e8 make osx build script gh runner friendly 2025-05-22 11:21:21 +08:00
Ronald A. Richardson
f04807de1e fixed upload to only run on release workflow, debug macos build on runer 2025-05-22 11:14:53 +08:00
Ronald A. Richardson
b7666eeb3e fix github workflows and setup to debug macos build 2025-05-22 10:45:37 +08:00
Ronald A. Richardson
dd895a0fd8 remove CURRENT_HASH file 2025-05-22 10:31:05 +08:00
Ronald A. Richardson
8c74c0fb99 release is almost ready 2025-05-22 10:30:10 +08:00
Ronald A. Richardson
92170c965e updated docker-compose to use latest images, patched osx binary build script, experimenting with artifact upload for binaries 2025-05-21 22:18:49 +08:00
Ronald A. Richardson
fcb3694874 added curl patch for gh runners 2025-05-21 21:33:45 +08:00
Ronald A. Richardson
aa46059bff minor tweak on php build of osx build script 2025-05-21 21:11:41 +08:00
Ronald A. Richardson
a5175bb11b fix php 8.4 detection in osx build script 2025-05-21 21:08:52 +08:00
Ronald A. Richardson
01816a1fe0 update osx build script to skip asdf install if php 8.4 is already installed 2025-05-21 21:02:04 +08:00
Ronald A. Richardson
15d500cd58 just install php via homebrew 2025-05-21 20:49:37 +08:00
Ronald A. Richardson
95d77a6ddd in osx build workflow use correct asdf commands 2025-05-21 20:45:46 +08:00
Ronald A. Richardson
eefc93e130 debug osx binary build workflow 2025-05-21 20:42:09 +08:00
Ronald A. Richardson
0f18ae85f1 debug osx binary build workflow 2025-05-21 20:35:22 +08:00
Ronald A. Richardson
a4812192da debug osx binary build workflow 2025-05-21 20:26:24 +08:00
Ronald A. Richardson
15d3c957b8 debug osx binary build workflow 2025-05-21 20:20:33 +08:00
Ronald A. Richardson
c2bd098d14 debug osx binary build workflow 2025-05-21 20:16:16 +08:00
Ronald A. Richardson
98511fd418 patch console route && debug osx action binary build 2025-05-21 20:13:14 +08:00
Ronald A. Richardson
225110c8dc attempt to patch binary build workflow 2025-05-21 19:39:04 +08:00
Ronald A. Richardson
1aa2a99763 added workflow to build fleetbase api binaries 2025-05-21 19:35:08 +08:00
Ron
6e888af772 Merge pull request #376 from fleetbase/feature/fleetbase-binary
Working static build script for a fleetbase binary (unix/linux/osx)
2025-05-21 19:24:49 +08:00
Ronald A. Richardson
d61205d898 added fleetbase config file, added ability to set tz for user and organization 2025-05-21 19:21:22 +08:00
Ronald A. Richardson
72078553cc remove hash tracking from docker build 2025-05-20 10:19:42 +08:00
Ronald A. Richardson
bfae04a645 attempt to fix current hash check in build 2025-05-20 10:16:43 +08:00
Ronald A. Richardson
c59f028755 fix docker build 2025-05-20 10:06:26 +08:00
Ronald A. Richardson
2b959db773 remove the current hash file 2025-05-19 19:36:05 +08:00
Ronald A. Richardson
a9354ccbfd removed EXPECTED_HASH checkin 2025-05-19 19:06:21 +08:00
Ronald A. Richardson
23e6d1e6b9 removed EXPECTED_HASH 2025-05-19 17:33:13 +08:00
Ronald A. Richardson
86da1bd095 Improvements to docker setups 2025-05-19 17:30:18 +08:00
Ronald A. Richardson
ae89600ae6 updated dockerhub publish workflow 2025-05-19 13:54:28 +08:00
Ronald A. Richardson
6697b79185 secured runtime config to only allow select config values to be set 2025-05-19 13:50:10 +08:00
Ronald A. Richardson
4dc9764853 v0.7.1 ~ Fleetbase console can now read in a runtime config 2025-05-19 13:12:57 +08:00
Ron
0626bc0171 Merge pull request #388 from fleetbase/dev-v0.7.0
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
v0.7.0 🛠️
2025-05-16 17:19:57 +08:00
Ronald A. Richardson
a8adf3fd84 Merge branch 'dev-v0.7.0' of github.com:fleetbase/fleetbase into dev-v0.7.0 2025-05-16 16:22:18 +08:00
Ronald A. Richardson
7b8bc4a593 removed old docker settings and github auth arg 2025-05-16 16:21:22 +08:00
Ron
490f2f1b41 Merge pull request #345 from nstankov-bg/feature/translate-bulgarian
feature/translate-bulgarian
2025-05-16 16:07:25 +08:00
Ron
e1fc7850d3 Merge pull request #385 from thawaba/add-arabic-language
Add Arabic language support
2025-05-16 16:06:44 +08:00
Ronald A. Richardson
cc278bf1bb * Patched fuel report creation/ fixed coordinates input implementation for fuel report
* Added bulk assign driver
* Improved performance for order dispatch/ bulk order dispatch/ bulk assign driver
* Added new columns to order export
* Added downloadable import templates for all importable resources via Import Modal
* Patched custom field rendering for order viewing
* Patched custom field values reset after order creation
* Added notification settings to FleetOps
* Added bulk search by ID or Tracking Number for Orders
* Patched all filters and filter indicator component
* Patched issue unable to select driver after selecting facilitator
* Fixed extension booting when not authenticated
* Fixed Internal ID rendering on order view
* Added ability to filter orders without a driver
2025-05-16 16:03:26 +08:00
aanmth
af86aaba8b Add Arabic language support 2025-05-15 05:42:17 +03:00
Ronald A. Richardson
f35dcb1544 fix: update package.json version v0.6.10
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
2025-05-08 20:06:27 +08:00
Ron
29c8f4340d Merge pull request #382 from fleetbase/dev-v0.6.10
Some checks are pending
Fleetbase CI / Build and Start Docker Services (push) Waiting to run
v0.6.10 ~ Added Product Update/Create API, Added `FRONTEND_HOSTS` ENV…
2025-05-08 12:33:02 +08:00
Ronald A. Richardson
1cb833e407 v0.6.10 ~ Added Product Update/Create API, Added FRONTEND_HOSTS ENV variable, other minor patches 2025-05-08 12:24:54 +08:00
Ronald A. Richardson
e372bc6396 minor update to linux build script 2025-05-07 14:32:17 +08:00
Ronald A. Richardson
2f432d148a Remove build artifacts; add dist & downloads to .gitignore 2025-05-07 14:26:57 +08:00
Ron
41bc6e39a7 Merge pull request #380 from fleetbase/dev-v0.6.9
Some checks failed
Fleetbase CI / Build and Start Docker Services (push) Has been cancelled
Enhancements and bug fixes for order workflow, labels, notifications,…
2025-05-01 12:15:36 +08:00
Ronald A. Richardson
5dbe2fb5bb Enhancements and bug fixes for order workflow, labels, notifications, and route optimization
- Added support for downloading labels and barcodes per package
- Fixed proof of delivery behavior to ensure accurate completion records
- Updated waypoint activity flow to rely on the `complete` flag
- Added support for setting waypoints as either pickup or dropoff
- Enabled sending notifications to order customer, driver, and facilitator
- Added events and notifications for `order.completed` and `order.failed` states
- Fixed route optimization logic and minor issues during order creation
- Normalized `meta` response structure to always return an object (never array)
- Patched issue with order config: deleting custom field categories no longer breaks config
2025-05-01 12:08:27 +08:00
Ronald A. Richardson
8f66bc12e4 Working static build script for a fleetbase binary (unix/linux) 2025-04-16 14:22:07 +08:00
Nikolay Stankov
1e331d70b1 feature/translate-bulgarian 2025-01-30 08:54:08 -05:00
233 changed files with 19988 additions and 7141 deletions

64
.github/workflows/build-binaries.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Build Fleetbase Binaries
on:
workflow_dispatch:
workflow_run:
workflows: ["Create Release"]
types: [completed]
permissions:
contents: write
env:
DIST_DIR: builds/dist
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build-linux:
name: Linux Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Linux binary
run: |
chmod +x ./builds/linux/build-linux.sh
./builds/linux/build-linux.sh
- name: Upload Linux binary
if: github.event_name == 'workflow_run'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.workflow_run.head_branch }}
files: |
${{ env.DIST_DIR }}/fleetbase-linux-x86_64
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos:
name: macOS (ARM64) Build
needs: build-linux
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install build dependencies
run: |
brew update
brew install autoconf automake coreutils asdf php@8.4
source "$(brew --prefix asdf)/libexec/asdf.sh"
asdf plugin add php https://github.com/asdf-community/asdf-php.git
- name: Build macOS binary
run: |
chmod +x ./builds/osx/build-osx.sh
./builds/osx/build-osx.sh
- name: Upload Linux binary
if: github.event_name == 'workflow_run'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.workflow_run.head_branch }}
files: |
${{ env.DIST_DIR }}/fleetbase-darwin-arm64
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -58,6 +58,43 @@ jobs:
files: |
./docker-bake.hcl
- name: Resolve ECS Targets
run: |
set -euo pipefail
# Detect naming scheme by checking if new cluster exists
NEW_CLUSTER="${PROJECT}-${STACK}-cluster"
if aws ecs describe-clusters --region "${AWS_REGION}" --clusters "${NEW_CLUSTER}" \
--query "clusters[?status=='ACTIVE'].clusterArn" --output text 2>/dev/null | grep -q .; then
# New scheme: use cluster suffix and service prefixes
CLUSTER="${NEW_CLUSTER}"
SERVICE_PREFIX="${PROJECT}-${STACK}-"
SERVICE_BASE="api"
else
# Legacy scheme: no suffixes/prefixes
CLUSTER="${PROJECT}-${STACK}"
SERVICE_PREFIX=""
SERVICE_BASE="app"
fi
# Build service names
API_SERVICE="${SERVICE_PREFIX}${SERVICE_BASE}"
SCHEDULER_SERVICE="${SERVICE_PREFIX}scheduler"
EVENTS_SERVICE="${SERVICE_PREFIX}events"
TASK_DEF="${PROJECT}-${STACK}-${SERVICE_BASE}"
# Get container name from task definition
CONTAINER_NAME="$(aws ecs describe-task-definition --task-definition "$TASK_DEF" \
--query 'taskDefinition.containerDefinitions[0].name' --output text 2>/dev/null || echo "$SERVICE_BASE")"
{
echo "CLUSTER=$CLUSTER"
echo "API_SERVICE=$API_SERVICE"
echo "SCHEDULER_SERVICE=$SCHEDULER_SERVICE"
echo "EVENTS_SERVICE=$EVENTS_SERVICE"
echo "TASK_DEF=$TASK_DEF"
echo "CONTAINER_NAME=$CONTAINER_NAME"
} >> "$GITHUB_ENV"
- name: Download ecs-tool
run: |
wget -O ecs-tool.tar.gz https://github.com/springload/ecs-tool/releases/download/1.9.6/ecs-tool_1.9.6_linux_amd64.tar.gz && tar -xvf ecs-tool.tar.gz ecs-tool
@@ -65,9 +102,21 @@ jobs:
- name: Deploy the images 🚀
run: |-
set -eu
# run deploy.sh script before deployments
env "ECS_RUN.SERVICE=app" "ECS_RUN.LAUNCH_TYPE=FARGATE" ./ecs-tool run -l "ecs-tool" --image_tag '{container_name}-${{ env.VERSION }}' --cluster ${{ env.PROJECT }}-${{ env.STACK }} --task_definition ${{ env.PROJECT }}-${{ env.STACK }}-app --container_name app ./deploy.sh
./ecs-tool deploy --image_tag '{container_name}-${{ env.VERSION }}' --cluster ${{ env.PROJECT }}-${{ env.STACK }} -s app -s scheduler -s events
# Run deploy.sh script before deployments
env "ECS_RUN.SERVICE=${API_SERVICE}" "ECS_RUN.LAUNCH_TYPE=FARGATE" \
./ecs-tool run -l "ecs-tool" \
--image_tag '{container_name}-${{ env.VERSION }}' \
--cluster "${CLUSTER}" \
--task_definition "${TASK_DEF}" \
--container_name "${CONTAINER_NAME}" \
./deploy.sh
# Deploy services
./ecs-tool deploy \
--image_tag '{container_name}-${{ env.VERSION }}' \
--cluster "${CLUSTER}" \
-s "${API_SERVICE}" -s "${SCHEDULER_SERVICE}" -s "${EVENTS_SERVICE}"
build_frontend:
name: Build and Deploy the Console
@@ -175,6 +224,11 @@ jobs:
set -u
DEPLOY_BUCKET=${STATIC_DEPLOY_BUCKET:-${{ env.PROJECT }}-${{ env.STACK }}}
NEW_BUCKET="${PROJECT}-${STACK}-console"
if aws s3api head-bucket --bucket "$NEW_BUCKET" 2>/dev/null; then
DEPLOY_BUCKET="$NEW_BUCKET"
fi
# this value will come from the dotenv above
echo "Deploying to $DEPLOY_BUCKET"
wget -O- https://github.com/bep/s3deploy/releases/download/v2.11.0/s3deploy_2.11.0_linux-amd64.tar.gz | tar xzv -f - s3deploy

21
.github/workflows/create-release.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Create Release
on:
push:
tags:
- 'v*'
jobs:
create:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body_path: RELEASE.md
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,61 @@
name: Discord Announcement
on:
workflow_run:
workflows: ["Create Release"]
types: [completed]
workflow_dispatch:
inputs:
tag:
description: "Release tag to announce (e.g. v0.7.1)"
required: true
jobs:
discord_announcement:
runs-on: ubuntu-latest
steps:
# 1⃣ Figure out which tag were talking about
- id: vars
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${{ github.event.workflow_run.head_branch }}"
fi
echo "TAG=$TAG" >> "$GITHUB_ENV"
# 2⃣ Check out the exact commit for that tag
- uses: actions/checkout@v3
with:
ref: ${{ env.TAG }}
fetch-depth: 1
# 3⃣ Stash RELEASE.md in an env var (one atomic write → no EOF error)
- id: prep-body
shell: bash
run: |
body=$(<RELEASE.md)
max=4000
[[ ${#body} -gt $max ]] && body="${body:0:$max}…" # add ellipsis if trimmed
{
echo "body<<EOF"
echo "$body"
echo "EOF"
} >> "$GITHUB_OUTPUT"
# 4⃣ Fire the webhook
- uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
username: Fleetbase
content: |
@everyone
📦 **Fleetbase ${{ env.TAG }} released!**
<https://github.com/${{ github.repository }}/releases/tag/${{ env.TAG }}>
embed-title: "Fleetbase ${{ env.TAG }} — release notes"
embed-url: "https://github.com/fleetbase/fleetbase/releases/tag/${{ env.TAG }}"
embed-description: ${{ steps.prep-body.outputs.body }}
embed-color: 4362730 # 0x4291EA (Fleetbase Blue)

View File

@@ -1,48 +0,0 @@
name: Discord Announcement
on:
push:
tags:
- "v*"
jobs:
discord_announcement:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Get tag message
id: tag
run: |
echo "::set-output name=version::$(git describe --tags --abbrev=0)"
if [[ "${ACT}" == "true" ]]; then
# If running with act, use an environment variable for the tag message
echo "::set-output name=message::$(echo -e "${TAG_MESSAGE}" | base64)"
else
# If running on GitHub, use git to get the tag message
echo "::set-output name=message::$(git tag -l --format='%(contents)' $(git describe --tags --abbrev=0) | base64)"
fi
- name: Print tag message
run: echo "${{ steps.tag.outputs.message }}"
- name: Get tag name
id: get_tag
run: echo "::set-output name=tag::${GITHUB_REF/refs\/tags\//}"
- name: Decode message
id: decode
run: |
echo "Decoding message..."
echo "::set-output name=message::$(echo '${{ steps.tag.outputs.message }}' | base64 --decode)"
- name: Send message to Discord
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: "@everyone \n📦 New Fleetbase Version ${{ steps.get_tag.outputs.tag }} Released!\n${{ steps.decode.outputs.message }} \nVersion: ${{ steps.get_tag.outputs.tag }} \n[Release Notes for ${{ steps.get_tag.outputs.tag }}](https://github.com/fleetbase/fleetbase/releases/tag/${{ steps.get_tag.outputs.tag }})"
username: Fleetbase

View File

@@ -1,4 +1,4 @@
name: Fleetbase CI/CD
name: Fleetbase GCP CI/CD
on:
push:

View File

@@ -0,0 +1,50 @@
name: Fleetbase Docker Images
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
branch:
description: 'Branch to build from'
required: false
default: 'main'
version:
description: 'Image version tag (e.g., v0.7.1-beta)'
required: false
jobs:
docker-release:
name: Build and Push Docker Images
runs-on: ubuntu-latest
env:
REGISTRY: fleetbase
VERSION: ${{ github.event.inputs.version || (github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && github.ref_name) || 'manual' }}
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.branch || github.ref_name }}
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push Console & API Images
uses: docker/bake-action@v2
with:
push: true
targets: |
fleetbase-console
fleetbase-api
files: |
./docker-bake.hcl

24
.gitignore vendored
View File

@@ -3,6 +3,7 @@
.env.backup
.phpunit.result.cache
.pnpm-store
.tool-versions
docker-compose.override.yml
npm-debug.log
yarn-error.log
@@ -16,6 +17,8 @@ api/composer.dev.json
api/composer-install-dev.sh
api/auth.json
api/crontab
api/go-crond
api/.fleetbase-id
act.sh
composer-auth.json
docker/database/*
@@ -31,7 +34,24 @@ packages/loconav
packages/internals
packages/projectargus-engine
packages/customer-portal
# wip
packages/solid
packages/aws-marketplace
packages/countries
packages/fliit
packages/samsara
packages/solid*
packages/valhalla
packages/vroom
solid
verdaccio
verdaccio
# asdf
.tools-versions
# binary build resources
builds/osx/frankenphp
# build artifacts
/builds/dist/
/builds/linux/spc/downloads/*
*.exe
*.dll
*.so
*.dylib

View File

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

110
README.md
View File

@@ -7,11 +7,11 @@
<p align="center" dir="auto">
Modular logistics and supply chain operating system
<br>
<a href="https://docs.fleetbase.io/" rel="nofollow">Documentation</a>
<a href="https://docs.fleetbase.io/" rel="nofollow" target="_fleetbase_docs">Documentation</a>
·
<a href="https://console.fleetbase.io" rel="nofollow">Cloud Version</a>
<a href="https://console.fleetbase.io" rel="nofollow" target="_fleetbase_console">Cloud Version</a>
·
<a href="https://fleetbase.apichecker.com" target="_api_status" rel="nofollow">API Status</a>
<a href="https://console.fleetbase.io/aws-marketplace" rel="nofollow" target="_aws_marketplace">Deploy on AWS</a>
·
<a href="https://tally.so/r/3NBpAW" rel="nofollow">Book a Demo</a>
·
@@ -25,23 +25,48 @@
Fleetbase is a modular logistics and supply chain operating system designed to streamline management, planning, optimization, and operational control across various sectors of the supply chain industry.
<p align="center" dir="auto">
<img src="https://github.com/fleetbase/fleetbase/assets/816371/125348c9-c88a-49fe-b098-9abec9d7dff8" alt="Fleetbase Console" width="1200" style="max-width: 100%;" />
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleetbase_overview.png" alt="Fleetbase Console" width="1200" style="max-width: 100%;" />
</p>
## Visual Feature Showcase
<p align="center" dir="auto">
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-board-kanban.png" alt="Fleetbase Order Board" width="1200" style="max-width: 100%;" />
<em>Visualize and manage your orders with a dynamic Kanban board.</em>
</p>
<p align="center" dir="auto">
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-workflow-config.png" alt="Fleetbase Order Workflow Configuration" width="1200" style="max-width: 100%;" />
<em>Create custom order flows and automation with the intuitive workflow builder.</em>
</p>
<p align="center" dir="auto">
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/order-map-view.png" alt="Fleetbase Order Map View" width="1200" style="max-width: 100%;" />
<em>Track individual orders in real-time on an interactive map.</em>
</p>
<p align="center" dir="auto">
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/live-map-tracking.png" alt="Fleetbase Live Map Tracking" width="1200" style="max-width: 100%;" />
<em>Get a complete overview of your fleet and active orders on a live map.</em>
</p>
<p align="center" dir="auto">
<img src="https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/fleet-map-zones.png" alt="Fleetbase Fleet Map with Zones" width="1200" style="max-width: 100%;" />
<em>Define and manage service areas and zones for your fleet.</em>
</p>
**Quickstart**
```bash
git clone git@github.com:fleetbase/fleetbase.git
cd fleetbase
docker-compose up -d
docker exec -ti fleetbase-application-1 bash
sh deploy.sh
cd fleetbase && ./scripts/docker-install.sh
```
## 📖 Table of contents
- [Features](#-features)
- [Install](#-install)
- [Deploy on AWS](#-deploy-on-aws-in-one-click)
- [Extensions](#-extensions)
- [Apps](#-apps)
- [Roadmap](#-roadmap)
@@ -75,10 +100,7 @@ Make sure you have both the latest versions of docker and docker-compose install
```bash
git clone git@github.com:fleetbase/fleetbase.git
cd fleetbase
docker-compose up -d
docker exec -ti fleetbase-application-1 bash
sh deploy.sh
cd fleetbase && ./scripts/docker-install.sh
```
### Accessing Fleetbase
@@ -89,17 +111,28 @@ Fleetbase API: http://localhost:8000
### Additional Configurations
**CORS:** If youre installing directly on a server you may need to add your IP address or domain to the `api/config/cors.php` file in the `allowed_hosts` array.
**CORS:** If youre installing directly on a server you will need to configure the environment variables to the application container:
```
CONSOLE_HOST=http://{yourhost}:4200
```
If you have additional applications or frontends you can use the environment variable `FRONTEND_HOSTS` to add a comma delimited list of additioal frontend hosts.
**Application Key** If you get an issue about a missing application key just run:
```bash
docker compose exec application bash -c "php artisan key:generate --show"
```
Next copy this value to the `APP_KEY` environment variable in the application container and restart.
**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:
CONSOLE_HOST: http://localhost:4200
MAIL_MAILER: (ses, smtp, mailgun, postmark, sendgrid)
OSRM_HOST: https://router.project-osrm.org
IPINFO_API_KEY:
@@ -108,11 +141,50 @@ services:
TWILIO_SID:
TWILIO_TOKEN:
TWILIO_FROM:
CONSOLE_HOST: http://localhost:4200
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
Deploy your complete Fleetbase logistics platform on AWS with enterprise-grade security, scalability, and reliability. No DevOps expertise required!
[![Deploy to AWS](https://img.shields.io/badge/Deploy%20to%20AWS-FF9900?style=for-the-badge&logo=amazon-aws&logoColor=white)](https://console.fleetbase.io/aws-marketplace)
### ✨ What You Get
- **Complete AWS Infrastructure**: ECS Fargate, RDS MySQL, ElastiCache Redis, S3, CloudFront, and more
- **25-Minute Setup**: From zero to production-ready logistics platform
- **Enterprise Security**: VPC isolation, encrypted storage, secrets management
- **Auto-Scaling**: Handle traffic spikes with ECS Fargate auto-scaling
- **High Availability**: Multi-AZ deployment with 99.9% uptime SLA
- **Cost Optimized**: Pay-as-you-use with optimized resource allocation
### 🏗️ Infrastructure Included
Your AWS deployment includes a complete, production-ready infrastructure stack:
- **Compute**: ECS Fargate cluster with auto-scaling services
- **Database**: RDS MySQL 8.0 with automated backups and Multi-AZ support
- **Cache**: ElastiCache Redis for high-performance caching
- **Storage**: S3 object storage with CloudFront CDN for global distribution
- **Networking**: VPC with private subnets, NAT gateways, and security groups
- **Load Balancing**: Application Load Balancer with SSL certificates
- **Monitoring**: CloudWatch logs, container insights, and health monitoring
- **Messaging**: SQS message queues for background job processing
[**🚀 Deploy Now**](https://console.fleetbase.io/aws-marketplace) | [**📖 Learn More**](https://docs.fleetbase.io/category/deploying/aws)
# 🧩 Extensions
Extensions are modular components that enhance the functionality of your Fleetbase instance. They allow you to add new features, customize existing behavior, or integrate with external systems.
@@ -145,9 +217,8 @@ Fleetbase offers a few open sourced apps which are built on Fleetbase which can
## 🛣️ Roadmap
1. **Inventory and Warehouse Management** ~ Pallet will be Fleetbases first official extension for WMS & Inventory.
2. **Accounting and Invoicing** ~ Ledger will be Fleetbases first official extension accounting and invoicing.
3. **Binary Builds** ~ Run Fleetbase from a single binary.
4. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
5. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
3. **AI** ~ AI Agent intrgation for system and workflows.
4. **Dynamic Rules System** ~ Trigger events, tasks jobs from a rule builder on resources.
## 🪲 Bugs and 💡 Feature Requests
@@ -183,3 +254,4 @@ Get updates on Fleetbase's development and chat with the project maintainers and
# License & Copyright
Fleetbase is made available under the terms of the <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">GNU Affero General Public License 3.0 (AGPL 3.0)</a>. For other licenses <a href="mailto:hello@fleetbase.io" target="_blank">contact us</a>.

40
RELEASE.md Normal file
View File

@@ -0,0 +1,40 @@
# 🚀 Fleetbase v0.7.24 — 2025-12-21
> "Critical core-api patches for cache key generation"
---
## ✨ Highlights
### Bug Fixes
- **Fixed cache key collision bug** - Different filter parameters (e.g., `type=customer` vs `type=contact`) now generate unique cache keys instead of returning wrong cached results
- **Fixed BadMethodCallException** - Models without soft deletes (like Permission) no longer crash when calling `getDeletedAtColumn()`
### Improvements
- **Added caching to Permission model** - Permission queries now benefit from Redis caching for improved performance
---
## ⚠️ Breaking Changes
- None 🙂
---
## 🔧 Upgrade Steps
```bash
# Pull latest version
git pull origin main --no-rebase
# Update docker
docker compose pull
docker compose down && docker compose up -d
# Run deploy script
docker compose exec application bash -c "./deploy.sh"
```
---
## Need help?
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,8 +40,6 @@ class Kernel extends HttpKernel
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

View File

@@ -2,10 +2,8 @@
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
@@ -17,17 +15,15 @@ class RouteServiceProvider extends ServiceProvider
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(
function () {
Route::get(
'/status',
function () {
'/health',
function (Request $request) {
return response()->json(
[
'status' => 'ok',
'time' => microtime(true) - LARAVEL_START
'time' => microtime(true) - $request->attributes->get('request_start_time')
]
);
}
@@ -35,19 +31,4 @@ class RouteServiceProvider extends ServiceProvider
}
);
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for(
'api',
function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
}
);
}
}

View File

@@ -7,31 +7,42 @@
"laravel"
],
"license": "AGPL-3.0-or-later",
"authors": [
{
"name": "Fleetbase Pte Ltd.",
"email": "hello@fleetbase.io"
},
{
"name": "Ronald A. Richardson",
"email": "ron@fleetbase.io"
}
],
"require": {
"php": "^8.0",
"php": ">=8.0 <=8.2.28",
"appstract/laravel-opcache": "^4.0",
"fleetbase/core-api": "^1.6.2",
"fleetbase/fleetops-api": "^0.6.5",
"fleetbase/registry-bridge": "^0.0.18",
"fleetbase/storefront-api": "^0.3.30",
"fleetbase/core-api": "^1.6.30",
"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",
"laravel/tinker": "^2.9",
"league/flysystem-aws-s3-v3": "^3.0",
"maatwebsite/excel": "^3.1",
"maennchen/zipstream-php": "3.1.2",
"phpoffice/phpspreadsheet": "^1.28",
"predis/predis": "^2.1",
"psr/http-factory-implementation": "*",
"resend/resend-php": "^0.14.0",
"s-ichikawa/laravel-sendgrid-driver": "^4.0",
"stripe/stripe-php": "13.13.0",
"symfony/mailgun-mailer": "^7.1",
"symfony/postmark-mailer": "^7.1"
},
"require-dev": {
"spatie/laravel-ignition": "^2.0",
"fakerphp/faker": "^1.9.1",
"kitloong/laravel-migrations-generator": "^6.10",
"laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
@@ -78,15 +89,6 @@
],
"clean-logs": [
"composer run-script clear-logs"
],
"dock": [
"docker exec -it fleetbase_os_application_1 /usr/bin/tmux -u new"
],
"dock-server": [
"docker exec -it fleetbase_os_httpd_1 /bin/sh"
],
"tunnel": [
"ngrok http --region=ap --hostname=fleetbase.ap.ngrok.io 8000"
]
},
"extra": {

3565
api/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ return [
|
*/
'env' => env('APP_ENV', 'production'),
'env' => env('APP_ENV', env('ENVIRONMENT', 'production')),
/*
|--------------------------------------------------------------------------

View File

@@ -17,11 +17,11 @@ return [
|
*/
'paths' => ['/*', 'sanctum/csrf-cookie'],
'paths' => ['*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST'))]),
'allowed_origins' => array_filter(['http://localhost:4200', env('CONSOLE_HOST'), Utils::addWwwToUrl(env('CONSOLE_HOST')), ...Utils::arrayFrom(env('FRONTEND_HOSTS', ''))]),
'allowed_origins_patterns' => [],

View File

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

View File

@@ -66,6 +66,18 @@ return [
'resend' => [],
'microsoft-graph' => [
'transport' => 'microsoft-graph',
'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),
'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),
'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@fleetbase.io'),
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Fleetbase')),
],
'save_to_sent_items' => env('MAIL_SAVE_TO_SENT_ITEMS', false),
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),

View File

@@ -1,5 +1,6 @@
<?php
use Fleetbase\Support\Utils;
use Laravel\Octane\Contracts\OperationTerminated;
use Laravel\Octane\Events\RequestHandled;
use Laravel\Octane\Events\RequestReceived;
@@ -104,8 +105,8 @@ return [
OperationTerminated::class => [
FlushOnce::class,
FlushTemporaryContainerInstances::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
DisconnectFromDatabases::class,
CollectGarbage::class,
],
WorkerErrorOccurred::class => [
@@ -192,6 +193,7 @@ return [
'routes',
'composer.lock',
'.env',
...Utils::arrayFrom(env('OCTANE_WATCH_DIRS'))
],
/*

View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة المصادقة
|--------------------------------------------------------------------------
|
| تحتوي الأسطر التالية على رسائل المصادقة التي نعرضها للمستخدم أثناء
| عمليات تسجيل الدخول أو غيرها. يمكنك تعديل هذه الرسائل حسب متطلباتك.
|
*/
'failed' => 'بيانات الاعتماد هذه غير متطابقة مع سجلاتنا.',
'password' => 'كلمة المرور التي تم إدخالها غير صحيحة.',
'throttle' => 'عدد كبير جداً من محاولات الدخول. يرجى المحاولة مرة أخرى خلال :seconds ثانية.',
];

View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة الترقيم الصفحي
|--------------------------------------------------------------------------
|
| تُستخدم الأسطر التالية من قبل مكتبة الترقيم الصفحي لبناء روابط
| الترقيم البسيطة. يمكنك تعديلها كما تشاء لتخصيص العرض بما يناسب
| تطبيقك بشكل أفضل.
|
*/
'previous' => '&laquo; السابق',
'next' => 'التالي &raquo;',
];

View File

@@ -0,0 +1,21 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| أسطر لغة إعادة تعيين كلمة المرور
|--------------------------------------------------------------------------
|
| الأسطر التالية هي الرسائل الافتراضية التي يقدمها نظام إعادة تعيين
| كلمة المرور عند فشل المحاولة، مثل رمز التحقق غير صالح أو كلمة مرور جديدة غير صحيحة.
|
*/
'reset' => 'تم إعادة تعيين كلمة المرور الخاصة بك!',
'sent' => 'لقد أرسلنا رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني!',
'throttled' => 'يرجى الانتظار قبل المحاولة مرة أخرى.',
'token' => 'رمز إعادة تعيين كلمة المرور هذا غير صالح.',
'user' => 'لا يمكننا العثور على مستخدم بهذا العنوان الإلكتروني.',
];

View File

@@ -0,0 +1,168 @@
<?php
return [
/*
|--------------------------------------------------------------------------<?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' => 'يجب قبول :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 صحيحًا أو خاطئًا.',
'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 صالحًا.',
/*
|--------------------------------------------------------------------------
| 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 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,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' => [],
];

View File

@@ -0,0 +1,47 @@
#!/bin/bash
set -e
# Resolve the directory the script is located in
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
APP_NAME="Fleetbase"
IMAGE_NAME="fleetbase-linux-static"
CONTAINER_NAME="fleetbase-linux-build"
DIST_DIR="$ROOT_DIR/builds/dist"
BINARY_NAME="fleetbase-linux-x86_64"
DOCKERFILE="$ROOT_DIR/builds/linux/static-build.Dockerfile"
# Ensure pkg-config archive is available
SPC_DOWNLOADS_DIR="$SCRIPT_DIR/spc/downloads"
PKG_TAR="pkg-config-0.29.2.tar.gz"
PKG_URL="https://static-php-cli.fra1.digitaloceanspaces.com/static-php-cli/deps/pkg-config/${PKG_TAR}"
if [[ ! -f "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" ]]; then
echo "📥 pkg-config archive missing downloading..."
mkdir -p "${SPC_DOWNLOADS_DIR}"
curl -L --retry 3 -o "${SPC_DOWNLOADS_DIR}/${PKG_TAR}" "${PKG_URL}"
else
echo "✅ pkg-config archive already present."
fi
# Build the image
echo "📦 Building static Linux binary for ${APP_NAME}..."
docker build -f "$DOCKERFILE" -t "$IMAGE_NAME" .
# Create a container from the built image
echo "📦 Creating container to extract binary..."
docker create --name "$CONTAINER_NAME" "$IMAGE_NAME"
# Make sure dist folder exist
mkdir -p "$DIST_DIR"
# Copy binary from container to local dist folder
echo "📂 Extracting binary..."
docker cp "$CONTAINER_NAME:/go/src/app/dist/frankenphp-linux-x86_64" "$DIST_DIR/$BINARY_NAME"
# Cleanup the temp container
docker rm "$CONTAINER_NAME"
echo "✅ Build complete! Binary is located at: $DIST_DIR/$BINARY_NAME"

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\linux\library;
class libgeos extends LinuxLibraryBase
{
use \SPC\builder\unix\library\libgeos;
public const NAME = 'libgeos';
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
trait libgeos
{
/**
* @throws FileSystemException
* @throws RuntimeException
*/
protected function build(): void
{
FileSystem::resetDir($this->source_dir . '/build');
shell()->cd($this->source_dir . '/build')
->setEnv([
'CFLAGS' => $this->getLibExtraCFlags(),
'LDFLAGS' => $this->getLibExtraLdFlags(),
'LIBS' => $this->getLibExtraLibs(),
])
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
->execWithEnv("make -j{$this->builder->concurrency}")
->execWithEnv('make install');
$this->patchPkgconfPrefix(['geos.pc']);
}
}

View File

@@ -0,0 +1,93 @@
# FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
FROM --platform=linux/amd64 docker.io/dunglas/frankenphp:static-builder@sha256:821526b776a26502735d83890cc0a0d579348c510ba6c777df0762cb1c50d967
WORKDIR /go/src/app
# Copy Fleetbase app
COPY ../../api ./dist/app
# Set working directory to the embedded Fleetbase app
WORKDIR /go/src/app/dist/app
# Setup for production environment
ENV APP_ENV=production
ENV APP_DEBUG=false
ENV BROADCAST_DRIVER=socketcluster
ENV OSRM_HOST="https://router.project-osrm.org"
ENV REGISTRY_PREINSTALLED_EXTENSIONS=true
# Optional: Ensure writable storage
RUN chmod -R 775 bootstrap/cache storage
# Set permissions for deploy script
RUN chmod +x ./deploy.sh
# Move back to main app directory before running build-static.sh
WORKDIR /go/src/app
# Install geos lib
RUN apk add --no-cache geos geos-dev
# Inject the libgeos library handlers
COPY ./builds/linux/spc/libgeos-linux.php ./dist/static-php-cli/src/SPC/builder/linux/library/libgeos.php
COPY ./builds/linux/spc/libgeos-unix.php ./dist/static-php-cli/src/SPC/builder/unix/library/libgeos.php
# Patch source.json to add geos extension source
RUN jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
# Pathc source.json to add libgeos library
RUN jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
./dist/static-php-cli/config/source.json > ./dist/static-php-cli/config/source.tmp.json && \
mv ./dist/static-php-cli/config/source.tmp.json ./dist/static-php-cli/config/source.json
# Patch ext.json to add geos extension dynamically
RUN jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
./dist/static-php-cli/config/ext.json > ./dist/static-php-cli/config/ext.tmp.json && \
mv ./dist/static-php-cli/config/ext.tmp.json ./dist/static-php-cli/config/ext.json
# Patch lib.json to add libgeos
RUN jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
./dist/static-php-cli/config/lib.json > ./dist/static-php-cli/config/lib.tmp.json && \
mv ./dist/static-php-cli/config/lib.tmp.json ./dist/static-php-cli/config/lib.json
# Install dependencies for SPC CLI
WORKDIR /go/src/app/dist/static-php-cli
RUN composer install --no-dev -a
# Set PHP extensions to be built (including geos!)
ENV PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
ENV PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
# Force SPC to use the local source version (not download binary)
ENV SPC_REL_TYPE=source
# Debug build
ENV SPC_LOG_LEVEL=debug
# Skip compression
ENV NO_COMPRESS=1
# set PHP version
ENV PHP_VERSION=8.2
# Move to the app directory
WORKDIR /go/src/app
# Make sure pkg-config is available within the static build container
COPY ./builds/linux/spc/downloads/pkg-config-0.29.2.tar.gz ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz
# Pre-build pkg-config using the existing tarball
RUN apk add --no-cache build-base && \
tar -xzf ./dist/static-php-cli/downloads/pkg-config-0.29.2.tar.gz -C /tmp && \
cd /tmp/pkg-config-0.29.2 && \
./configure --with-internal-glib --prefix=/go/src/app/dist/static-php-cli/build/bin && \
make && make install && \
rm -rf /tmp/pkg-config-0.29.2
# Do not run git pull
RUN sed -i 's/^[ \t]*git pull/# git pull/' ./build-static.sh
# Build the FrankenPHP static binary
RUN EMBED=dist/app ./build-static.sh

188
builds/osx/build-osx.sh Executable file
View File

@@ -0,0 +1,188 @@
#!/bin/bash
set -e
log() {
echo -e "\033[1;34m[🔧 $1]\033[0m"
}
log_success() {
echo -e "\033[1;32m[✅ $1]\033[0m"
}
log_warn() {
echo -e "\033[1;33m[⚠️ $1]\033[0m"
}
log_error() {
echo -e "\033[1;31m[❌ $1]\033[0m"
}
# Define base paths
log "Resolving directories..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
OSX_DIR="$ROOT_DIR/builds/osx"
DIST_DIR="$ROOT_DIR/builds/dist"
APP_DIR="$ROOT_DIR/api"
BREW_PREFIX="/opt/homebrew"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
BINARY_NAME="fleetbase-$OS-$ARCH"
log "Binary will be: $BINARY_NAME"
# Setup PHP 8.4
log "Detecting current PHP version..."
ORIGINAL_PHP_PATH="$(which php)"
ORIGINAL_PHP_VERSION="$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION.".".PHP_RELEASE_VERSION;' 2>/dev/null)"
IS_ASDF_MANAGED=false
if [[ "$ORIGINAL_PHP_PATH" == *".asdf"* ]]; then
IS_ASDF_MANAGED=true
fi
# 🔁 Trap to restore PHP when script exits
trap 'if [ "$IS_ASDF_MANAGED" = true ]; then
log "Restoring asdf-managed PHP version: $ORIGINAL_PHP_VERSION"
asdf set php "$ORIGINAL_PHP_VERSION" || true
log "Reverted to PHP $(php -v | head -n 1)"
else
log "Unsetting asdf set to restore system PHP"
asdf set php system || true
log "Reverted to PHP $(php -v | head -n 1)"
fi' EXIT
log "Detected PHP version: $ORIGINAL_PHP_VERSION"
log "Detected PHP binary: $ORIGINAL_PHP_PATH"
# ───────────────────────────────────────────────────────────────────────────────
# If the *current* php is already 8.4.x, we skip the entire asdf install step
# ───────────────────────────────────────────────────────────────────────────────
if [[ "$ORIGINAL_PHP_PATH" == "$BREW_PREFIX/bin/php" && "$ORIGINAL_PHP_VERSION" =~ ^8\.4\. ]]; then
log "Homebrew PHP $ORIGINAL_PHP_VERSION detected at $ORIGINAL_PHP_PATH — skipping asdf build/install."
else
# Only install under asdf if we dont already have 8.4.0 installed
log "No Homebrew PHP 8.4 detected (found $ORIGINAL_PHP_PATH $ORIGINAL_PHP_VERSION), using asdf to build/install."
if ! asdf list php | grep -q "8.4.0"; then
# Use brew to install required dependencies for asdf php management
log "Checking and installing Homebrew packages required for PHP 8.4 build..."
for pkg in autoconf automake bison freetype gd gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip pkg-config re2c zlib sqlite3 libsodium oniguruma openssl@3 nasm; do
if ! brew list "$pkg" &>/dev/null; then
log_warn "$pkg not found. Installing..."
arch -arm64 brew install "$pkg"
else
log "$pkg already installed. Skipping."
fi
done
# Set necessary env flags/paths for PHP build on OSX ARM64
export CPPFLAGS="-I$BREW_PREFIX/opt/oniguruma/include -I$BREW_PREFIX/opt/libsodium/include -I$BREW_PREFIX/opt/bzip2/include -I$BREW_PREFIX/opt/zlib/include -I$BREW_PREFIX/opt/openssl@3/include -I$BREW_PREFIX/opt/libxml2/include -I$BREW_PREFIX/opt/libedit/include -I$BREW_PREFIX/opt/curl/include -I$BREW_PREFIX/opt/sqlite3/include -I$BREW_PREFIX/opt/freetype/include -I$BREW_PREFIX/opt/jpeg/include -I$BREW_PREFIX/opt/libpng/include -I$BREW_PREFIX/opt/libzip/include"
export LDFLAGS="-L$BREW_PREFIX/opt/openssl@3/lib -lssl -lcrypto -lz -L$BREW_PREFIX/opt/oniguruma/lib -lonig -L$BREW_PREFIX/opt/libsodium/lib -lsodium -L$BREW_PREFIX/opt/bzip2/lib -Wl,-rpath,$BREW_PREFIX/opt/bzip2/lib -lbz2 -L$BREW_PREFIX/opt/zlib/lib -L$BREW_PREFIX/opt/openssl@3/lib -L$BREW_PREFIX/opt/libxml2/lib -L$BREW_PREFIX/opt/libedit/lib -L$BREW_PREFIX/opt/sqlite3/lib -lsqlite3 -L$BREW_PREFIX/opt/curl/lib -lcurl -L$BREW_PREFIX/opt/freetype/lib -L$BREW_PREFIX/opt/jpeg/lib -L$BREW_PREFIX/opt/libpng/lib -L$BREW_PREFIX/opt/libzip/lib -lzip -lz"
export PKG_CONFIG_PATH="$BREW_PREFIX/opt/openssl/lib/pkgconfig:$BREW_PREFIX/opt/oniguruma/lib/pkgconfig:$BREW_PREFIX/opt/libsodium/lib/pkgconfig:$BREW_PREFIX/opt/libzip/lib/pkgconfig:$BREW_PREFIX/opt/gd/lib/pkgconfig:$BREW_PREFIX/opt/zlib/lib/pkgconfig:$BREW_PREFIX/opt/openssl@3/lib/pkgconfig:$BREW_PREFIX/opt/libxml2/lib/pkgconfig:$BREW_PREFIX/opt/curl/lib/pkgconfig:$BREW_PREFIX/opt/sqlite3/lib/pkgconfig:$BREW_PREFIX/opt/freetype/lib/pkgconfig:$BREW_PREFIX/opt/jpeg/lib/pkgconfig:$BREW_PREFIX/opt/libpng/lib/pkgconfig"
export PHP_CONFIGURE_OPTIONS="--with-openssl=$(brew --prefix openssl) --with-iconv=$(brew --prefix libiconv)"
log "Installing PHP 8.4.0 with asdf..."
asdf install php 8.4.0 --verbose
else
log "asdf already has PHP 8.4.0 installed, skipping"
fi
log "Switching to PHP 8.4.0 with asdf set..."
asdf set php 8.4.0 --home
fi
# Clone FrankenPHP
if [ ! -d "$OSX_DIR/frankenphp" ]; then
log "Cloning FrankenPHP..."
git clone https://github.com/dunglas/frankenphp "$OSX_DIR/frankenphp"
else
log_warn "FrankenPHP already cloned. Skipping."
fi
cd "$OSX_DIR/frankenphp"
# Patch build script
log "Patching build-static.sh to skip git pull..."
sed -i '' 's/^[ \t]*git pull/# git pull/' ./build-static.sh
# Set environment variables
log "Exporting build environment variables..."
export PHP_VERSION=8.2
export PHP_EXTENSIONS="pdo_mysql,gd,bcmath,redis,intl,zip,gmp,apcu,opcache,imagick,sockets,pcntl,geos,iconv,mbstring,fileinfo,ctype,tokenizer,simplexml,dom,filter,session"
export PHP_EXTENSION_LIBS="libgeos,libzip,bzip2,libxml2,openssl,zlib"
export SPC_REL_TYPE=source
export NO_COMPRESS=1
export SPC_OPT_BUILD_ARGS="--debug"
export CMAKE_OSX_ARCHITECTURES=arm64
# Clone and prepare static-php-cli in dist/
STATIC_PHP_CLI_DIR="$OSX_DIR/frankenphp/dist/static-php-cli"
if [ ! -d "$STATIC_PHP_CLI_DIR" ]; then
log "Cloning static-php-cli into dist/..."
git clone --depth 1 --branch 2.5.2 https://github.com/crazywhalecc/static-php-cli.git "$STATIC_PHP_CLI_DIR"
else
log_warn "static-php-cli already exists in dist/. Skipping clone."
fi
# Inject libgeos support
log "Injecting libgeos patch files..."
cp "$ROOT_DIR/builds/osx/spc/libgeos-unix.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/library/libgeos.php"
cp "$ROOT_DIR/builds/osx/spc/libgeos-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/macos/library/libgeos.php"
cp "$ROOT_DIR/builds/osx/spc/UnixBuilderBase-macos.php" "$STATIC_PHP_CLI_DIR/src/SPC/builder/unix/UnixBuilderBase.php"
# Patch SPC config
log "Patching SPC config files (source.json, ext.json, lib.json)..."
jq '. + {"php-geos": {"type": "url", "url": "https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip", "license": [{"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/MIT-LICENSE"}, {"type": "file", "path": "php-geos-dfe1ab17b0f155cc315bc13c75689371676e02e1/LGPL-2"}]}}' \
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
jq '. + {"libgeos": {"type": "url", "url": "https://download.osgeo.org/geos/geos-3.12.1.tar.bz2", "filename": "geos-3.12.1.tar.bz2", "extract": "geos-3.12.1", "build-dir": "build", "license": [{"type": "file", "path": "COPYING"}]}}' \
"$STATIC_PHP_CLI_DIR/config/source.json" > "$STATIC_PHP_CLI_DIR/config/source.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/source.tmp.json" "$STATIC_PHP_CLI_DIR/config/source.json"
jq '. + {"libgeos": {"source": "libgeos", "static-libs-unix": ["libgeos.a", "libgeos_c.a"]}}' \
"$STATIC_PHP_CLI_DIR/config/lib.json" > "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/lib.tmp.json" "$STATIC_PHP_CLI_DIR/config/lib.json"
jq '. + {"geos": {"type": "external", "arg-type": "enable", "source": "php-geos", "lib-depends": ["libgeos"]}}' \
"$STATIC_PHP_CLI_DIR/config/ext.json" > "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" && \
mv "$STATIC_PHP_CLI_DIR/config/ext.tmp.json" "$STATIC_PHP_CLI_DIR/config/ext.json"
# Prepare app embed folder
log "📦 Preparing embedded app directory..."
rm -rf ./dist/app
mkdir -p ./dist/app
cp -R "$APP_DIR"/* ./dist/app/
log "Patching build-static.sh to skip git pull and composer install..."
# Skip `git pull`
sed -i '' 's/^[[:space:]]*git pull/# git pull/' "$OSX_DIR/frankenphp/build-static.sh"
# Patch add CoreServices framework for Caddy build on OSX
sed -i '' 's/-framework CoreFoundation -framework SystemConfiguration/& -framework CoreServices/' "$OSX_DIR/frankenphp/build-static.sh"
# ── work around 403 on GH macOS runners ────────────────────────────────────────
log "Patching curl to use a browser-like User-Agent (to avoid 403s)…"
curl() {
command curl -sSL -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15" "$@"
}
export -f curl
# Build the binary
log "⚙️ Running FrankenPHP build-static.sh..."
EMBED=dist/app ./build-static.sh
# Move built binary to dist
log "Moving built binary to output folder..."
mkdir -p "$DIST_DIR"
mv dist/frankenphp-mac-$ARCH "$DIST_DIR/$BINARY_NAME"
log_success "✅ macOS binary built at: $DIST_DIR/$BINARY_NAME"
# Clean up frankenphp build and app embed folder
log "🧹 Cleaning temporary app directory..."
rm -rf "$OSX_DIR/frankenphp"

View File

@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix;
use SPC\builder\BuilderBase;
use SPC\builder\linux\LinuxBuilder;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\util\DependencyUtil;
use SPC\util\SPCConfigUtil;
abstract class UnixBuilderBase extends BuilderBase
{
/** @var string cflags */
public string $arch_c_flags;
/** @var string C++ flags */
public string $arch_cxx_flags;
/** @var string cmake toolchain file */
public string $cmake_toolchain_file;
/**
* @throws WrongUsageException
* @throws FileSystemException
*/
public function getAllStaticLibFiles(): array
{
$libs = [];
// reorder libs
foreach ($this->libs as $lib) {
foreach ($lib->getDependencies() as $dep) {
$libs[] = $dep;
}
$libs[] = $lib;
}
$libFiles = [];
$libNames = [];
// merge libs
foreach ($libs as $lib) {
if (!in_array($lib::NAME, $libNames, true)) {
$libNames[] = $lib::NAME;
array_unshift($libFiles, ...$lib->getStaticLibs());
}
}
return array_map(fn ($x) => realpath(BUILD_LIB_PATH . "/{$x}"), $libFiles);
}
/**
* Return generic cmake options when configuring cmake projects
*/
public function makeCmakeArgs(): string
{
$extra = $this instanceof LinuxBuilder ? '-DCMAKE_C_COMPILER=' . getenv('CC') . ' ' : '';
// NEW: allow env-variable override
$arch = getenv('CMAKE_OSX_ARCHITECTURES') ?: 'arm64';
return $extra .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'-DCMAKE_INSTALL_BINDIR=bin ' .
'-DCMAKE_INSTALL_LIBDIR=lib ' .
'-DCMAKE_INSTALL_INCLUDEDIR=include ' .
"-DCMAKE_OSX_ARCHITECTURES={$arch} " .
"-DCMAKE_TOOLCHAIN_FILE={$this->cmake_toolchain_file}";
}
/**
* Generate configure flags
*/
public function makeAutoconfFlags(int $flag = AUTOCONF_ALL): string
{
$extra = '';
// TODO: add auto pkg-config support
if (($flag & AUTOCONF_LIBS) === AUTOCONF_LIBS) {
$extra .= 'LIBS="' . BUILD_LIB_PATH . '" ';
}
if (($flag & AUTOCONF_CFLAGS) === AUTOCONF_CFLAGS) {
$extra .= 'CFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
}
if (($flag & AUTOCONF_CPPFLAGS) === AUTOCONF_CPPFLAGS) {
$extra .= 'CPPFLAGS="-I' . BUILD_INCLUDE_PATH . '" ';
}
if (($flag & AUTOCONF_LDFLAGS) === AUTOCONF_LDFLAGS) {
$extra .= 'LDFLAGS="-L' . BUILD_LIB_PATH . '" ';
}
return $extra;
}
public function proveLibs(array $sorted_libraries): void
{
// search all supported libs
$support_lib_list = [];
$classes = FileSystem::getClassesPsr4(
ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library',
'SPC\builder\\' . osfamily2dir() . '\library'
);
foreach ($classes as $class) {
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
$support_lib_list[$class::NAME] = $class;
}
}
// if no libs specified, compile all supported libs
if ($sorted_libraries === [] && $this->isLibsOnly()) {
$libraries = array_keys($support_lib_list);
$sorted_libraries = DependencyUtil::getLibs($libraries);
}
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
}
$lib = new ($support_lib_list[$library])($this);
$this->addLib($lib);
}
// calculate and check dependencies
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
$this->lib_list = $sorted_libraries;
}
/**
* Sanity check after build complete
*
* @throws RuntimeException
*/
protected function sanityCheck(int $build_target): void
{
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('running cli sanity check');
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "echo \"hello\";"');
$raw_output = implode('', $output);
if ($ret !== 0 || trim($raw_output) !== 'hello') {
throw new RuntimeException("cli failed sanity check: ret[{$ret}]. out[{$raw_output}]");
}
foreach ($this->getExts(false) as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
$ext->runCliCheckUnix();
}
}
// sanity check for phpmicro
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
$test_task = $this->getMicroTestTasks();
foreach ($test_task as $task_name => $task) {
$test_file = SOURCE_PATH . '/' . $task_name . '.exe';
if (file_exists($test_file)) {
@unlink($test_file);
}
file_put_contents($test_file, file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') . $task['content']);
chmod($test_file, 0755);
[$ret, $out] = shell()->execWithResult($test_file);
foreach ($task['conditions'] as $condition => $closure) {
if (!$closure($ret, $out)) {
$raw_out = trim(implode('', $out));
throw new RuntimeException("micro failed sanity check: {$task_name}, condition [{$condition}], ret[{$ret}], out[{$raw_out}]");
}
}
}
}
// sanity check for embed
if (($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) {
logger()->info('running embed sanity check');
$sample_file_path = SOURCE_PATH . '/embed-test';
if (!is_dir($sample_file_path)) {
@mkdir($sample_file_path);
}
// copy embed test files
copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c');
copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php');
$util = new SPCConfigUtil($this);
$config = $util->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}";
if (PHP_OS_FAMILY === 'Linux' && getenv('SPC_LIBC') === 'musl') {
$lens .= ' -static';
}
[$ret, $out] = shell()->cd($sample_file_path)->execWithResult(getenv('CC') . ' -o embed embed.c ' . $lens);
if ($ret !== 0) {
throw new RuntimeException('embed failed sanity check: build failed. Error message: ' . implode("\n", $out));
}
// if someone changed to --enable-embed=shared, we need to add LD_LIBRARY_PATH
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
$ext_path = 'LD_LIBRARY_PATH=' . BUILD_ROOT_PATH . '/lib:$LD_LIBRARY_PATH ';
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.a');
} else {
$ext_path = '';
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/lib/libphp.so');
}
[$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output));
}
}
}
/**
* 将编译好的二进制文件发布到 buildroot
*
* @param int $type 发布类型
* @throws RuntimeException
* @throws FileSystemException
*/
protected function deployBinary(int $type): bool
{
$src = match ($type) {
BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php',
BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx',
BUILD_TARGET_FPM => SOURCE_PATH . '/php-src/sapi/fpm/php-fpm',
default => throw new RuntimeException('Deployment does not accept type ' . $type),
};
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file');
FileSystem::createDir(BUILD_ROOT_PATH . '/bin');
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '/bin/'));
return true;
}
/**
* Run php clean
*
* @throws RuntimeException
*/
protected function cleanMake(): void
{
logger()->info('cleaning up');
shell()->cd(SOURCE_PATH . '/php-src')->exec('make clean');
}
/**
* Patch phpize and php-config if needed
* @throws FileSystemException
*/
protected function patchPhpScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
logger()->debug('Patching php-config prefix and libs order');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\macos\library;
class libgeos extends MacOSLibraryBase
{
use \SPC\builder\unix\library\libgeos;
public const NAME = 'libgeos';
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\store\FileSystem;
trait libgeos
{
/**
* @throws FileSystemException
* @throws RuntimeException
*/
protected function build(): void
{
FileSystem::resetDir($this->source_dir . '/build');
shell()->cd($this->source_dir . '/build')
->setEnv([
'CFLAGS' => $this->getLibExtraCFlags(),
'LDFLAGS' => $this->getLibExtraLdFlags(),
'LIBS' => $this->getLibExtraLibs(),
])
->execWithEnv("cmake {$this->builder->makeCmakeArgs()} -DBUILD_SHARED_LIBS=OFF ..")
->execWithEnv("make -j{$this->builder->concurrency}")
->execWithEnv('make install');
$this->patchPkgconfPrefix(['geos.pc']);
}
}

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

@@ -6,19 +6,19 @@ WORKDIR /console
# Create the pnpm directory and set the PNPM_HOME environment variable
RUN mkdir -p ~/.pnpm
ENV PNPM_HOME /root/.pnpm
ENV PNPM_HOME=/root/.pnpm
# Set environment
ARG ENVIRONMENT=production
# Add the pnpm global bin to the PATH
ENV PATH /root/.pnpm/bin:$PATH
ENV PATH=/root/.pnpm/bin:$PATH
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
COPY console/package.json console/pnpm-lock.yaml ./
COPY package.json pnpm-lock.yaml ./
# Copy over .npmrc if applicable
COPY console/.npmr[c] ./
COPY .npmr[c] ./
# Install global dependencies
RUN npm install -g ember-cli pnpm
@@ -33,7 +33,7 @@ RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN pnpm install
# Copy the console directory contents into the container at /console
COPY console .
COPY . .
# Build the application
RUN pnpm build --environment $ENVIRONMENT
@@ -48,7 +48,7 @@ COPY --from=builder /console/dist /usr/share/nginx/html
EXPOSE 4200
# Use custom nginx.conf
COPY console/nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,57 +0,0 @@
# ---- Build Stage ----
FROM node:18.15.0-alpine
# Set the working directory in the container to /console
WORKDIR /console
# Create the pnpm directory and set the PNPM_HOME environment variable
RUN mkdir -p ~/.pnpm
ENV PNPM_HOME /root/.pnpm
# Set environment
ARG ENVIRONMENT=production
# Add the pnpm global bin to the PATH
ENV PATH /root/.pnpm/bin:$PATH
# Copy pnpm-lock.yaml (or package.json) into the directory /console in the container
COPY console/package.json console/pnpm-lock.yaml ./
# Copy over .npmrc if applicable
COPY console/.npmr[c] ./
# Install global dependencies
RUN npm install -g ember-cli pnpm
# Install git
RUN apk update && apk add git openssh-client
# Trust GitHub's RSA host key
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
# Install app dependencies
RUN pnpm install
# Copy the console directory contents into the container at /console
COPY console .
# Build the application
RUN pnpm build --environment $ENVIRONMENT
# # Make sure the build output is available in /console/dist
# RUN ls -la /console/dist
# # ---- Serve Stage ----
# FROM nginx:alpine
# # Copy the built app to our served directory
# COPY --from=builder /console/dist /usr/share/nginx/html
# # Expose the port nginx is bound to
# EXPOSE 4201
# # Use custom nginx.conf
# COPY console/nginx.conf /etc/nginx/conf.d/default.conf
# # Start Nginx server
# CMD ["nginx", "-g", "daemon off;"]

View File

@@ -2,8 +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 './deprecation-workflow';
export default class App extends Application {
modulePrefix = config.modulePrefix;
@@ -11,13 +10,6 @@ export default class App extends Application {
Resolver = Resolver;
extensions = [];
engines = {};
async ready() {
const extensions = await loadExtensions();
this.extensions = extensions;
this.engines = mapEngines(extensions);
}
}
loadInitializers(App, config.modulePrefix);

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="Filesystem" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Filesystem" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Driver" @helpText="Select the default filesystem driver for Fleetbase to use.">
<Select @options={{this.disks}} @value={{this.driver}} @onSelect={{this.setDriver}} @placeholder="Select filesystem driver" class="w-full" disabled={{this.isLoading}} />
</InputGroup>

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="Mail" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Mail" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Mailer" @helpText="Select the default mailer for Fleetbase to use.">
<Select @options={{this.mailers}} @value={{this.mailer}} @onSelect={{this.setMailer}} @placeholder="Select mailer" class="w-full" />
</InputGroup>
@@ -13,6 +13,14 @@
<InputGroup @name="SMTP Timeout" @value={{this.smtpTimeout}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="SMTP Auth Mode" @value={{this.smtpAuth_mode}} disabled={{this.loadConfigValues.isRunning}} />
{{/if}}
{{#if (eq this.mailer "microsoft-graph")}}
<InputGroup @name="Client ID" @value={{this.microsoftGraphClient_id}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Client Secret" @value={{this.microsoftGraphClient_secret}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Tenant ID" @value={{this.microsoftGraphTenant_id}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup>
<Toggle @isToggled={{this.microsoftGraphSave_to_sent_items}} @onToggle={{fn (mut this.microsoftGraphSave_to_sent_items)}} @label="Save to sent items" />
</InputGroup>
{{/if}}
{{#if (eq this.mailer "mailgun")}}
<InputGroup @name="Mailgun Domain" @value={{this.mailgunDomain}} disabled={{this.loadConfigValues.isRunning}} />
<InputGroup @name="Mailgun Endpoint" @value={{this.mailgunEndpoint}} disabled={{this.loadConfigValues.isRunning}} />

View File

@@ -26,6 +26,10 @@ export default class ConfigureMailComponent extends Component {
@tracked postmarkToken = null;
@tracked sendgridApi_key = null;
@tracked resendKey = null;
@tracked microsoftGraphClient_id = null;
@tracked microsoftGraphClient_secret = null;
@tracked microsoftGraphTenant_id = null;
@tracked microsoftGraphSave_to_sent_items = false;
/**
* Creates an instance of ConfigureFilesystemComponent.
@@ -64,6 +68,19 @@ export default class ConfigureMailComponent extends Component {
};
}
@action serializeMicrosoftGraphConfig() {
return {
client_id: this.microsoftGraphClient_id,
client_secret: this.microsoftGraphClient_secret,
tenant_id: this.microsoftGraphTenant_id,
save_to_sent_items: this.microsoftGraphSave_to_sent_items,
from: {
address: this.fromAddress,
name: this.fromName,
},
};
}
@action serializeMailgunConfig() {
return {
domain: this.mailgunDomain,
@@ -112,6 +129,7 @@ export default class ConfigureMailComponent extends Component {
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
microsoftGraph: this.serializeMicrosoftGraphConfig(),
});
} catch (error) {
this.notifications.serverError(error);
@@ -131,6 +149,7 @@ export default class ConfigureMailComponent extends Component {
postmark: this.serializePostmarkConfig(),
sendgrid: this.serializeSendgridConfig(),
resend: this.serializeResendConfig(),
microsoftGraph: this.serializeMicrosoftGraphConfig(),
});
this.notifications.success('Mail configuration saved.');
} catch (error) {

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="APN Configutation" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="APN Configutation" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="APN Key ID" @value={{this.apn.key_id}} disabled={{this.isLoading}} />
<InputGroup @name="APN Team ID" @value={{this.apn.team_id}} disabled={{this.isLoading}} />
<InputGroup @name="APN App Bundle ID" @value={{this.apn.app_bundle_id}} disabled={{this.isLoading}} />
@@ -20,7 +20,7 @@
</InputGroup>
</ContentPanel>
<ContentPanel @title="Firebase Configutation" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Firebase Configutation" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @wrapperClass="flex flex-row items-center mb-0i">
<UploadButton @name="firebase-service-account" @accept="text/plain,text/javascript,application/json" @onFileAdded={{this.uploadFirebaseCredentials}} @buttonText="Upload Service Account JSON" @icon="upload" class="w-auto m-0i mt-0i" />
{{#if this.firebase.credentials_file}}
@@ -33,7 +33,7 @@
</InputGroup>
</ContentPanel>
<ContentPanel @title="Test Push Notification" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-900">
<ContentPanel @title="Test Push Notification" @open={{true}} @wrapperClass="bordered-classic">
{{#if this.testResponse}}
<div class="flex flex-row items-center rounded-lg border {{if (eq this.testResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
<FaIcon @icon={{if (eq this.testResponse.status 'error') 'triangle-exclamation' 'circle-check'}} class="mr-1.5 {{if (eq this.testResponse.status 'error') 'text-red-200' 'text-green-200'}}" />

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="Queue" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Queue" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Driver" @helpText="Select the default queue driver for Fleetbase to use.">
<Select @options={{this.connections}} @value={{this.driver}} @onSelect={{this.setDriver}} @placeholder="Select queue driver" disabled={{this.isLoading}} class="w-full" />
</InputGroup>

View File

@@ -1,15 +1,15 @@
<ContentPanel @title="AWS" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="AWS" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="AWS Access Key" @value={{this.awsKey}} disabled={{this.isLoading}} />
<InputGroup @name="AWS Access Secret" @value={{this.awsSecret}} disabled={{this.isLoading}} />
<InputGroup @name="AWS Region" @value={{this.awsRegion}} disabled={{this.isLoading}} />
</ContentPanel>
<ContentPanel @title="Google Maps" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Google Maps" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Google Maps API Key" @value={{this.googleMapsApiKey}} disabled={{this.isLoading}} />
<InputGroup @name="Google Maps Locale" @value={{this.googleMapsLocale}} disabled={{this.isLoading}} />
</ContentPanel>
<ContentPanel @title="Twilio" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Twilio" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Twilio SID" @value={{this.twilioSid}} disabled={{this.isLoading}} />
<InputGroup @name="Twilio Token" @value={{this.twilioToken}} disabled={{this.isLoading}} />
<InputGroup @name="Twilio From" @value={{this.twilioFrom}} disabled={{this.isLoading}} />
@@ -25,7 +25,7 @@
</div>
</ContentPanel>
<ContentPanel @title="Sentry" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="Sentry" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="Sentry DSN" @value={{this.sentryDsn}} disabled={{this.isLoading}} />
{{#if this.sentryTestResponse}}
<div class="flex flex-row items-center rounded-lg border {{if (eq this.sentryTestResponse.status 'error') 'border-red-900 bg-red-800 text-red-100' 'border-green-900 bg-green-800 text-green-100'}} shadow-sm my-2 px-4 py-2">
@@ -36,7 +36,7 @@
<Button @wrapperClass="mt-3" @icon="plug" @text="Test Sentry Config" @onClick={{perform this.testSentry}} @isLoading={{this.testSentry.isRunning}} @disabled={{not this.sentryDsn}} />
</ContentPanel>
<ContentPanel @title="IP Info" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="IP Info" @open={{true}} @wrapperClass="bordered-classic">
<InputGroup @name="IP Info API Key" @value={{this.ipinfoApiKey}} disabled={{this.isLoading}} />
</ContentPanel>

View File

@@ -1,4 +1,4 @@
<ContentPanel @title="SocketCluster Connection" @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<ContentPanel @title="SocketCluster Connection" @open={{true}} @wrapperClass="bordered-classic">
<p class="mb-4">The SocketCluster configuration cannot be changed at this time.</p>
<div id="output" class="font-mono rounded-lg max-h-full px-6 py-4 overflow-y-scroll bg-black shadow-inner dark:shadow-none">
<div class="flex items-center justify-between mb-4">

View File

@@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { task } from 'ember-concurrency-decorators';
import { task } from 'ember-concurrency';
export default class MetricComponent extends Component {
@service fetch;

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

@@ -0,0 +1,61 @@
<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>

View File

@@ -0,0 +1,77 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, getProperties } from '@ember/object';
import { isBlank } from '@ember/utils';
import { task } from 'ember-concurrency';
import OnboardValidations from '../../validations/onboard';
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
export default class OnboardingFormComponent extends Component {
@service fetch;
@service session;
@service router;
@service notifications;
@service urlSearchParams;
@tracked name;
@tracked email;
@tracked phone;
@tracked organization_name;
@tracked password;
@tracked password_confirmation;
@tracked error;
get filled() {
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
return Object.values(input).every((val) => !isBlank(val));
}
@task *onboard(event) {
event?.preventDefault?.();
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
yield changeset.validate();
if (changeset.get('isInvalid')) {
const errorMessage = changeset.errors.firstObject.validation.firstObject;
this.notifications.error(errorMessage);
return;
}
// Set user timezone
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
try {
const { status, skipVerification, token, session } = yield this.fetch.post('onboard/create-account', input);
if (status !== 'success') {
this.notifications.error('Onboard failed');
return;
}
// save session
this.args.context.persist('session', session);
if (skipVerification === true && token) {
// only manually authenticate if skip verification
this.session.isOnboarding().manuallyAuthenticate(token);
yield this.router.transitionTo('console');
return this.notifications.success('Welcome to Fleetbase!');
} else {
this.args.orchestrator.next();
this.urlSearchParams.setParamsToCurrentUrl({
step: this.args.orchestrator?.current?.id,
session,
});
}
} catch (err) {
this.notifications.serverError(err);
}
}
}

View File

@@ -0,0 +1,82 @@
{{page-title (t "onboard.verify-email.header-title")}}
<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>
<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>
{{#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>
{{/if}}
</form>
</div>
{{/if}}
</div>
</div>

View File

@@ -0,0 +1,53 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { later, next } from '@ember/runloop';
import { not } from '@ember/object/computed';
import { task } from 'ember-concurrency';
export default class OnboardingVerifyEmailComponent extends Component {
@service('session') authSession;
@service('user-verification') verification;
@service fetch;
@service notifications;
@service router;
@service urlSearchParams;
@tracked code;
@tracked session;
@tracked initialized = false;
constructor() {
super(...arguments);
next(() => this.#initialize());
}
#initialize() {
this.code = this.urlSearchParams.get('code');
this.session = this.args.context.get('session') ?? this.urlSearchParams.get('session');
this.initialized = true;
this.verification.start();
}
@task *verify(event) {
event?.preventDefault?.();
try {
const { status, token } = yield this.fetch.post('onboard/verify-email', { session: this.session, code: this.code });
if (status === 'ok') {
this.notifications.success('Email successfully verified!');
if (token) {
this.notifications.info('Welcome to Fleetbase!');
this.authSession.manuallyAuthenticate(token);
return this.router.transitionTo('console');
}
return this.router.transitionTo('auth.login');
}
} catch (error) {
this.notifications.serverError(error);
}
}
}

View File

@@ -0,0 +1,13 @@
<section class="onboarding step-host">
{{#if this.initialized}}
{{#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">
<Spinner />
</div>
{{/if}}
</section>

View File

@@ -0,0 +1,27 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { next } from '@ember/runloop';
export default class OnboardingYieldComponent extends Component {
@service('onboarding-orchestrator') orchestrator;
@service('onboarding-context') context;
@tracked initialized = false;
get currentComponent() {
return this.orchestrator.current && this.orchestrator.current.component;
}
constructor(owner, { step, session, code }) {
super(...arguments);
next(() => this.#initialize(step, session, code));
}
#initialize(step, session, code) {
if (step) this.orchestrator.goto(step);
if (session) this.context.persist('session', session);
if (code) this.context.set('code', code);
this.initialized = true;
}
}

View File

@@ -1,6 +1,6 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { task } from 'ember-concurrency-decorators';
import { task } from 'ember-concurrency';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';

View File

@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
import { later } from '@ember/runloop';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { dasherize } from '@ember/string';
import first from '@fleetbase/ember-core/utils/first';
export default class ConsoleController extends Controller {
@@ -16,67 +17,19 @@ export default class ConsoleController extends Controller {
@service intl;
@service universe;
@service abilities;
/**
* Authenticated user organizations.
*
* @var {Array}
*/
@service sidebar;
@tracked organizations = [];
/**
* Sidebar Context Controls
*
* @var {SidebarContext}
*/
@tracked sidebarContext;
/**
* State of sidebar toggle icon
*
* @var {SidebarContext}
*/
@tracked sidebarToggleEnabled = true;
/**
* The sidebar toggle state.
*
* @var {SidebarContext}
*/
@tracked sidebarToggleState = {};
/**
* Routes which should hide the sidebar menu.
*
* @var {Array}
*/
@tracked hiddenSidebarRoutes = ['console.home', 'console.notifications', 'console.virtual'];
/**
* Menu items to be added to the main header navigation bar.
*
* @memberof ConsoleController
*/
@tracked menuItems = [];
/**
* Menu items to be added to the user dropdown menu located in the header.
*
* @memberof ConsoleController
*/
@tracked userMenuItems = [];
/**
* Menu items to be added to the organization dropdown menu located in the header.
*
* @memberof ConsoleController
*/
@tracked organizationMenuItems = [];
/**
* Creates an instance of ConsoleController.
* @memberof ConsoleController
*/
get currentRouteClass() {
return dasherize(this.router.currentRouteName.replace(/\./g, ' '));
}
constructor() {
super(...arguments);
this.router.on('routeDidChange', (transition) => {
@@ -89,17 +42,17 @@ export default class ConsoleController extends Controller {
// Hide the sidebar if the current route is in hiddenSidebarRoutes
if (shouldHideSidebar) {
this.sidebarContext.hideNow();
this.sidebar.hideNow();
this.sidebarToggleEnabled = false;
return; // Exit early as no further action is required
}
// If the sidebar was manually closed and not on a hidden route, keep it closed
if (isSidebarManuallyClosed) {
this.sidebarContext.hideNow();
this.sidebar.hideNow();
} else {
// Otherwise, show the sidebar
this.sidebarContext.show();
this.sidebar.show();
}
// Ensure toggle is enabled unless on a hidden route
@@ -134,7 +87,7 @@ export default class ConsoleController extends Controller {
this.universe.trigger('sidebarContext.available', sidebarContext);
if (this.hiddenSidebarRoutes.includes(this.router.currentRouteName)) {
this.sidebarContext.hideNow();
this.sidebar.hideNow();
this.sidebarToggleEnabled = false;
}
}

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

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency-decorators';
import { task } from 'ember-concurrency';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
/**

View File

@@ -1,7 +1,9 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { alias } from '@ember/object/computed';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';
export default class ConsoleAccountIndexController extends Controller {
@@ -40,6 +42,18 @@ export default class ConsoleAccountIndexController extends Controller {
*/
@alias('currentUser.user') user;
/**
* Available timezones for selection.
*
* @memberof ConsoleAccountIndexController
*/
@tracked timezones = [];
constructor() {
super(...arguments);
this.loadTimezones.perform();
}
/**
* Handle upload of new photo
*
@@ -54,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({
@@ -116,6 +131,19 @@ export default class ConsoleAccountIndexController extends Controller {
return isPasswordValid;
}
/**
* Load all available timezones from lookup.
*
* @memberof ConsoleAccountIndexController
*/
@task *loadTimezones() {
try {
this.timezones = yield this.fetch.get('lookup/timezones');
} catch (error) {
debug(`Unable to load timezones : ${error.message}`);
}
}
/**
* Checks if any user attribute has been changed
*

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

@@ -1,136 +0,0 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import createNotificationKey from '../../../utils/create-notification-key';
export default class ConsoleAdminNotificationsController extends Controller {
/**
* Inject the notifications service.
*
* @memberof ConsoleAdminNotificationsController
*/
@service notifications;
/**
* Inject the fetch service.
*
* @memberof ConsoleAdminNotificationsController
*/
@service fetch;
/**
* The notification settings value JSON.
*
* @memberof ConsoleAdminNotificationsController
* @var {Object}
*/
@tracked notificationSettings = {};
/**
* Notification transport methods enabled.
*
* @memberof ConsoleAdminNotificationsController
* @var {Array}
*/
@tracked notificationTransportMethods = ['email', 'sms'];
/**
* Tracked property for the loading state
*
* @memberof ConsoleAdminNotificationsController
* @var {Boolean}
*/
@tracked isLoading = false;
/**
* Creates an instance of ConsoleAdminNotificationsController.
* @memberof ConsoleAdminNotificationsController
*/
constructor() {
super(...arguments);
this.getSettings();
}
/**
* Selectes notifiables for settings.
*
* @param {Object} notification
* @param {Array} notifiables
* @memberof ConsoleAdminNotificationsController
*/
@action onSelectNotifiable(notification, notifiables) {
const notificationKey = createNotificationKey(notification.definition, notification.name);
const _notificationSettings = { ...this.notificationSettings };
if (!_notificationSettings[notificationKey]) {
_notificationSettings[notificationKey] = {};
}
_notificationSettings[notificationKey].notifiables = notifiables;
_notificationSettings[notificationKey].definition = notification.definition;
_notificationSettings[notificationKey].via = notifiables.map((notifiable) => {
return {
identifier: notifiable.value,
methods: this.notificationTransportMethods,
};
});
this.mutateNotificationSettings(_notificationSettings);
}
/**
* Mutates the notification settings property.
*
* @param {Object} [_notificationSettings={}]
* @memberof ConsoleAdminNotificationsController
*/
mutateNotificationSettings(_notificationSettings = {}) {
this.notificationSettings = {
...this.notificationSettings,
..._notificationSettings,
};
}
/**
* Save notification settings to the server.
*
* @action
* @method saveSettings
* @returns {Promise}
* @memberof ConsoleAdminNotificationsController
*/
@action saveSettings() {
const { notificationSettings } = this;
this.isLoading = true;
return this.fetch
.post('notifications/save-settings', { notificationSettings })
.then(() => {
this.notifications.success('Notification settings successfully saved.');
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
/**
* Fetches and updates notification settings asynchronously.
*
* @returns {Promise<void>} A promise for successful retrieval and update, or an error on failure.
*/
getSettings() {
return this.fetch
.get('notifications/get-settings')
.then(({ notificationSettings }) => {
this.notificationSettings = notificationSettings;
})
.catch((error) => {
this.notifications.serverError(error);
});
}
}

View File

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency-decorators';
import { task } from 'ember-concurrency';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
/**

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

@@ -2,6 +2,8 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { debug } from '@ember/debug';
import { task } from 'ember-concurrency';
export default class ConsoleSettingsIndexController extends Controller {
/**
@@ -25,13 +27,6 @@ export default class ConsoleSettingsIndexController extends Controller {
*/
@service fetch;
/**
* The request loading state.
*
* @memberof ConsoleSettingsIndexController
*/
@tracked isLoading = false;
/**
* the upload queue.
*
@@ -46,23 +41,32 @@ export default class ConsoleSettingsIndexController extends Controller {
*/
@tracked uploadedFiles = [];
/**
* Available timezones for selection.
*
* @memberof ConsoleAccountIndexController
*/
@tracked timezones = [];
constructor() {
super(...arguments);
this.loadTimezones.perform();
}
/**
* Save the organization settings.
*
* @memberof ConsoleSettingsIndexController
*/
@action saveSettings(event) {
event.preventDefault();
this.isLoading = true;
@task *saveSettings(event) {
event?.preventDefault();
this.model
.save()
.then(() => {
this.notifications.success('Organization changes successfully saved.');
})
.finally(() => {
this.isLoading = false;
});
try {
yield this.model.save();
this.notifications.success('Organization changes successfully saved.');
} catch (error) {
debug(`Unable to save organization settings : ${error.message}`);
}
}
/**
@@ -91,4 +95,17 @@ export default class ConsoleSettingsIndexController extends Controller {
}
);
}
/**
* Load all available timezones from lookup.
*
* @memberof ConsoleAccountIndexController
*/
@task *loadTimezones() {
try {
this.timezones = yield this.fetch.get('lookup/timezones');
} catch (error) {
debug(`Unable to load timezones : ${error.message}`);
}
}
}

View File

@@ -0,0 +1,152 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import createNotificationKey from '@fleetbase/ember-core/utils/create-notification-key';
import { task } from 'ember-concurrency';
export default class ConsoleSettingsNotificationsController extends Controller {
@service notifications;
@service fetch;
@service store;
@service currentUser;
@tracked notificationSettings = {};
@tracked notificationTransportMethods = ['email', 'sms'];
@tracked company;
/**
* Creates an instance of ConsoleSettingsNotificationsController.
* @memberof ConsoleSettingsNotificationsController
*/
constructor() {
super(...arguments);
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.
*
* @param {Object} notification
* @param {Array} notifiables
* @memberof ConsoleSettingsNotificationsController
*/
@action onSelectNotifiable(notification, notifiables) {
const notificationKey = createNotificationKey(notification.definition, notification.name);
const _notificationSettings = { ...this.notificationSettings };
if (!_notificationSettings[notificationKey]) {
_notificationSettings[notificationKey] = {};
}
_notificationSettings[notificationKey].notifiables = notifiables;
_notificationSettings[notificationKey].definition = notification.definition;
_notificationSettings[notificationKey].via = notifiables.map((notifiable) => {
return {
identifier: notifiable.value,
methods: this.notificationTransportMethods,
};
});
this.mutateNotificationSettings(_notificationSettings);
}
/**
* Mutates the notification settings property.
*
* @param {Object} [_notificationSettings={}]
* @memberof ConsoleSettingsNotificationsController
*/
mutateNotificationSettings(_notificationSettings = {}) {
this.notificationSettings = {
...this.notificationSettings,
..._notificationSettings,
};
}
/**
* Save notification settings.
*
* @memberof ConsoleSettingsNotificationsController
*/
@task *saveSettings() {
const { notificationSettings } = this;
try {
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);
}
}
/**
* Get notification settings.
*
* @memberof ConsoleSettingsNotificationsController
*/
@task *getSettings() {
try {
const { notificationSettings } = yield this.fetch.get('notifications/get-settings');
this.notificationSettings = notificationSettings;
} catch (error) {
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

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency-decorators';
import { task } from 'ember-concurrency';
import getTwoFaMethods from '@fleetbase/console/utils/get-two-fa-methods';
export default class ConsoleSettingsTwoFaController extends Controller {

View File

@@ -2,7 +2,7 @@ import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency-decorators';
import { task } from 'ember-concurrency';
export default class InstallController extends Controller {
@service fetch;

View File

@@ -1,151 +1,8 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, getProperties } from '@ember/object';
import OnboardValidations from '../../validations/onboard';
import lookupValidator from 'ember-changeset-validations';
import Changeset from 'ember-changeset';
export default class OnboardIndexController extends Controller {
/**
* Inject the `fetch` service
*
* @memberof OnboardIndexController
*/
@service fetch;
/**
* Inject the `session` service
*
* @memberof OnboardIndexController
*/
@service session;
/**
* Inject the `router` service
*
* @memberof OnboardIndexController
*/
@service router;
/**
* Inject the `notifications` service
*
* @memberof OnboardIndexController
*/
@service notifications;
/**
* The name input field.
*
* @memberof OnboardIndexController
*/
@tracked name;
/**
* The email input field.
*
* @memberof OnboardIndexController
*/
@tracked email;
/**
* The phone input field.
*
* @memberof OnboardIndexController
*/
@tracked phone;
/**
* The organization_name input field.
*
* @memberof OnboardIndexController
*/
@tracked organization_name;
/**
* The password input field.
*
* @memberof OnboardIndexController
*/
@tracked password;
/**
* The name password confirmation field.
*
* @memberof OnboardIndexController
*/
@tracked password_confirmation;
/**
* The property for error message.
*
* @memberof OnboardIndexController
*/
@tracked error;
/**
* The loading state of the onboard request.
*
* @memberof OnboardIndexController
*/
@tracked isLoading = false;
/**
* The ready state for the form.
*
* @memberof OnboardIndexController
*/
@tracked readyToSubmit = false;
/**
* Start the onboard process.
*
* @return {Promise}
* @memberof OnboardIndexController
*/
@action async startOnboard(event) {
event.preventDefault();
// eslint-disable-next-line ember/no-get
const input = getProperties(this, 'name', 'email', 'phone', 'organization_name', 'password', 'password_confirmation');
const changeset = new Changeset(input, lookupValidator(OnboardValidations), OnboardValidations);
await changeset.validate();
if (changeset.get('isInvalid')) {
const errorMessage = changeset.errors.firstObject.validation.firstObject;
this.notifications.error(errorMessage);
return;
}
// Set user timezone
input.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
this.isLoading = true;
return this.fetch
.post('onboard/create-account', input)
.then(({ status, skipVerification, token, session }) => {
if (status === 'success') {
if (skipVerification === true && token) {
// only manually authenticate if skip verification
this.session.isOnboarding().manuallyAuthenticate(token);
return this.router.transitionTo('console').then(() => {
this.notifications.success('Welcome to Fleetbase!');
});
}
return this.router.transitionTo('onboard.verify-email', { queryParams: { hello: session } });
}
})
.catch((error) => {
this.notifications.serverError(error);
})
.finally(() => {
this.isLoading = false;
});
}
@tracked step;
@tracked session;
@tracked code;
}

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

@@ -1,6 +0,0 @@
import { helper } from '@ember/component/helper';
import createNotificationKey from '../utils/create-notification-key';
export default helper(function getNotificationKey([definition, name]) {
return createNotificationKey(definition, name);
});

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

@@ -0,0 +1,44 @@
import translations from 'ember-intl/translations';
import { all } from 'rsvp';
const isBrowser = typeof window !== 'undefined';
function langOf(tag = 'en') {
return String(tag).toLowerCase().split('-')[0];
}
async function loadBasePolyfills() {
await import('@formatjs/intl-numberformat/polyfill-force');
await import('@formatjs/intl-pluralrules/polyfill-force');
await import('@formatjs/intl-datetimeformat/polyfill-force');
await import('@formatjs/intl-relativetimeformat/polyfill-force');
}
async function loadLocaleData(lang) {
return all([
import(`@formatjs/intl-numberformat/locale-data/${lang}.js`),
import(`@formatjs/intl-pluralrules/locale-data/${lang}.js`),
import(`@formatjs/intl-datetimeformat/locale-data/${lang}.js`),
import(`@formatjs/intl-relativetimeformat/locale-data/${lang}.js`),
]);
}
export function initialize(application) {
if (!isBrowser) return;
// Build-time list of locales from the generated module
const locales = translations.map(([locale]) => String(locale));
const langs = [...new Set(locales.map(langOf))];
application.deferReadiness();
(async () => {
await loadBasePolyfills();
await all(langs.map(loadLocaleData));
application.advanceReadiness();
})();
}
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

@@ -14,5 +14,6 @@ export function initialize(application) {
}
export default {
name: 'load-leaflet',
initialize,
};

View File

@@ -0,0 +1,19 @@
export function initialize(owner) {
const registry = owner.lookup('service:onboarding-registry');
if (registry) {
const defaultFlow = {
id: 'default@v1',
entry: 'signup',
steps: [
{ id: 'signup', component: 'onboarding/form', next: 'verify-email' },
{ id: 'verify-email', component: 'onboarding/verify-email' },
],
};
registry.registerFlow(defaultFlow);
}
}
export default {
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

@@ -0,0 +1,20 @@
import Model, { attr } from '@ember-data/model';
export default class ActivityModel extends Model {
@attr('string') uuid;
@attr('string') log_name;
@attr('string') description;
@attr('string') company_id;
@attr('string') subject_id;
@attr('string') subject_type;
@attr('string') humanized_subject_type;
@attr('string') event;
@attr('string') causer_id;
@attr('string') causer_type;
@attr('string') humanized_causer_type;
@attr('object') properties;
@attr('object') causer;
@attr('object') subject;
@attr('date') created_at;
@attr('date') updated_at;
}

311
console/app/models/alert.js Normal file
View File

@@ -0,0 +1,311 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { format, formatDistanceToNow, differenceInMinutes } from 'date-fns';
export default class AlertModel extends Model {
/** @attributes */
@attr('string') type;
@attr('string') severity;
@attr('string') status;
@attr('string') subject_type;
@attr('string') subject_uuid;
@attr('string') message;
/** @json attributes */
@attr() rule;
@attr() context;
@attr() meta;
/** @dates */
@attr('date') triggered_at;
@attr('date') acknowledged_at;
@attr('date') resolved_at;
@attr('date') created_at;
@attr('date') updated_at;
@attr('date') deleted_at;
/** @relationships */
@belongsTo('company') company;
@belongsTo('user', { inverse: null }) acknowledgedBy;
@belongsTo('user', { inverse: null }) resolvedBy;
/** @computed - Date formatting */
@computed('triggered_at') get triggeredAgo() {
if (!this.triggered_at) return 'Unknown';
return formatDistanceToNow(this.triggered_at) + ' ago';
}
@computed('triggered_at') get triggeredAt() {
if (!this.triggered_at) return 'Unknown';
return format(this.triggered_at, 'yyyy-MM-dd HH:mm');
}
@computed('acknowledged_at') get acknowledgedAgo() {
if (!this.acknowledged_at) return null;
return formatDistanceToNow(this.acknowledged_at) + ' ago';
}
@computed('acknowledged_at') get acknowledgedAt() {
if (!this.acknowledged_at) return 'Not acknowledged';
return format(this.acknowledged_at, 'yyyy-MM-dd HH:mm');
}
@computed('resolved_at') get resolvedAgo() {
if (!this.resolved_at) return null;
return formatDistanceToNow(this.resolved_at) + ' ago';
}
@computed('resolved_at') get resolvedAt() {
if (!this.resolved_at) return 'Not resolved';
return format(this.resolved_at, 'yyyy-MM-dd HH:mm');
}
@computed('updated_at') get updatedAgo() {
return formatDistanceToNow(this.updated_at) + ' ago';
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
@computed('created_at') get createdAgo() {
return formatDistanceToNow(this.created_at) + ' ago';
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
/** @computed - Status checks */
@computed('acknowledged_at') get isAcknowledged() {
return !!this.acknowledged_at;
}
@computed('resolved_at') get isResolved() {
return !!this.resolved_at;
}
@computed('isAcknowledged', 'isResolved') get isPending() {
return !this.isAcknowledged && !this.isResolved;
}
@computed('isAcknowledged', 'isResolved') get isActive() {
return this.isAcknowledged && !this.isResolved;
}
/** @computed - Duration calculations */
@computed('triggered_at', 'acknowledged_at') get acknowledgmentDurationMinutes() {
if (!this.triggered_at || !this.acknowledged_at) return null;
return differenceInMinutes(new Date(this.acknowledged_at), new Date(this.triggered_at));
}
@computed('triggered_at', 'resolved_at') get resolutionDurationMinutes() {
if (!this.triggered_at || !this.resolved_at) return null;
return differenceInMinutes(new Date(this.resolved_at), new Date(this.triggered_at));
}
@computed('triggered_at') get ageMinutes() {
if (!this.triggered_at) return 0;
return differenceInMinutes(new Date(), new Date(this.triggered_at));
}
@computed('acknowledgmentDurationMinutes') get acknowledgmentDurationFormatted() {
if (!this.acknowledgmentDurationMinutes) return null;
const minutes = this.acknowledgmentDurationMinutes;
if (minutes < 60) return `${minutes}m`;
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
}
@computed('resolutionDurationMinutes') get resolutionDurationFormatted() {
if (!this.resolutionDurationMinutes) return null;
const minutes = this.resolutionDurationMinutes;
if (minutes < 60) return `${minutes}m`;
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
}
@computed('ageMinutes') get ageFormatted() {
const minutes = this.ageMinutes;
if (minutes < 60) return `${minutes}m`;
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
return `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`;
}
/** @computed - Severity styling */
@computed('severity') get severityBadgeClass() {
const severityClasses = {
critical: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
high: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
low: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
info: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
};
return severityClasses[this.severity] || severityClasses['info'];
}
@computed('severity') get severityIcon() {
const severityIcons = {
critical: 'fas fa-exclamation-circle',
high: 'fas fa-exclamation-triangle',
medium: 'fas fa-exclamation',
low: 'fas fa-info-circle',
info: 'fas fa-info',
};
return severityIcons[this.severity] || severityIcons['info'];
}
@computed('severity') get severityColor() {
const severityColors = {
critical: 'text-red-600 dark:text-red-400',
high: 'text-orange-600 dark:text-orange-400',
medium: 'text-yellow-600 dark:text-yellow-400',
low: 'text-blue-600 dark:text-blue-400',
info: 'text-gray-600 dark:text-gray-400',
};
return severityColors[this.severity] || severityColors['info'];
}
/** @computed - Status styling */
@computed('status', 'isAcknowledged', 'isResolved') get statusBadgeClass() {
if (this.isResolved) {
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
}
if (this.isAcknowledged) {
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
}
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
}
@computed('status', 'isAcknowledged', 'isResolved') get statusText() {
if (this.isResolved) return 'Resolved';
if (this.isAcknowledged) return 'Acknowledged';
return 'Pending';
}
@computed('status', 'isAcknowledged', 'isResolved') get statusIcon() {
if (this.isResolved) return 'fas fa-check-circle';
if (this.isAcknowledged) return 'fas fa-eye';
return 'fas fa-bell';
}
/** @computed - Type styling */
@computed('type') get typeIcon() {
const typeIcons = {
maintenance: 'fas fa-wrench',
temperature: 'fas fa-thermometer-half',
fuel: 'fas fa-gas-pump',
speed: 'fas fa-tachometer-alt',
location: 'fas fa-map-marker-alt',
system: 'fas fa-cog',
security: 'fas fa-shield-alt',
performance: 'fas fa-chart-line',
compliance: 'fas fa-clipboard-check',
};
return typeIcons[this.type] || 'fas fa-bell';
}
@computed('type') get typeBadgeClass() {
const typeClasses = {
maintenance: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
temperature: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300',
fuel: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
speed: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
location: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
system: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
security: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
performance: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-300',
compliance: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300',
};
return typeClasses[this.type] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
}
/** @computed - Subject information */
@computed('subject_type') get subjectTypeFormatted() {
if (!this.subject_type) return 'Unknown';
// Convert from model class name to human readable
const typeMap = {
vehicle: 'Vehicle',
driver: 'Driver',
order: 'Order',
device: 'Device',
asset: 'Asset',
maintenance: 'Maintenance',
fuel_report: 'Fuel Report',
};
return typeMap[this.subject_type] || this.subject_type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
}
/** @computed - Priority and urgency */
@computed('severity', 'ageMinutes') get urgencyLevel() {
const severityWeight = {
critical: 4,
high: 3,
medium: 2,
low: 1,
info: 0,
};
const weight = severityWeight[this.severity] || 0;
const ageHours = this.ageMinutes / 60;
// Calculate urgency based on severity and age
if (weight >= 3 && ageHours > 1) return 'urgent';
if (weight >= 2 && ageHours > 4) return 'urgent';
if (weight >= 3) return 'high';
if (weight >= 2) return 'medium';
return 'low';
}
@computed('urgencyLevel') get urgencyBadgeClass() {
const urgencyClasses = {
urgent: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300 animate-pulse',
high: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300',
medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300',
low: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
};
return urgencyClasses[this.urgencyLevel] || urgencyClasses['low'];
}
/** @computed - Context information */
@computed('context') get hasContext() {
return !!(this.context && Object.keys(this.context).length > 0);
}
@computed('rule') get hasRule() {
return !!(this.rule && Object.keys(this.rule).length > 0);
}
@computed('context.location') get hasLocation() {
return !!this.context?.location;
}
@computed('context.value', 'rule.{operator,threshold}') get thresholdExceeded() {
if (!this.context?.value || !this.rule?.threshold) return null;
const value = parseFloat(this.context.value);
const threshold = parseFloat(this.rule.threshold);
const operator = this.rule.operator || '>';
switch (operator) {
case '>':
return value > threshold;
case '<':
return value < threshold;
case '>=':
return value >= threshold;
case '<=':
return value <= threshold;
case '==':
return value === threshold;
case '!=':
return value !== threshold;
default:
return null;
}
}
}

View File

@@ -18,6 +18,10 @@ export default class CategoryModel extends Model {
@hasMany('category', { inverse: 'parent' }) subcategories;
@tracked parent_category;
/** Array<CustomFieldModel> attached at runtime for rendering */
@tracked customFields = [];
@tracked isEditing = false;
/** @attributes */
@attr('string') owner_type;
@attr('string') name;
@@ -46,7 +50,7 @@ export default class CategoryModel extends Model {
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'PPP p');
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
@computed('updated_at') get updatedAtShort() {
@@ -58,7 +62,7 @@ export default class CategoryModel extends Model {
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'PPP p');
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
@computed('created_at') get createdAtShort() {

View File

@@ -31,7 +31,7 @@ export default class CommentModel extends Model {
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'PPP p');
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
@computed('updated_at') get updatedAgo() {
@@ -39,6 +39,6 @@ export default class CommentModel extends Model {
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'PPP p');
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
}

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;
@@ -50,7 +50,7 @@ export default class Company extends Model {
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'PPP p');
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
@computed('updated_at') get updatedAtShort() {
@@ -62,7 +62,7 @@ export default class Company extends Model {
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'PPP p');
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
@computed('created_at') get createdAtShort() {

View File

@@ -41,7 +41,7 @@ export default class CustomFieldValueModel extends Model {
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'PPP p');
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
@computed('updated_at') get updatedAgo() {
@@ -49,6 +49,6 @@ export default class CustomFieldValueModel extends Model {
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'PPP p');
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
}

View File

@@ -12,6 +12,7 @@ export default class CustomFieldModel extends Model {
/** @attributes */
@attr('string') name;
@attr('string') description;
@attr('string') for;
@attr('string') help_text;
@attr('string') label;
@attr('string') type;
@@ -30,12 +31,20 @@ export default class CustomFieldModel extends Model {
@attr('date') deleted_at;
/** @computed */
@computed('type') get valueType() {
if (this.type === 'file-upload') return 'file';
if (this.type === 'date-time-input') return 'date';
if (this.type === 'model-select') return 'model';
return 'text';
}
@computed('created_at') get createdAgo() {
return formatDistanceToNow(this.created_at);
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'PPP p');
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
@computed('updated_at') get updatedAgo() {
@@ -43,6 +52,6 @@ export default class CustomFieldModel extends Model {
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'PPP p');
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
}

View File

@@ -25,7 +25,7 @@ export default class DashboardWidgetModel extends Model {
}
@computed('updated_at') get updatedAt() {
return format(this.updated_at, 'PPP p');
return format(this.updated_at, 'yyyy-MM-dd HH:mm');
}
@computed('updated_at') get updatedAtShort() {
@@ -37,7 +37,7 @@ export default class DashboardWidgetModel extends Model {
}
@computed('created_at') get createdAt() {
return format(this.created_at, 'PPP p');
return format(this.created_at, 'yyyy-MM-dd HH:mm');
}
@computed('created_at') get createdAtShort() {

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