@fleetbase/ember-core 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Lookup user's IP address and geolocation information using geoiplookup.io
3
+ *
4
+ * This method calls the geoiplookup.io API directly from the browser to get
5
+ * accurate user location data, avoiding the issue of server-side IP lookup
6
+ * returning the server's location instead of the user's location.
7
+ *
8
+ * @param {Object} options - Configuration options
9
+ * @param {number} options.timeout - Request timeout in milliseconds (default: 5000)
10
+ * @param {boolean} options.cache - Whether to cache the result (default: true)
11
+ * @returns {Promise<Object>} User's IP and geolocation data
12
+ *
13
+ * @example
14
+ * const whois = await lookupUserIp();
15
+ * console.log(whois.city); // "New York"
16
+ * console.log(whois.country_code); // "US"
17
+ */
18
+ export default async function lookupUserIp(options = {}) {
19
+ const { timeout = 5000, cache = true } = options;
20
+
21
+ // Check cache first if enabled
22
+ if (cache) {
23
+ const cached = getCachedWhois();
24
+ if (cached) {
25
+ return cached;
26
+ }
27
+ }
28
+
29
+ // Try multiple APIs with fallback
30
+ const apis = [
31
+ {
32
+ url: 'https://json.geoiplookup.io/',
33
+ normalize: normalizeGeoIPLookup,
34
+ },
35
+ {
36
+ url: 'https://ipapi.co/json/',
37
+ normalize: normalizeIPApi,
38
+ },
39
+ ];
40
+
41
+ for (const api of apis) {
42
+ try {
43
+ const controller = new AbortController();
44
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
45
+
46
+ const response = await fetch(api.url, {
47
+ signal: controller.signal,
48
+ headers: {
49
+ Accept: 'application/json',
50
+ },
51
+ });
52
+
53
+ clearTimeout(timeoutId);
54
+
55
+ if (!response.ok) {
56
+ console.warn(`[lookupUserIp] ${api.url} returned ${response.status}`);
57
+ continue;
58
+ }
59
+
60
+ const data = await response.json();
61
+ const normalized = api.normalize(data);
62
+
63
+ // Cache the result if enabled
64
+ if (cache) {
65
+ cacheWhois(normalized);
66
+ }
67
+
68
+ return normalized;
69
+ } catch (error) {
70
+ console.warn(`[lookupUserIp] ${api.url} failed:`, error.message);
71
+ // Continue to next API
72
+ }
73
+ }
74
+
75
+ // All APIs failed, return fallback
76
+ console.error('[lookupUserIp] All IP lookup APIs failed, using fallback data');
77
+ return getFallbackWhois();
78
+ }
79
+
80
+ /**
81
+ * Normalize geoiplookup.io response to match Fleetbase whois format
82
+ */
83
+ function normalizeGeoIPLookup(data) {
84
+ return {
85
+ ip: data.ip,
86
+ city: data.city,
87
+ region: data.region,
88
+ country_code: data.country_code,
89
+ country_name: data.country_name,
90
+ continent_code: data.continent_code,
91
+ continent_name: data.continent_name,
92
+ latitude: data.latitude,
93
+ longitude: data.longitude,
94
+ postal_code: data.postal_code,
95
+ timezone: data.timezone_name,
96
+ currency: {
97
+ code: data.currency_code,
98
+ name: data.currency_name,
99
+ },
100
+ languages: [
101
+ {
102
+ code: data.language_code,
103
+ name: data.language_name,
104
+ },
105
+ ],
106
+ isp: data.isp,
107
+ org: data.org,
108
+ asn: data.asn,
109
+ connection_type: data.connection_type,
110
+ // Metadata
111
+ _source: 'geoiplookup.io',
112
+ _timestamp: Date.now(),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Normalize ipapi.co response to match Fleetbase whois format
118
+ */
119
+ function normalizeIPApi(data) {
120
+ return {
121
+ ip: data.ip,
122
+ city: data.city,
123
+ region: data.region,
124
+ country_code: data.country_code,
125
+ country_name: data.country_name,
126
+ continent_code: data.continent_code,
127
+ continent_name: null,
128
+ latitude: data.latitude,
129
+ longitude: data.longitude,
130
+ postal_code: data.postal,
131
+ timezone: data.timezone,
132
+ currency: {
133
+ code: data.currency,
134
+ name: data.currency_name,
135
+ },
136
+ languages: data.languages ? data.languages.split(',').map((lang) => ({ code: lang.trim(), name: lang.trim() })) : [],
137
+ isp: data.org,
138
+ org: data.org,
139
+ asn: data.asn,
140
+ connection_type: null,
141
+ // Metadata
142
+ _source: 'ipapi.co',
143
+ _timestamp: Date.now(),
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Get cached whois data from localStorage
149
+ */
150
+ function getCachedWhois() {
151
+ try {
152
+ const cached = localStorage.getItem('fleetbase:whois');
153
+ if (!cached) {
154
+ return null;
155
+ }
156
+
157
+ const data = JSON.parse(cached);
158
+ const age = Date.now() - data._timestamp;
159
+ const maxAge = 60 * 60 * 1000; // 1 hour
160
+
161
+ if (age > maxAge) {
162
+ localStorage.removeItem('fleetbase:whois');
163
+ return null;
164
+ }
165
+
166
+ return data;
167
+ } catch (error) {
168
+ console.error('[getCachedWhois] Error reading cache:', error);
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Cache whois data to localStorage
175
+ */
176
+ function cacheWhois(data) {
177
+ try {
178
+ localStorage.setItem('fleetbase:whois', JSON.stringify(data));
179
+ } catch (error) {
180
+ console.error('[cacheWhois] Error writing cache:', error);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get fallback whois data when all APIs fail
186
+ */
187
+ function getFallbackWhois() {
188
+ // Try to get browser language and timezone as fallback
189
+ const browserLang = navigator.language || 'en-US';
190
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
191
+
192
+ return {
193
+ ip: null,
194
+ city: null,
195
+ region: null,
196
+ country_code: null,
197
+ country_name: null,
198
+ continent_code: null,
199
+ continent_name: null,
200
+ latitude: null,
201
+ longitude: null,
202
+ postal_code: null,
203
+ timezone: timezone,
204
+ currency: {
205
+ code: null,
206
+ name: null,
207
+ },
208
+ languages: [
209
+ {
210
+ code: browserLang.split('-')[0],
211
+ name: browserLang,
212
+ },
213
+ ],
214
+ isp: null,
215
+ org: null,
216
+ asn: null,
217
+ connection_type: null,
218
+ _source: 'fallback',
219
+ _timestamp: Date.now(),
220
+ };
221
+ }
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/ember-core/services/events';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-core",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-core",
@@ -1,294 +0,0 @@
1
- # Boot Sequence Refactor Guide
2
-
3
- ## Overview
4
-
5
- This guide provides the steps to refactor the application boot sequence to enable true lazy loading and move away from the old `bootEngines` mechanism that loads all extensions at startup.
6
-
7
- ## Understanding the Extension Loading Flow
8
-
9
- The Fleetbase application has a three-tier extension loading system:
10
-
11
- 1. **pnpm Installation**: All extensions are installed via pnpm, making them available to the application
12
- 2. **System Configuration**: Extensions defined in `fleetbase.config.js` or `EXTENSIONS` environment variable are loaded globally
13
- 3. **User Permissions**: Individual users can install/uninstall extensions, which affects what loads for them specifically
14
-
15
- Only extensions that are both installed AND enabled (via config or user permissions) will be initialized.
16
-
17
- ## The Goal
18
-
19
- Stop loading all extension code at boot time. Instead:
20
- - Load only the `extension.js` files (metadata registration)
21
- - Keep engine bundles lazy-loaded (loaded on-demand when routes are visited)
22
- - Preserve the `engines` property required by ember-engines for lazy loading
23
-
24
- ## Key Changes
25
-
26
- 1. **Keep `app.engines` property**: Required by ember-engines for lazy loading
27
- 2. **Create new `initialize-universe` instance initializer**: Loads `extension.js` files and registers metadata
28
- 3. **Remove `bootEngines` calls**: No more manual engine booting at startup
29
-
30
- ## Step-by-Step Guide
31
-
32
- ### Step 1: Update `app.js` to Preserve Engines Property
33
-
34
- The `engines` property is **required** by ember-engines to enable lazy loading. Keep the existing structure but remove any `bootEngines` calls.
35
-
36
- **Current `app.js` (fleetbase/console/app/app.js):**
37
-
38
- ```javascript
39
- import Application from '@ember/application';
40
- import Resolver from 'ember-resolver';
41
- import loadInitializers from 'ember-load-initializers';
42
- import config from '@fleetbase/console/config/environment';
43
- import loadExtensions from '@fleetbase/ember-core/utils/load-extensions';
44
- import mapEngines from '@fleetbase/ember-core/utils/map-engines';
45
- import loadRuntimeConfig from '@fleetbase/console/utils/runtime-config';
46
- import applyRouterFix from './utils/router-refresh-patch';
47
-
48
- export default class App extends Application {
49
- modulePrefix = config.modulePrefix;
50
- podModulePrefix = config.podModulePrefix;
51
- Resolver = Resolver;
52
- extensions = [];
53
- engines = {}; // ← KEEP THIS! Required by ember-engines
54
-
55
- async ready() {
56
- applyRouterFix(this);
57
- const extensions = await loadExtensions();
58
-
59
- this.extensions = extensions;
60
- this.engines = mapEngines(extensions); // ← KEEP THIS! Maps extensions to engines
61
- }
62
- }
63
-
64
- document.addEventListener('DOMContentLoaded', async () => {
65
- await loadRuntimeConfig();
66
- loadInitializers(App, config.modulePrefix);
67
-
68
- let fleetbase = App.create();
69
- fleetbase.deferReadiness();
70
- fleetbase.boot();
71
- });
72
- ```
73
-
74
- **What to Keep:**
75
- - ✅ `extensions` property - tracks which extensions are enabled
76
- - ✅ `engines` property - required by ember-engines for lazy loading
77
- - ✅ `loadExtensions()` - determines which extensions to load based on config + user permissions
78
- - ✅ `mapEngines()` - creates the engines object required by ember-engines
79
-
80
- **What Changes:**
81
- - ❌ Remove any `bootEngines()` calls (if present in instance initializers)
82
- - ❌ Remove `initialize-widgets.js` instance initializer (logic moves to `extension.js`)
83
-
84
- ### Step 2: Remove Old Instance Initializers
85
-
86
- Delete the following instance initializers that perform eager engine loading:
87
-
88
- **Files to Delete:**
89
- - `app/instance-initializers/load-extensions.js` (if it calls `bootEngines`)
90
- - `app/instance-initializers/initialize-widgets.js` (widgets now registered via `extension.js`)
91
-
92
- ### Step 3: Create New `initialize-universe` Initializer
93
-
94
- Create a new instance initializer at `app/instance-initializers/initialize-universe.js`:
95
-
96
- ```javascript
97
- import { getOwner } from '@ember/application';
98
- import { scheduleOnce } from '@ember/runloop';
99
-
100
- /**
101
- * Initializes the Universe by loading and executing extension.js files
102
- * from all enabled extensions. This replaces the old bootEngines mechanism.
103
- *
104
- * Key differences from old approach:
105
- * - Only loads extension.js files (small, metadata only)
106
- * - Does NOT load engine bundles (those lazy-load when routes are visited)
107
- * - Respects both system config and user permissions
108
- *
109
- * @param {ApplicationInstance} appInstance The application instance
110
- */
111
- export function initialize(appInstance) {
112
- const universe = appInstance.lookup('service:universe');
113
- const owner = getOwner(appInstance);
114
- const app = owner.application;
115
-
116
- // Set application instance on universe
117
- universe.applicationInstance = appInstance;
118
-
119
- // Get the list of enabled extensions from the app
120
- // This list already respects config + user permissions via loadExtensions()
121
- const extensions = app.extensions || [];
122
-
123
- // Load and execute extension.js from each enabled extension
124
- extensions.forEach(extensionName => {
125
- try {
126
- // Dynamically require the extension.js file
127
- // This is a small file with only metadata, not the full engine bundle
128
- const setupExtension = require(`${extensionName}/extension`).default;
129
-
130
- if (typeof setupExtension === 'function') {
131
- // Execute the extension setup function
132
- // This registers menus, widgets, hooks, etc. as metadata
133
- setupExtension(appInstance, universe);
134
- }
135
- } catch (error) {
136
- // Silently fail if extension.js doesn't exist
137
- // Extensions can migrate gradually to the new pattern
138
- // console.warn(`Could not load extension.js for ${extensionName}:`, error);
139
- }
140
- });
141
-
142
- // Execute any boot callbacks
143
- scheduleOnce('afterRender', universe, 'executeBootCallbacks');
144
- }
145
-
146
- export default {
147
- name: 'initialize-universe',
148
- initialize
149
- };
150
- ```
151
-
152
- ### Step 4: Verify `router.js` Engine Mounting
153
-
154
- Your `prebuild.js` script already handles mounting engines in `router.js`. Verify that engines are mounted like this:
155
-
156
- ```javascript
157
- // This is generated by prebuild.js
158
- this.mount('@fleetbase/fleetops-engine', { as: 'console.fleet-ops' });
159
- this.mount('@fleetbase/customer-portal-engine', { as: 'console.customer-portal' });
160
- ```
161
-
162
- **Important**: The `this.mount()` calls are what enable ember-engines lazy loading. When a user navigates to a route, ember-engines automatically loads the engine bundle on-demand.
163
-
164
- ### Step 5: Migrate Extensions to `extension.js` Pattern
165
-
166
- For each extension, create an `addon/extension.js` file that registers metadata without importing components:
167
-
168
- **Example: FleetOps `addon/extension.js`**
169
-
170
- ```javascript
171
- import { MenuItem, MenuPanel, Widget, ExtensionComponent } from '@fleetbase/ember-core/contracts';
172
-
173
- export default function (app, universe) {
174
- // Register admin menu panel
175
- universe.registerAdminMenuPanel(
176
- 'Fleet-Ops',
177
- new MenuPanel({
178
- title: 'Fleet-Ops',
179
- icon: 'route',
180
- items: [
181
- new MenuItem({
182
- title: 'Navigator App',
183
- icon: 'location-arrow',
184
- component: new ExtensionComponent('@fleetbase/fleetops-engine', 'components/admin/navigator-app')
185
- }),
186
- new MenuItem({
187
- title: 'Avatar Management',
188
- icon: 'images',
189
- component: new ExtensionComponent('@fleetbase/fleetops-engine', 'components/admin/avatar-management')
190
- })
191
- ]
192
- })
193
- );
194
-
195
- // Register widgets
196
- universe.registerDefaultWidget(
197
- new Widget({
198
- widgetId: 'fleet-ops-metrics',
199
- name: 'Fleet-Ops Metrics',
200
- description: 'Key metrics from Fleet-Ops',
201
- icon: 'truck',
202
- component: new ExtensionComponent('@fleetbase/fleetops-engine', 'components/widget/metrics'),
203
- grid_options: { w: 12, h: 12, minW: 8, minH: 12 }
204
- })
205
- );
206
-
207
- // Register hooks
208
- universe.registerHook(
209
- new Hook({
210
- name: 'application:before-model',
211
- handler: (session, router) => {
212
- // Custom logic here
213
- },
214
- priority: 10
215
- })
216
- );
217
- }
218
- ```
219
-
220
- **Key Points:**
221
- - ❌ NO `import MyComponent from './components/my-component'` - this would load the engine!
222
- - ✅ Use `ExtensionComponent` with engine name + path for lazy loading
223
- - ✅ Use contract classes (`MenuItem`, `Widget`, `Hook`) for type safety
224
-
225
- See [UNIVERSE_REFACTOR_MIGRATION_GUIDE.md](./UNIVERSE_REFACTOR_MIGRATION_GUIDE.md) for detailed migration examples.
226
-
227
- ## How Lazy Loading Works with This Approach
228
-
229
- 1. **App Boot**: Application boots with `app.engines` property set
230
- 2. **`initialize-universe`**: Loads small `extension.js` files via `require()`
231
- 3. **Metadata Registration**: Extensions register menus, widgets, hooks (no component code loaded)
232
- 4. **User Navigation**: User navigates to `/console/fleet-ops`
233
- 5. **Ember-Engines**: Detects route is in a mounted engine, lazy-loads the engine bundle
234
- 6. **Component Resolution**: `<LazyEngineComponent>` resolves components from loaded engine
235
-
236
- ## Performance Impact
237
-
238
- | Metric | Before (bootEngines) | After (Lazy Loading) |
239
- |--------|---------------------|---------------------|
240
- | Initial Load Time | 10-40 seconds | <1 second |
241
- | Initial Bundle Size | Core + All Engines | Core + extension.js files |
242
- | Engine Loading | All at boot | On-demand when route visited |
243
- | Memory Usage | All engines in memory | Only visited engines in memory |
244
-
245
- ## Ember-Engines Requirements
246
-
247
- According to [ember-engines documentation](https://github.com/ember-engines/ember-engines):
248
-
249
- > **Lazy loading** - An engine can allow its parent to boot with only its routing map loaded. The rest of the engine can be loaded only as required (i.e. when a route in an engine is visited). This allows applications to boot faster and limit their memory consumption.
250
-
251
- **Required for lazy loading:**
252
- 1. ✅ `app.engines` property must be set (maps extension names to engine modules)
253
- 2. ✅ Engines must be mounted in `router.js` via `this.mount()`
254
- 3. ✅ Engine's `index.js` must have `lazyLoading: true` (default)
255
-
256
- **What breaks lazy loading:**
257
- 1. ❌ Calling `owner.lookup('engine:my-engine')` at boot time
258
- 2. ❌ Importing components from engines in `extension.js`
259
- 3. ❌ Manual `bootEngines()` calls
260
-
261
- ## Troubleshooting
262
-
263
- ### Extension not loading
264
- - Check that extension is in `app.extensions` array
265
- - Verify `extension.js` file exists and exports a function
266
- - Check browser console for errors
267
-
268
- ### Components not rendering
269
- - Ensure `ExtensionComponent` has correct engine name and path
270
- - Verify engine is mounted in `router.js`
271
- - Check that `<LazyEngineComponent>` is used in templates
272
-
273
- ### Engines loading at boot
274
- - Remove any `owner.lookup('engine:...')` calls from initializers
275
- - Remove component imports from `extension.js`
276
- - Verify no `bootEngines()` calls remain
277
-
278
- ## Migration Checklist
279
-
280
- - [ ] Update `app.js` to keep `engines` property
281
- - [ ] Remove old instance initializers (`load-extensions.js`, `initialize-widgets.js`)
282
- - [ ] Create new `initialize-universe.js` instance initializer
283
- - [ ] Verify `router.js` has `this.mount()` calls for all engines
284
- - [ ] Create `extension.js` for each extension
285
- - [ ] Replace component imports with `ExtensionComponent` definitions
286
- - [ ] Test lazy loading in browser dev tools (Network tab)
287
- - [ ] Verify initial bundle size reduction
288
- - [ ] Test all extension functionality still works
289
-
290
- ## References
291
-
292
- - [Ember Engines Guide](https://guides.emberjs.com/v5.6.0/applications/ember-engines/)
293
- - [ember-engines GitHub](https://github.com/ember-engines/ember-engines)
294
- - [Ember Engines RFC](https://github.com/emberjs/rfcs/blob/master/text/0010-engines.md)