Compare commits

..

80 Commits

Author SHA1 Message Date
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
80 changed files with 4697 additions and 3187 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 }}

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

13
.gitignore vendored
View File

@@ -34,4 +34,15 @@ packages/customer-portal
# wip
packages/solid
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

@@ -32,10 +32,7 @@ Fleetbase is a modular logistics and supply chain operating system designed to s
```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
@@ -75,10 +72,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,7 +83,17 @@ 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.
@@ -100,6 +104,7 @@ 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,7 +113,6 @@ services:
TWILIO_SID:
TWILIO_TOKEN:
TWILIO_FROM:
CONSOLE_HOST: http://localhost:4200
```
You can learn more about full installation, and configuration in the [official documentation](https://docs.fleetbase.io/getting-started/install).
@@ -145,9 +149,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. **Fleetbase for Desktop** ~ Desktop builds for OSX and Windows.
4. **Custom Maps and Routing Engines** ~ Feature to enable easy integrations with custom maps and routing engines like Google Maps or Mapbox etc…
## 🪲 Bugs and 💡 Feature Requests

34
RELEASE.md Normal file
View File

@@ -0,0 +1,34 @@
# 🚀 Fleetbase v0.7.5 — 2025-05-30
> “Route optimization and routing control advancements”
---
## ✨ Highlights
- Added route optimization and routing control services for registering additional routing engines and route optimization services.
- Added settings for Routing (Next release will be able to set unit "Miles" or "Kilometers")
- Improved and optimized environment and settings mapper.
- Added entity activity events
- Patched multiple waypoint order creation via API
---
## ⚠️ Breaking Changes
- None 🙂
---
## 🔧 Upgrade Steps
```bash
# Pull latest version
git pull origin main --no-rebase
# Update docker
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/fleetbase/discussions) or drop by [#fleetbase on Discord](https://discord.com/invite/HnTqQ6zAVn)

View File

@@ -10,10 +10,10 @@
"require": {
"php": "^8.0",
"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.11",
"fleetbase/fleetops-api": "^0.6.14",
"fleetbase/registry-bridge": "^0.0.19",
"fleetbase/storefront-api": "^0.4.0",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^10.0",
"laravel/octane": "^2.3",

928
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

@@ -21,7 +21,7 @@ return [
'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

@@ -1,5 +1,6 @@
<?php
use Fleetbase\Support\Utils;
use Laravel\Octane\Contracts\OperationTerminated;
use Laravel\Octane\Events\RequestHandled;
use Laravel\Octane\Events\RequestReceived;
@@ -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,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,92 @@
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder
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 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']);
}
}

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

@@ -4,6 +4,7 @@ import loadInitializers from 'ember-load-initializers';
import config from '@fleetbase/console/config/environment';
import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
import mapEngines from '@fleetbase/ember-core/utils/map-engines';
import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
export default class App extends Application {
modulePrefix = config.modulePrefix;
@@ -20,4 +21,11 @@ export default class App extends Application {
}
}
loadInitializers(App, config.modulePrefix);
document.addEventListener('DOMContentLoaded', async () => {
await loadRuntimeConfig();
loadInitializers(App, config.modulePrefix);
let fleetbase = App.create();
fleetbase.deferReadiness();
fleetbase.boot();
});

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,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

@@ -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
*
@@ -116,6 +130,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,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,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

@@ -2,27 +2,28 @@ 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';
import createNotificationKey from '@fleetbase/ember-core/utils/create-notification-key';
import { task } from 'ember-concurrency';
export default class ConsoleAdminNotificationsController extends Controller {
export default class ConsoleSettingsNotificationsController extends Controller {
/**
* Inject the notifications service.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@service notifications;
/**
* Inject the fetch service.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@service fetch;
/**
* The notification settings value JSON.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
* @var {Object}
*/
@tracked notificationSettings = {};
@@ -30,26 +31,18 @@ export default class ConsoleAdminNotificationsController extends Controller {
/**
* Notification transport methods enabled.
*
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
* @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
* Creates an instance of ConsoleSettingsNotificationsController.
* @memberof ConsoleSettingsNotificationsController
*/
constructor() {
super(...arguments);
this.getSettings();
this.getSettings.perform();
}
/**
@@ -57,7 +50,7 @@ export default class ConsoleAdminNotificationsController extends Controller {
*
* @param {Object} notification
* @param {Array} notifiables
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@action onSelectNotifiable(notification, notifiables) {
const notificationKey = createNotificationKey(notification.definition, notification.name);
@@ -83,7 +76,7 @@ export default class ConsoleAdminNotificationsController extends Controller {
* Mutates the notification settings property.
*
* @param {Object} [_notificationSettings={}]
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
mutateNotificationSettings(_notificationSettings = {}) {
this.notificationSettings = {
@@ -93,44 +86,32 @@ export default class ConsoleAdminNotificationsController extends Controller {
}
/**
* Save notification settings to the server.
* Save notification settings.
*
* @action
* @method saveSettings
* @returns {Promise}
* @memberof ConsoleAdminNotificationsController
* @memberof ConsoleSettingsNotificationsController
*/
@action saveSettings() {
@task *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;
});
try {
yield this.fetch.post('notifications/save-settings', { notificationSettings });
this.notifications.success('Notification settings successfully saved.');
} catch (error) {
this.notifications.serverError(error);
}
}
/**
* Fetches and updates notification settings asynchronously.
* Get notification settings.
*
* @returns {Promise<void>} A promise for successful retrieval and update, or an error on failure.
* @memberof ConsoleSettingsNotificationsController
*/
getSettings() {
return this.fetch
.get('notifications/get-settings')
.then(({ notificationSettings }) => {
this.notificationSettings = notificationSettings;
})
.catch((error) => {
this.notifications.serverError(error);
});
@task *getSettings() {
try {
const { notificationSettings } = yield this.fetch.get('notifications/get-settings');
this.notificationSettings = notificationSettings;
} 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,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

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

View File

@@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import groupBy from '@fleetbase/ember-core/utils/group-by';
export default class ConsoleAdminNotificationsRoute extends Route {
export default class ConsoleSettingsNotificationsRoute extends Route {
@service fetch;
model() {
@@ -14,6 +14,8 @@ export default class ConsoleAdminNotificationsRoute extends Route {
}
setupController(controller, { registry, notifiables }) {
super.setupController(...arguments);
controller.groupedNotifications = groupBy(registry, 'package');
controller.notifiables = notifiables;
}

View File

@@ -67,3 +67,11 @@ body.console-admin-organizations-index-index .next-table-wrapper > table {
body[data-theme='dark'] #boot-loader > .loader-container > .loading-message {
color: #fff;
}
/** hotfix: ember-power-select-trigger broken padding after upgrade - @todo move to ember-ui */
body.fleetbase-console .ember-power-select-trigger {
padding-top: 0.5rem;
padding-right: 2.5rem;
padding-bottom: 0.5rem;
padding-left: 0.75rem;
}

View File

@@ -1,5 +1,6 @@
{{page-title (t "app.name")}}
<ModalsContainer />
<NotificationContainer @position="top" @zindex="99999" />
<BasicDropdownWormhole />
<div id="application-root-wormhole"></div>
{{outlet}}

View File

@@ -33,6 +33,9 @@
<PhoneInput @value={{this.user.phone}} @onInput={{fn (mut this.user.phone)}} class="form-input input-lg w-full" />
</InputGroup>
<InputGroup @name={{t "common.date-of-birth"}} @type="date" @value={{this.user.date_of_birth}} />
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.account.index.timezone"}}>
<Select @value={{this.user.timezone}} @options={{this.timezones}} @onSelect={{fn (mut this.user.timezone)}} @placeholder={{t "console.account.index.timezone"}} />
</InputGroup>
</div>
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text={{t "common.save-button-text"}} @onClick={{perform this.saveProfile}} @isLoading={{not this.saveProfile.isIdle}} />

View File

@@ -4,7 +4,6 @@
<Layout::Sidebar::Item @route="console.admin.index" @icon="rectangle-list">{{t "common.overview"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.organizations" @icon="building">{{t "common.organizations"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.branding" @icon="palette">{{t "common.branding"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.notifications" @icon="bell">{{t "common.notifications"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.two-fa-settings" @icon="shield-halved">{{t "common.2fa-config"}}</Layout::Sidebar::Item>
<Layout::Sidebar::Item @route="console.admin.schedule-monitor" @icon="calendar-check">{{t "console.admin.schedule-monitor.schedule-monitor"}}</Layout::Sidebar::Item>
{{#each this.universe.adminMenuItems as |menuItem|}}

View File

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

View File

@@ -4,7 +4,7 @@
<div class="container mx-auto h-screen">
<div class="max-w-3xl my-10 mx-auto space-y-6">
<ContentPanel @title={{t "console.settings.index.title"}} @open={{true}} @pad={{true}} @panelBodyClass="bg-white dark:bg-gray-800">
<form {{on "submit" this.saveSettings}}>
<form {{on "submit" (perform this.saveSettings)}}>
<InputGroup @name={{t "console.settings.index.organization-name"}} @value={{@model.name}} />
<InputGroup @name={{t "console.settings.index.organization-description"}} @value={{@model.description}} />
<InputGroup @name={{t "console.settings.index.organization-phone"}}>
@@ -13,9 +13,12 @@
<InputGroup @name={{t "console.settings.index.organization-currency"}}>
<CurrencySelect @currency={{@model.currency}} @onCurrencyChange={{fn (mut @model.currency)}} @triggerClass="w-full form-select" />
</InputGroup>
<InputGroup @name={{t "common.timezone"}} @helpText={{t "console.settings.index.organization-timezone"}}>
<Select @value={{@model.timezone}} @options={{this.timezones}} @onSelect={{fn (mut @model.timezone)}} @placeholder={{t "console.settings.index.select-timezone"}} class="w-full form-select" />
</InputGroup>
<InputGroup @name={{t "console.settings.index.organization-id"}} @value={{@model.public_id}} @disabled={{true}} />
<div class="mt-3 flex items-center justify-end">
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="{{t "common.save-button-text"}}" @isLoading={{this.isLoading}} />
<Button @buttonType="submit" @type="primary" @size="lg" @icon="save" @text="{{t "common.save-button-text"}}" @isLoading={{this.saveSettings.isRunning}} />
</div>
</form>
</ContentPanel>

View File

@@ -1,6 +1,6 @@
{{page-title "Push Notifications"}}
<Layout::Section::Header @title={{t "console.admin.notifications.title"}}>
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{this.saveSettings}} @disabled={{this.isLoading}} @isLoading={{this.isLoading}} />
{{page-title "Notifications"}}
<Layout::Section::Header @title={{t "common.notifications"}}>
<Button @type="primary" @size="sm" @icon="save" @text={{t "common.save-button-text"}} @onClick={{perform this.saveSettings}} @disabled={{this.saveSettings.isRunning}} @isLoading={{or this.saveSettings.isRunning this.getSettings.isRunning}} />
</Layout::Section::Header>
<Layout::Section::Body class="overflow-y-scroll h-full">

View File

@@ -1,9 +1,11 @@
<div class="bg-white dark:bg-gray-800 py-5 px-4 shadow rounded-lg w-full">
<div class="mb-4">
<Image src={{@model.logo_url}} @fallbackSrc="/images/fleetbase-logo-svg.svg" alt={{t "app.name"}} width="160" height="56" class="w-40 h-14 mx-auto" />
<h2 class="text-center text-lg font-extrabold text-gray-900 dark:text-white truncate">
{{t "onboard.index.title"}}
</h2>
<Image src={{@model.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">

View File

@@ -1,8 +0,0 @@
import { camelize } from '@ember/string';
export default function createNotificationKey(definition, name) {
const withoutSlashes = definition.replace(/[\W_]+/g, '');
const key = `${camelize(withoutSlashes)}__${camelize(name)}`;
return key;
}

View File

@@ -19,6 +19,8 @@ export default function getPermissionAction(permissionName) {
'notify',
'assign_vehicle',
'assign_order_to',
'assign_driver_to',
'assign_vehicle_to',
'dispatch_order_to',
'dispatch',
'assign',

View File

@@ -0,0 +1,84 @@
import config from '@fleetbase/console/config/environment';
import toBoolean from '@fleetbase/ember-core/utils/to-boolean';
import { set } from '@ember/object';
import { debug } from '@ember/debug';
/**
* Maps allowed runtime keys to internal config paths.
*/
const RUNTIME_CONFIG_MAP = {
API_HOST: 'API.host',
API_NAMESPACE: 'API.namespace',
SOCKETCLUSTER_PATH: 'socket.path',
SOCKETCLUSTER_HOST: 'socket.hostname',
SOCKETCLUSTER_SECURE: 'socket.secure',
SOCKETCLUSTER_PORT: 'socket.port',
OSRM_HOST: 'osrm.host',
EXTENSIONS: 'APP.extensions',
};
/**
* Coerce and sanitize runtime config values based on key.
*
* @param {String} key
* @param {*} value
* @return {*}
*/
function coerceValue(key, value) {
switch (key) {
case 'SOCKETCLUSTER_PORT':
return parseInt(value, 10);
case 'SOCKETCLUSTER_SECURE':
return toBoolean(value);
case 'EXTENSIONS':
return typeof value === 'string' ? value.split(',') : Array.from(value);
default:
return value;
}
}
/**
* Apply runtime config overrides based on strict allowlist mapping.
*
* @param {Object} rawConfig
*/
export function applyRuntimeConfig(rawConfig = {}) {
Object.entries(rawConfig).forEach(([key, value]) => {
const configPath = RUNTIME_CONFIG_MAP[key];
if (configPath) {
const coercedValue = coerceValue(key, value);
set(config, configPath, coercedValue);
} else {
debug(`[runtime-config] Ignored unknown key: ${key}`);
}
});
}
/**
* Load and apply runtime config.
*
* @export
* @return {void}
*/
export default async function loadRuntimeConfig() {
if (config.APP.disableRuntimeConfig) {
return;
}
try {
const response = await fetch(`/fleetbase.config.json?_t=${Date.now()}`, { cache: 'no-cache' });
if (!response.ok) {
debug('No fleetbase.config.json found, using built-in config defaults');
return;
}
const runtimeConfig = await response.json();
applyRuntimeConfig(runtimeConfig);
} catch (e) {
debug(`Failed to load runtime config : ${e.message}`);
}
}

View File

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

View File

@@ -3,6 +3,8 @@
/** eslint-disable node/no-unpublished-require */
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const FleetbaseExtensionsIndexer = require('fleetbase-extensions-indexer');
const Funnel = require('broccoli-funnel');
const writeFile = require('broccoli-file-creator');
const postcssImport = require('postcss-import');
const postcssPresetEnv = require('postcss-preset-env');
const postcssEach = require('postcss-each');
@@ -11,6 +13,8 @@ const postcssConditionals = require('postcss-conditionals-renewed');
const postcssAtRulesVariables = require('postcss-at-rules-variables');
const autoprefixer = require('autoprefixer');
const tailwind = require('tailwindcss');
const toBoolean = require('./config/utils/to-boolean');
const environment = process.env.EMBER_ENV;
module.exports = function (defaults) {
const app = new EmberApp(defaults, {
@@ -59,19 +63,15 @@ module.exports = function (defaults) {
});
let extensions = new FleetbaseExtensionsIndexer();
let runtimeConfigTree;
if (toBoolean(process.env.DISABLE_RUNTIME_CONFIG)) {
runtimeConfigTree = writeFile('fleetbase.config.json', '{}');
} else {
runtimeConfigTree = new Funnel('.', {
files: ['fleetbase.config.json'],
destDir: '/',
});
}
// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
return app.toTree([extensions]);
return app.toTree([extensions, runtimeConfigTree].filter(Boolean));
};

View File

@@ -0,0 +1,3 @@
{
"API_HOST": "http://localhost:8000"
}

View File

@@ -6,4 +6,18 @@ server {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html =404;
}
# Serve runtime config with no cache
location = /fleetbase.config.json {
root /usr/share/nginx/html;
default_type application/json;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
try_files /fleetbase.config.json @config_fallback;
}
location @config_fallback {
return 200 '{}';
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@fleetbase/console",
"version": "0.6.8",
"version": "0.7.5",
"private": true,
"description": "Modular logistics and supply chain operating system (LSOS)",
"repository": "https://github.com/fleetbase/fleetbase",
@@ -33,20 +33,19 @@
"@fleetbase/ember-core": "latest",
"@fleetbase/ember-ui": "latest",
"@fleetbase/fleetops-data": "latest",
"@fleetbase/fleetops-engine": "^0.6.5",
"@fleetbase/fleetops-engine": "^0.6.14",
"@fleetbase/iam-engine": "^0.1.3",
"@fleetbase/leaflet-routing-machine": "^3.2.16",
"@fleetbase/registry-bridge-engine": "^0.0.18",
"@fleetbase/storefront-engine": "^0.3.30",
"@fleetbase/leaflet-routing-machine": "^3.2.17",
"@fleetbase/registry-bridge-engine": "^0.0.19",
"@fleetbase/storefront-engine": "^0.4.0",
"@fortawesome/ember-fontawesome": "^2.0.0",
"ember-changeset": "4.1.2",
"ember-changeset-validations": "4.1.2",
"ember-composable-helpers": "^5.0.0",
"ember-concurrency": "^3.1.1",
"ember-concurrency": "^4.0.4",
"ember-concurrency-decorators": "^2.0.3",
"ember-intl": "6.3.2",
"ember-math-helpers": "^2.18.2",
"ember-power-select": "^7.2.0",
"ember-prism": "^0.13.0",
"ember-radio-button": "3.0.0-beta.1",
"ember-tag-input": "^3.1.0",
@@ -56,13 +55,13 @@
"postcss-nth-list": "^1.0.2"
},
"devDependencies": {
"@embroider/macros": "1.16.12",
"@babel/core": "^7.25.2",
"@babel/eslint-parser": "^7.25.1",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@ember/optional-features": "^2.1.0",
"@ember/string": "^3.1.1",
"@ember/test-helpers": "^3.3.1",
"@embroider/macros": "1.16.12",
"@fleetbase/intl-lint": "^0.0.1",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
@@ -72,6 +71,7 @@
"@tailwindcss/forms": "^0.5.7",
"autoprefixer": "^10.4.20",
"broccoli-asset-rev": "^3.0.0",
"broccoli-file-creator": "^2.1.1",
"broccoli-funnel": "^3.0.8",
"concurrently": "^8.2.2",
"date-fns": "^2.30.0",

4267
console/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@ Router.map(function () {
this.route('settings', function () {
this.route('virtual', { path: '/:slug' });
this.route('two-fa');
this.route('notifications');
});
this.route('virtual', { path: '/:slug' });
this.route('admin', function () {
@@ -49,7 +50,6 @@ Router.map(function () {
this.route('socket');
});
this.route('branding');
this.route('notifications');
this.route('two-fa-settings');
this.route('virtual', { path: '/:slug' });
this.route('organizations', function () {

View File

@@ -1,17 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from '@fleetbase/console/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Helper | get-notification-key', function (hooks) {
setupRenderingTest(hooks);
// TODO: Replace this with your real tests.
test('it renders', async function (assert) {
this.set('inputValue', '1234');
await render(hbs`{{get-notification-key this.inputValue}}`);
assert.dom(this.element).hasText('1234');
});
});

View File

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

View File

@@ -1,11 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from '@fleetbase/console/tests/helpers';
module('Unit | Route | console/admin/notifications', function (hooks) {
module('Unit | Route | console/settings/notifications', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup('route:console/admin/notifications');
let route = this.owner.lookup('route:console/settings/notifications');
assert.ok(route);
});
});

View File

@@ -1,10 +0,0 @@
import createNotificationKey from '@fleetbase/console/utils/create-notification-key';
import { module, test } from 'qunit';
module('Unit | Utility | create-notification-key', function () {
// TODO: Replace this with your real tests.
test('it works', function (assert) {
let result = createNotificationKey();
assert.ok(result);
});
});

View File

@@ -0,0 +1,10 @@
import runtimeConfig from '@fleetbase/console/utils/runtime-config';
import { module, test } from 'qunit';
module('Unit | Utility | runtime-config', function () {
// TODO: Replace this with your real tests.
test('it works', function (assert) {
let result = runtimeConfig();
assert.ok(result);
});
});

View File

@@ -0,0 +1,365 @@
app:
name: Fleetbase
terms:
new: Ново
sort: Сортиране
filter: Филтър
columns: Колони
settings: Настройки
home: Начало
admin: Админ
logout: Изход
dashboard: Табло
search: Търсене
search-input: Поле за търсене
common:
confirm: Потвърждение
edit: Редактиране
save: Запазване
save-changes: Запази промените
cancel: Отказ
2fa-config: 2FA Конфигурация
account: Акаунт
admin: Админ
branding: Брандиране
columns: Колони
dashboard: Табло
date-of-birth: Дата на раждане
delete: Изтрий
email: Имейл
filesystem: Файлова система
filter: Филтър
home: Начало
logout: Изход
mail: Поща
name: Име
new: Ново
notification-channels: Канали за известия
notifications: Известия
organization: Организация
organizations: Организации
overview: Общ преглед
phone: Вашият телефонен номер
profile: Профил
queue: Опашка
save-button-text: Запази промените
search-input: Поле за търсене
search: Търсене
services: Услуги
settings: Настройки
socket: Сокет
sort: Сортиране
two-factor: Двуфакторно
uploading: Качване...
your-profile: Вашият профил
created-at: Създаден на
country: Държава
phone-number: Телефон
status: Статус
close-and-save: Затвори и запази
users: Потребители
changelog: Дневник на промените
ok: ОК
select-file: Избери файл
back: Назад
next: Напред
continue: Продължи
done: Готово
export: Експорт
reload: Презареди
reload-data: Презареди данните
unauthorized: Неразрешено
unauthorized-to: Нямате разрешение за
unauthorized-access: Неразрешен достъп
unauthorized-access-message: Неразрешен достъп, трябва да заявите необходимите разрешения.
permissions-required-for-changes: Нямате нужните разрешения, за да направите промени.
push-notifications: Push известия
role: Роля
component:
file:
dropdown-label: Действия с файла
import-modal:
loading-message: Обработване на импорта...
drop-upload: Пуснете, за да качите
invalid: Невалидно
ready-upload: готово за качване.
upload-spreadsheets: Качване на електронни таблици
drag-drop: Плъзнете и пуснете файлове с електронни таблици върху тази зона
button-text: или изберете електронни таблици за качване
spreadsheets: електронни таблици
upload-queue: Опашка за качване
dropzone:
file: файл
drop-to-upload: Пуснете, за да качите
invalid: Невалидно
files-ready-for-upload: >-
{numOfFiles} готови за качване.
upload-images-videos: Качване на изображения и видеа
upload-documents: Качване на документи
upload-documents-files: Качване на документи и файлове
upload-avatar-files: Качване на персонализирани аватари
dropzone-supported-images-videos: Плъзнете и пуснете изображения или видео файлове в тази зона
dropzone-supported-avatars: Плъзнете и пуснете SVG или PNG файлове
dropzone-supported-files: Плъзнете и пуснете файлове в тази зона
or-select-button-text: или изберете файлове за качване.
upload-queue: Опашка за качване
uploading: Качване...
two-fa-enforcement-alert:
message: За да подобрите сигурността на акаунта си, организацията ви изисква двуфакторно удостоверяване (2FA). Активирайте 2FA в настройките на акаунта си за допълнителен слой защита.
button-text: Настройте 2FA
comment-thread:
publish-comment-button-text: Публикувай коментар
publish-reply-button-text: Публикувай отговор
reply-comment-button-text: Отговори
edit-comment-button-text: Редактирай
delete-comment-button-text: Изтрий
comment-published-ago: >-
Преди {createdAgo}
comment-input-placeholder: Напишете нов коментар...
comment-reply-placeholder: Напишете вашия отговор...
comment-input-empty-notification: Не можете да публикувате празен коментар...
comment-min-length-notification: Коментарът трябва да е поне 2 символа
dashboard:
select-dashboard: Изберете табло
create-new-dashboard: Създайте ново табло
create-a-new-dashboard: Създайте ново табло
confirm-create-dashboard: Създаване на табло!
edit-layout: Редактиране на подредбата
add-widgets: Добавяне на джаджи
delete-dashboard: Изтрийте таблото
save-dashboard: Запазете таблото
you-cannot-delete-this-dashboard: Не можете да изтриете това табло.
are-you-sure-you-want-delete-dashboard: Сигурни ли сте, че искате да изтриете {dashboardName}?
dashboard-widget-panel:
widget-name: >-
Джаджа {widgetName}
select-widgets: Изберете джаджи
close-and-save: Затвори и запази
services:
dashboard-service:
create-dashboard-success-notification: Ново табло `{dashboardName}` е създадено успешно.
delete-dashboard-success-notification: Таблото `{dashboardName}` беше изтрито.
auth:
verification:
header-title: Верификация на акаунта
title: Потвърдете своя имейл адрес
message-text: <strong>Почти готово!</strong><br> Проверете имейла си за код за потвърждение.
verification-code-text: Въведете кода за потвърждение, който сте получили по имейл.
verification-input-label: Код за потвърждение
verify-button-text: Потвърдете и продължете
didnt-receive-a-code: Все още не сте получили код?
not-sent:
message: Все още не сте получили код?
alternative-choice: Използвайте алтернативните опции по-долу, за да потвърдите акаунта си.
resend-email: Изпратете имейл отново
send-by-sms: Изпратете по SMS
two-fa:
verify-code:
verification-code: Код за потвърждение
check-title: Проверете своя имейл или телефон
check-subtitle: Изпратихме ви код за потвърждение. Въведете го по-долу, за да завършите процеса на вход.
expired-help-text: Вашият 2FA код е изтекъл. Можете да заявите нов код, ако ви е нужно още време.
resend-code: Изпрати кода отново
verify-code: Потвърдете кода
cancel-two-factor: Откажи двуфакторното удостоверяване
invalid-session-error-notification: Невалидна сесия. Моля, опитайте отново.
verification-successful-notification: Успешна верификация!
verification-code-expired-notification: Кодът за потвърждение е изтекъл. Моля, заявете нов.
verification-code-failed-notification: Верификацията не беше успешна. Моля, опитайте отново.
resend-code:
verification-code-resent-notification: Изпратен е нов код за потвърждение.
verification-code-resent-error-notification: Възникна грешка при повторното изпращане на кода. Моля, опитайте отново.
forgot-password:
success-message: Проверете имейла си, за да продължите!
is-sent:
title: Почти готово!
message: <strong>Проверете имейла си!</strong><br> Изпратихме ви магически линк, който ще ви позволи да нулирате паролата си. Линкът изтича след 15 минути.
not-sent:
title: Забравена парола?
message: <strong>Не се притеснявайте, ние ще помогнем.</strong><br> Въведете имейла, който използвате за вход в {appName}, и ще ви изпратим сигурен линк за нулиране на паролата.
form:
email-label: Вашият имейл адрес
submit-button: Добре, изпратете ми магически линк!
nevermind-button: Отказ
login:
title: Влезте в акаунта си
no-identity-notification: Пропуснахте ли да въведете своя имейл?
no-password-notification: Пропуснахте ли да въведете своята парола?
unverified-notification: Акаунтът ви трябва да бъде потвърден, за да продължите.
password-reset-required: Необходимо е нулиране на паролата, за да продължите.
failed-attempt:
message: <strong>Забравихте паролата си?</strong><br> Кликнете върху бутона по-долу, за да я нулирате.
button-text: Добре, помогнете ми!
form:
email-label: Имейл адрес
password-label: Парола
remember-me-label: Запомни ме
forgot-password-label: Забравена парола?
sign-in-button: Вход
create-account-button: Създаване на нов акаунт
slow-connection-message: Забавена връзка.
reset-password:
success-message: Паролата ви е нулирана! Влезте, за да продължите.
invalid-verification-code: Тази връзка за нулиране на паролата е невалидна или е изтекла.
title: Нулирайте паролата си
form:
code:
label: Вашият код за нулиране
help-text: Кодът за потвърждение, който получихте по имейл.
password:
label: Нова парола
help-text: Въведете парола от поне 6 символа, за да продължите.
confirm-password:
label: Потвърдете новата парола
help-text: Въведете парола от поне 6 символа, за да продължите.
submit-button: Нулирай паролата
back-button: Назад
console:
create-or-join-organization:
modal-title: Създаване или присъединяване към организация
join-success-notification: Присъединихте се към нова организация!
create-success-notification: Създадохте нова организация!
switch-organization:
modal-title: Сигурни ли сте, че искате да превключите към организацията {organizationName}?
modal-body: При потвърждение акаунтът ви ще остане вписан, но основната ви организация ще бъде сменена.
modal-accept-button-text: Да, искам да превключа
success-notification: Превключихте организацията
account:
index:
upload-new: Качете нов
phone: Вашият телефонен номер.
photos: снимки
admin:
schedule-monitor:
schedule-monitor: Монитор на графика
task-logs-for: >-
Логове на задачите за:
showing-last-count: Показване на последните {count} записа
name: Име
type: Тип
timezone: Часова зона
last-started: Последно стартиране
last-finished: Последно приключване
last-failure: Последна грешка
date: Дата
memory: Памет
runtime: Времетраене
output: Резултат
no-output: Няма резултат
config:
database:
title: Конфигурация на базата данни
filesystem:
title: Конфигурация на файловата система
mail:
title: Конфигурация на пощата
notification-channels:
title: Конфигурация на Push известия
queue:
title: Конфигурация на опашката
services:
title: Конфигурация на услугите
socket:
title: Конфигурация на сокета
branding:
title: Брандиране
icon-text: Икона
upload-new: Качете нов
reset-default: Възстанови по подразбиране
logo-text: Лого
theme: Основна тема
index:
total-users: Общо потребители
total-organizations: Общо организации
total-transactions: Общо транзакции
notifications:
title: Известия
notification-settings: Настройки за известия
organizations:
index:
title: Организации
owner-name-column: Собственик
owner-phone-column: Телефон на собственика
owner-email-column: Имейл на собственика
users-count-column: Потребители
phone-column: Телефон
email-column: Имейл
users:
title: Потребители
settings:
index:
title: Настройки на организацията
organization-name: Име на организацията
organization-description: Описание на организацията
organization-phone: Телефонен номер на организацията
organization-currency: Валута на организацията
organization-id: Идентификатор на организацията
organization-branding: Брандиране на организацията
logo: Лого
logo-help-text: Лого на вашата организация.
upload-new-logo: Качете ново лого
backdrop: Фон
backdrop-help-text: По желание можете да зададете банер или фонова картинка за организацията.
upload-new-backdrop: Качете нов фон
extensions:
title: Разширенията идват скоро!
message: Моля, проверете отново в следващите версии, докато подготвяме хранилище и пазар за разширения.
notifications:
select-all: Избери всички
mark-as-read: Маркирай като прочетени
received: >-
Получено:
message: Няма известия за показване.
invite:
for-users:
invitation-message: Поканени сте да се присъедините към {companyName}
invitation-sent-message: Поканени сте да се присъедините към организацията {companyName} в {appName}. За да приемете тази покана, въведете кода за покана, който сте получили по имейл, и натиснете Продължи.
invitation-code-sent-text: Вашият код за покана
accept-invitation-text: Приемете поканата
onboard:
index:
title: Създайте своя акаунт
welcome-title: <strong>Добре дошли в {companyName}!</strong><br />
welcome-text: Попълнете изискваните данни по-долу, за да започнете.
full-name: Пълно име
full-name-help-text: Вашето пълно име
your-email: Имейл адрес
your-email-help-text: Вашият имейл адрес
phone: Телефонен номер
phone-help-text: Вашият телефонен номер
organization-name: Име на организацията
organization-help-text: Името на вашата организация; всички услуги и ресурси ще се управляват под тази организация. По-късно можете да създавате колкото организации желаете.
password: Въведете парола
password-help-text: Вашата парола; уверете се, че е достатъчно сигурна.
confirm-password: Потвърдете паролата
confirm-password-help-text: Просто за да я потвърдите, въведете я отново.
continue-button-text: Продължи
verify-email:
header-title: Верификация на акаунта
title: Потвърдете своя имейл адрес
message-text: <strong>Почти готово!</strong><br> Проверете имейла си за код за потвърждение.
verification-code-text: Въведете кода за потвърждение, който сте получили по имейл.
verification-input-label: Код за потвърждение
verify-button-text: Потвърдете и продължете
didnt-receive-a-code: Все още не сте получили код?
not-sent:
message: Все още не сте получили код?
alternative-choice: Използвайте алтернативните опции по-долу, за да потвърдите акаунта си.
resend-email: Изпратете имейл отново
send-by-sms: Изпратете по SMS
install:
installer-header: Инсталатор
failed-message-sent: Инсталацията не беше успешна! Кликнете бутона по-долу, за да опитате отново.
retry-install: Опитайте да инсталирате отново
start-install: Стартиране на инсталацията
layout:
header:
menus:
organization:
settings: Настройки на организацията
create-or-join: Създаване или присъединяване към организации
explore-extensions: Разгледайте разширенията
user:
view-profile: Преглед на профила
keyboard-shortcuts: Покажи клавишните комбинации
changelog: Дневник на промените

View File

@@ -75,6 +75,7 @@ common:
permissions-required-for-changes: You do not have the required permissions to make changes.
push-notifications: Push Notifications
role: Role
timezone: Timezone
component:
file:
dropdown-label: File actions
@@ -229,6 +230,7 @@ console:
upload-new: Upload new
phone: Your phone number.
photos: photos
timezone: Select your timezone.
admin:
schedule-monitor:
schedule-monitor: Schedule Monitor
@@ -301,6 +303,8 @@ console:
backdrop: Backdrop
backdrop-help-text: Optional banner or background image for your organization.
upload-new-backdrop: Upload new backdrop
organization-timezone: Select the default timezone for your organization.
select-timezone: Select timezone.
extensions:
title: Extensions are coming soon!
message: Please check back in the upcoming versions as we prepare to launch the Extensions repository and marketplace.

View File

@@ -9,6 +9,10 @@ group "default" {
targets = ["app", "app-httpd"]
}
group "release" {
targets = ["fleetbase-console", "fleetbase-api"]
}
target "app" {
name = "app-${tgt}"
@@ -48,4 +52,27 @@ target "app-httpd" {
GCP ? "${REGISTRY}/app-httpd:%s" : "${REGISTRY}:app-httpd-%s",
compact(["latest", VERSION])
) : []
}
target "fleetbase-console" {
context = "./console"
dockerfile = "Dockerfile"
platforms = ["linux/amd64"]
tags = notequal("", REGISTRY) ? formatlist(
"${REGISTRY}/fleetbase-console:%s",
compact(["latest", VERSION])
) : []
}
target "fleetbase-api" {
context = "./"
dockerfile = "docker/Dockerfile"
target = "app-release"
platforms = ["linux/amd64"]
tags = notequal("", REGISTRY) ? formatlist(
"${REGISTRY}/fleetbase-api:%s",
compact(["latest", VERSION])
) : []
}

View File

@@ -31,13 +31,20 @@ services:
SOCKETCLUSTER_WORKERS: 10
SOCKETCLUSTER_BROKERS: 10
scheduler:
image: fleetbase/fleetbase-api:latest
command: ["go-crond", "--verbose", "root:./crontab"]
environment:
DATABASE_URL: "mysql://root@database/fleetbase"
QUEUE_CONNECTION: redis
CACHE_DRIVER: redis
CACHE_PATH: /fleetbase/api/storage/framework/cache
CACHE_URL: tcp://cache
REDIS_URL: tcp://cache
queue:
build:
context: .
dockerfile: docker/Dockerfile
target: events-dev
args:
ENVIRONMENT: development
image: fleetbase/fleetbase-api:latest
command: ["php", "artisan", "queue:work"]
healthcheck:
test: ["CMD", "php", "artisan", "queue:status"]
interval: 30s
@@ -52,26 +59,16 @@ services:
REDIS_URL: tcp://cache
console:
build:
context: .
dockerfile: console/Dockerfile.server-build
args:
ENVIRONMENT: development
image: fleetbase/fleetbase-console:latest
ports:
- "4200:4200"
volumes:
- console-build:/console
- ./console/fleetbase.config.json:/usr/share/nginx/html/fleetbase.config.json
application:
build:
context: .
dockerfile: docker/Dockerfile
target: app-dev
args:
ENVIRONMENT: development
GITHUB_AUTH_KEY: ${GITHUB_AUTH_KEY}
image: fleetbase/fleetbase-api:latest
volumes:
- console-build:/fleetbase/console
- ./api/.env:/fleetbase/api/.env
environment:
ENVIRONMENT: development
DATABASE_URL: "mysql://root@database/fleetbase"
@@ -100,7 +97,4 @@ services:
ports:
- "8000:80"
depends_on:
- application
volumes:
console-build:
- application

View File

@@ -1,9 +1,9 @@
# syntax = docker/dockerfile:1.2
# Base stage
FROM dunglas/frankenphp:1.5.0-php8.2-bookworm as base
FROM dunglas/frankenphp:1.5.0-php8.2-bookworm AS base
# Install packages
RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm nano \
RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm nano uuid-runtime \
&& mkdir -p /root/.ssh \
&& ssh-keyscan github.com >> /root/.ssh/known_hosts
@@ -62,10 +62,10 @@ COPY --from=ghcr.io/springload/ssm-parent:1.8 /usr/bin/ssm-parent /sbin/ssm-pare
# 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
# Add the pnpm global bin to the PATH
ENV PATH /root/.pnpm/bin:$PATH
ENV PATH=/root/.pnpm/bin:$PATH
# Set some build ENV variables
ENV LOG_CHANNEL=stdout
@@ -75,6 +75,7 @@ ENV QUEUE_CONNECTION=redis
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
ENV CONSOLE_PATH=/fleetbase/console
ENV OCTANE_SERVER=frankenphp
ENV FLEETBASE_VERSION=0.7.5
# Set environment
ARG ENVIRONMENT=production
@@ -89,6 +90,13 @@ COPY --chown=www-data:www-data ./Caddyfile $CADDYFILE_PATH
# Create /fleetbase directory and set correct permissions
RUN mkdir -p /fleetbase/api && mkdir -p /fleetbase/console && chown -R www-data:www-data /fleetbase
# Generate and store a UUID in .fleetbase-id
RUN uuidgen > /fleetbase/api/.fleetbase-id
# Export the same ID as an environment variable for use in the container
RUN export FLEETBASE_INSTANCE_ID=$(cat /fleetbase/api/.fleetbase-id) && \
echo "FLEETBASE_INSTANCE_ID=$FLEETBASE_INSTANCE_ID" >> /etc/environment
# Set working directory
WORKDIR /fleetbase/api
@@ -122,40 +130,42 @@ RUN chmod -R 755 /fleetbase/api/storage
# Set permissions for deploy script
RUN chmod +x /fleetbase/api/deploy.sh
# Scheduler base stage
FROM base as scheduler-base
# Install go-crond
RUN curl -L https://github.com/webdevops/go-crond/releases/download/23.12.0/go-crond.linux.amd64 > /usr/local/bin/go-crond && chmod +x /usr/local/bin/go-crond
COPY docker/crontab ./crontab
RUN chmod 0600 ./crontab
# Scheduler dev stage
FROM scheduler-base as scheduler-dev
FROM base AS scheduler-dev
ENTRYPOINT []
CMD ["go-crond", "--verbose", "root:./crontab"]
# Scheduler stage
FROM scheduler-base as scheduler
FROM base AS scheduler
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--"]
CMD ["go-crond", "--verbose", "root:./crontab"]
# Events stage
FROM base as events
FROM base AS events
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
CMD ["php", "artisan", "queue:work"]
# Events stage
FROM base as events-dev
FROM base AS events-dev
ENTRYPOINT []
CMD ["php", "artisan", "queue:work"]
# Application dev stage
FROM base as app-dev
FROM base AS app-dev
ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0 --watch"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0 --watch"]
# Application release stage
FROM base AS app-release
ENTRYPOINT ["docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0"]
# Application stage
FROM base as app
FROM base AS app
ENTRYPOINT ["/sbin/ssm-parent", "-c", ".ssm-parent.yaml", "run", "--", "docker-php-entrypoint"]
CMD ["sh", "-c", "php artisan octane:frankenphp --max-requests=250 --port=8000 --host=0.0.0.0"]

View File

@@ -1,154 +0,0 @@
# syntax = docker/dockerfile:1.2
# Base stage
FROM dunglas/frankenphp:1.5.0-php8.2-bookworm AS base
# Install packages
RUN apt-get update && apt-get install -y git bind9-utils mycli nodejs npm nano mariadb-server redis-server \
&& mkdir -p /root/.ssh \
&& ssh-keyscan github.com >> /root/.ssh/known_hosts
# Install PHP Extensions
RUN install-php-extensions \
pdo_mysql \
gd \
bcmath \
redis \
intl \
zip \
gmp \
apcu \
opcache \
memcached \
imagick \
# geos \
sockets \
pcntl \
@composer
# Install build dependencies for GEOS
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
pkg-config \
libgeos-dev \
libgeos++-dev \
autoconf \
build-essential \
unzip \
&& rm -rf /var/lib/apt/lists/*
# Download, extract, compile, and enable the php-geos extension
RUN curl -fsSL -o php-geos.zip \
https://github.com/libgeos/php-geos/archive/dfe1ab17b0f155cc315bc13c75689371676e02e1.zip \
&& unzip php-geos.zip \
&& rm php-geos.zip \
&& cd php-geos-* \
&& ./autogen.sh \
&& ./configure \
&& make -j"$(nproc)" install \
&& docker-php-ext-enable geos \
&& cd .. \
&& rm -rf php-geos-*
# Update PHP configurations
RUN sed -e 's/^expose_php.*/expose_php = Off/' "$PHP_INI_DIR/php.ini-production" > "$PHP_INI_DIR/php.ini" \
&& sed -i -e 's/^upload_max_filesize.*/upload_max_filesize = 600M/' -e 's/^post_max_size.*/post_max_size = 0/' \
-e 's/^memory_limit.*/memory_limit = 600M/' "$PHP_INI_DIR/php.ini"
# Install global node modules
RUN npm install -g chokidar pnpm socketcluster ember-cli npm-cli-login
# Create application directory
RUN mkdir -p /fleetbase
# Setup Console
COPY --chown=www-data:www-data ./console /fleetbase/console
WORKDIR /fleetbase/console
RUN pnpm install && pnpm build
# Setup SocketCluster
RUN socketcluster create /fleetbase/socketcluster
# Install ssm-parent
COPY --from=ghcr.io/springload/ssm-parent:1.8 /usr/bin/ssm-parent /sbin/ssm-parent
# Create the pnpm directory and set the PNPM_HOME environment variable
RUN mkdir -p ~/.pnpm
ENV PNPM_HOME=/root/.pnpm
# Add the pnpm global bin to the PATH
ENV PATH=/root/.pnpm/bin:$PATH
# Set some build ENV variables
ENV LOG_CHANNEL=stdout
ENV CACHE_DRIVER=null
ENV BROADCAST_DRIVER=socketcluster
ENV QUEUE_CONNECTION=redis
ENV CADDYFILE_PATH=/fleetbase/Caddyfile
ENV CONSOLE_PATH=/fleetbase/console
ENV OCTANE_SERVER=frankenphp
ENV DB_CONNECTION=mysql
ENV DB_HOST=127.0.0.1
ENV DB_PORT=3306
ENV DB_DATABASE=fleetbase
ENV DB_USERNAME=root
ENV DB_PASSWORD=secret
ENV REDIS_HOST=127.0.0.1
ENV REDIS_PORT=6379
ENV REDIS_CLIENT=phpredis
# Set environment
ARG ENVIRONMENT=production
ENV APP_ENV=$ENVIRONMENT
# Setup github auth
ARG GITHUB_AUTH_KEY
# Copy Caddyfile
COPY --chown=www-data:www-data ./Caddyfile $CADDYFILE_PATH
# Create /fleetbase directory and set correct permissions
RUN mkdir -p /fleetbase/api && mkdir -p /fleetbase/console && chown -R www-data:www-data /fleetbase
# Set working directory
WORKDIR /fleetbase/api
# Prepare composer cache directory
RUN mkdir -p /var/www/.cache/composer && chown -R www-data:www-data /var/www/.cache/composer
# Optimize Composer Dependency Installation
COPY --chown=www-data:www-data ./api/composer.json ./api/composer.lock /fleetbase/api/
# Pre-install Composer dependencies
RUN su www-data -s /bin/sh -c "composer install --no-scripts --optimize-autoloader --no-dev --no-cache"
# Setup application
COPY --chown=www-data:www-data ./api /fleetbase/api
# Dump autoload
RUN su www-data -s /bin/sh -c "composer dumpautoload"
# Setup composer root directory
RUN mkdir -p /root/.composer
RUN mkdir -p /fleetbase/api/.composer && chown www-data:www-data /fleetbase/api/.composer
# Setup logging
RUN mkdir -p /fleetbase/api/storage/logs/ && touch /fleetbase/api/storage/logs/laravel-$(date +'%Y-%m-%d').log
RUN chown -R www-data:www-data /fleetbase/api/storage
RUN chmod -R 755 /fleetbase/api/storage
# Set permissions and run deploy script
RUN chmod +x /fleetbase/api/deploy.sh
# RUN sh ./fleetbase/api/deploy.sh
# Install go-crond
RUN curl -L https://github.com/webdevops/go-crond/releases/download/23.12.0/go-crond.linux.amd64 > /usr/local/bin/go-crond && chmod +x /usr/local/bin/go-crond
COPY docker/crontab ./crontab
RUN chmod 0600 ./crontab
# Expose ports
EXPOSE 8000 4200 3800 3306 6379
# Execute
COPY ./docker/fullstack-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env bash
set -e
echo "🚀 Fleetbase Fullstack Container Starting..."
# Graceful shutdown handler
trap 'echo "🛑 Caught termination signal, shutting down..."; kill $(jobs -p); wait' SIGTERM SIGINT
echo "🔧 Starting MySQL service..."
service mysql start
echo "🗄️ Configuring MySQL database and user..."
mysql --user=root <<-EOSQL
CREATE DATABASE IF NOT EXISTS fleetbase;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'secret';
FLUSH PRIVILEGES;
EOSQL
echo "🧠 Waiting for MySQL to become ready..."
until mysqladmin ping --silent; do
echo "⏳ Waiting for MySQL..."
sleep 1
done
echo "✅ MySQL is ready."
echo "🔧 Starting Redis service..."
service redis-server start
echo "🧠 Waiting for Redis to become ready..."
until redis-cli ping | grep -q PONG; do
echo "⏳ Waiting for Redis..."
sleep 1
done
echo "✅ Redis is ready."
echo "📡 Launching SocketCluster on port 3800..."
node /fleetbase/socketcluster/index.js &
echo "🖥️ Serving Ember frontend on port 4200..."
npx serve -l 4200 /fleetbase/console/dist &
echo "📦 Running Fleetbase deploy script..."
sh /fleetbase/api/deploy.sh
echo "🧬 Starting Laravel API (FrankenPHP Octane) on port 8000..."
cd /fleetbase/api
exec php artisan octane:frankenphp --port=8000 --host=0.0.0.0

170
scripts/docker-install.sh Executable file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# scripts/docker-install.sh
# Fleetbase Docker installer (dev / prod aware)
# --------------------------------------------
set -euo pipefail
###############################################################################
# 1. Ask for host (default: localhost)
###############################################################################
read -rp "Enter host or IP address to bind to [localhost]: " HOST_INPUT
HOST=${HOST_INPUT:-localhost}
echo "➜ Using host: $HOST"
###############################################################################
# 2. Ask for environment (development | production)
###############################################################################
while true; do
read -rp "Choose environment (development / production) [development]: " ENV_INPUT
ENV_INPUT=$(echo "$ENV_INPUT" | tr '[:upper:]' '[:lower:]')
case "$ENV_INPUT" in
""|d|dev|development) ENVIRONMENT=development; break ;;
p|prod|production) ENVIRONMENT=production; break ;;
*) echo "Please type either 'development' or 'production'." ;;
esac
done
echo "➜ Environment: $ENVIRONMENT"
USE_HTTPS=false
APP_DEBUG=true
SC_SECURE=false
if [[ "$ENVIRONMENT" == "production" ]]; then
USE_HTTPS=true
APP_DEBUG=false
SC_SECURE=true
fi
###############################################################################
# 3. Determine project root no matter where script is called from
###############################################################################
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
cd "$PROJECT_ROOT"
###############################################################################
# 4. Generate a fresh Laravel APP_KEY
###############################################################################
if ! command -v openssl >/dev/null 2>&1; then
echo "✖ openssl is required but not found. Install it and retry." >&2
exit 1
fi
APP_KEY="base64:$(openssl rand -base64 32 | tr -d '\n')"
echo "✔ Generated APP_KEY"
###############################################################################
# 5. Ensure dockercompose.override.yml is present & updated
###############################################################################
OVERRIDE_FILE="docker-compose.override.yml"
# url helpers
SCHEME_API=$([[ "$USE_HTTPS" == true ]] && echo "https" || echo "http")
SCHEME_CONSOLE=$([[ "$USE_HTTPS" == true ]] && echo "https" || echo "http")
update_override_with_yq() {
yq -i "
.services.application.environment.APP_KEY = \"$APP_KEY\" |
.services.application.environment.CONSOLE_HOST = \"$SCHEME_CONSOLE://$HOST:4200\" |
.services.application.environment.ENVIRONMENT = \"$ENVIRONMENT\" |
.services.application.environment.APP_DEBUG = \"$APP_DEBUG\"
" "$OVERRIDE_FILE"
echo "$OVERRIDE_FILE updated (yq)"
}
create_override() {
cat > "$OVERRIDE_FILE" <<YML
services:
application:
environment:
APP_KEY: "$APP_KEY"
CONSOLE_HOST: "$SCHEME_CONSOLE://$HOST:4200"
ENVIRONMENT: "$ENVIRONMENT"
APP_DEBUG: "$APP_DEBUG"
YML
echo "$OVERRIDE_FILE written"
}
if [[ -f "$OVERRIDE_FILE" ]]; then
if command -v yq >/dev/null 2>&1; then
update_override_with_yq
else
cp "$OVERRIDE_FILE" "${OVERRIDE_FILE}.bak.$(date +%Y%m%d%H%M%S)"
echo " Existing $OVERRIDE_FILE backed up (no yq found — recreating)"
create_override
fi
else
create_override
fi
###############################################################################
# 6. Write console/fleetbase.config.json atomically
###############################################################################
CONFIG_DIR="console"
CONFIG_PATH="$CONFIG_DIR/fleetbase.config.json"
mkdir -p "$CONFIG_DIR"
cat > "${CONFIG_PATH}.tmp" <<JSON
{
"API_HOST": "$SCHEME_API://$HOST:8000",
"SOCKETCLUSTER_HOST": "$HOST",
"SOCKETCLUSTER_PORT": "38000",
"SOCKETCLUSTER_SECURE": "$SC_SECURE"
}
JSON
mv -f "${CONFIG_PATH}.tmp" "$CONFIG_PATH"
echo "$CONFIG_PATH updated"
###############################################################################
# 7. Start stack, wait for DB, then run deploy
###############################################################################
echo "⏳ Starting Fleetbase containers..."
docker compose up -d
###############################################################################
# 7a. Wait for the database container to be ready
###############################################################################
DB_SERVICE="database" # ← change if your dockercompose uses a different name
DB_WAIT_TIMEOUT=60 # seconds
echo "⏳ Waiting for “$DB_SERVICE” to become ready (timeout: ${DB_WAIT_TIMEOUT}s)…"
DB_CONTAINER=$(docker compose ps -q "$DB_SERVICE")
if [ -z "$DB_CONTAINER" ]; then
echo "✖ Cannot find a running container for service \"$DB_SERVICE\". Check dockercompose.yml."
exit 1
fi
# If the service defines a HEALTHCHECK we can rely on it…
if docker inspect -f '{{.State.Health.Status}}' "$DB_CONTAINER" &>/dev/null; then
SECONDS=0
until [ "$(docker inspect -f '{{.State.Health.Status}}' "$DB_CONTAINER")" = "healthy" ]; do
if [ "$SECONDS" -ge "$DB_WAIT_TIMEOUT" ]; then
echo "✖ Timed out waiting for the database to become healthy."
exit 1
fi
sleep 2
done
sleep 12
else
# Fallback: use mysqladmin ping (works for MySQL / MariaDB)
SECONDS=0
until docker compose exec "$DB_SERVICE" sh -c "mysqladmin --silent --wait=1 -uroot -h127.0.0.1 ping" &>/dev/null; do
if [ "$SECONDS" -ge "$DB_WAIT_TIMEOUT" ]; then
echo "✖ Timed out waiting for the database to accept connections."
exit 1
fi
sleep 2
done
fi
echo "✔ Database is ready."
###############################################################################
# 7b. Run the deploy script inside the application container
###############################################################################
echo "⏳ Running deploy script inside the application container..."
docker compose exec application bash -c "./deploy.sh"
docker compose up -d
echo
echo "🏁 Fleetbase is up!"
printf " API → %s://%s:8000\n" "$SCHEME_API" "$HOST"
printf " Console → %s://%s:4200\n\n" "$SCHEME_CONSOLE" "$HOST"

View File

@@ -1,55 +0,0 @@
#!/bin/bash
# Find the root directory of your repository
root_dir=$(git rev-parse --show-toplevel)
# Check if the packages directory exists
packages_dir="$root_dir/packages"
if [ ! -d "$packages_dir" ]; then
echo "Packages directory not found."
exit 1
fi
# Initialize flags
remove_lock=false
remove_modules=false
for arg in "$@"
do
case $arg in
--remove-lock)
remove_lock=true
;;
--remove-modules)
remove_modules=true
;;
esac
done
# Navigate to the packages directory
cd "$packages_dir"
# Find all child directories and run pnpm install if package.json exists
for dir in */; do
if [[ -f "${dir}package.json" ]]; then
echo "Running pnpm install in $dir"
# Remove pnpm-lock.yaml if the option is set
if [ "$remove_lock" = true ] && [ -f "${dir}pnpm-lock.yaml" ]; then
echo "Removing pnpm-lock.yaml in $dir"
rm "${dir}pnpm-lock.yaml"
fi
# Remove ./node_modules if the option is set
if [ "$remove_modules" = true ] && [ -d "${dir}node_modules" ]; then
echo "Removing /node_modules in $dir"
rm -rf "${dir}node_modules"
fi
cd "$dir"
pnpm install
cd "$packages_dir" # Go back to the packages directory
else
echo "No package.json found in $dir, skipping..."
fi
done

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# Exit the script as soon as a command fails
set -e
echo "Switching to the main branch..."
# git checkout main
echo "Updating submodules..."
git submodule update --init --recursive
echo "Updating console..."
cd console
git checkout main
git pull
cd ..
echo "Building Docker images..."
docker-compose build console
docker-compose build application
echo "Update completed. Run \`docker-compose up -d\` to launch!"