Commit 4cb7693
fix(map): expose L_.Map_ before makeLayers to prevent race condition (#986)
* fix: mobile TimeUI opens in tool panel with header, end time, expanded rows
- MobileTimeUIToggle now opens/closes the tool panel via ToolController_
- Closes any active tool before showing TimeUI
- Forces expanded state when opening
- CSS hides start time inputs, positions expanded content properly
- Overrides absolute positioning of expanded content for tool panel flow
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: rewrite separated tools system from jQuery to React components
- Add separatedToolsList/activeSeparatedTools state to Zustand uiStore
- Rewrite SeparatedTools.jsx with glassmorphism panels, CSS Module styling
- Replace SepToolsContainer (setInterval hack) with SepToolButton/SepToolsSection
- Remove ~170 lines of jQuery DOM construction from ToolController_.js
- Fix hardcoded rgba(26,26,27,0.88) to theme-aware var(--color-a-rgb)
- Remove separated tool entries from themeApplier.js
- Remove separated tool overrides from FloatingElements.css
- Move Legend CSS overrides from Toolbar.module.css to SeparatedTools.module.css
- Remove jQuery active-state manipulation from IdentifierTool.js
- Add store sync in Map_.js displayOnStart logic
- Preserve all DOM IDs for backward compatibility (mmgisAPI, tool make())
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.28-20260501 [version bump]
* fix: TimeUI mobile checks — use Zustand store instead of L_.UserInterface_
L_.UserInterface_ is null when TimeUI.init() runs (TimeControl.init is called
before L_.link sets UserInterface_). All 16 isMobile checks now read from
useUIStore.getState().isMobile which is set at startup.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: move displayOnStart logic from Map_.js to ToolController_.finalizeTools()
- Map_ no longer references specific tools (LegendTool)
- displayOnStart is now handled generically for all separated tools
- Added DOM element polling (tryMake) to handle React render timing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* revert: remove all TimeUI-related mobile changes
Reverts TimeUI.js and BottomBar.js to development base.
Restores #timeUI DOM removal in UserInterfaceBridge.fina().
Removes MobileTimeUIToggle component from Toolbar.jsx.
Removes TimeUI mobile CSS overrides from UserInterfaceMobile_.css.
Non-TimeUI refinements (toolbar height, scalebar positioning, etc.) preserved.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* simplify: remove DOM polling, use simple setTimeout(0) for auto-open
LegendTool handles its own content lifecycle via subscribeOnLayerToggle.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: mobile TimeUI — fix isMobile detection, staging container, toolbar toggle
- TimeUI.js: import useUIStore and replace all 16 L_.UserInterface_?.isMobile
checks with useUIStore.getState().isMobile (L_.UserInterface_ is null when
TimeUI.init() runs, so mobile conditionals were dead code)
- TimeUI.js: stage mobile #timeUI in hidden #timeUIMobileStaging instead of
placing directly in #tools (which gets cleared by other tools)
- UserInterfaceBridge.js: stop removing #timeUI from DOM on mobile
- Toolbar.jsx: add MobileTimeUIToggle that moves #timeUI between staging and
#tools, opens/closes tool panel via ToolController_
- BottomBar.js: hide TimeUI toggle from settings modal on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: rescue #timeUI back to staging when another tool opens
Subscribe to activeToolName changes — when a tool becomes active while
TimeUI is showing, move #timeUI back to #timeUIMobileStaging before
the new tool's make() clears #tools.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: remove separatedTool/justification config toggles, fix review issues
- Remove separatedTool checkbox and justification dropdown from Legend
and Identifier config.json (these are always separated, not configurable)
- Remove justification property/code from LegendTool.js, IdentifierTool.js
- Simplify Globe_.js separated tool count (no justification filter)
- Remove justification from Reference-Mission config blueprint
- Update LegendTool help docs and Legend.md documentation
- Add --color-a-rgb fallback (29,31,32) in SeparatedTools.module.css
- Add display:none !important to .panelIdentifier to prevent 12px gap
- Update e2e test comment
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: circular import in TimeUI.js, toolbar/bottomFloatingBar position sync
- TimeUI.js: replace top-level useUIStore import with lazy _getUIStore()
accessor to avoid 'Cannot access useUIStore before initialization'
circular import error at _remakeTimeSlider
- SplitScreens.jsx: skip #timeUI reparenting observer on mobile (mobile
uses MobileTimeUIToggle to manage #timeUI placement in #tools)
- BottomElementPositioner.jsx: unify mobile transition to 0.3s (matches
toolsWrapper and toolbar), guard pxIsTools against undefined
- Toolbar.jsx: align toolbar transition to 0.3s ease-out
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* LegendTool fix empty message
* chore: remove separated tools offset logic from Globe_.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: skip _makeHistogram on mobile (no timeline slider, timestamps unset)
_makeHistogram renders inside the timeline slider which doesn't exist
on mobile. Without it, _timelineStartTimestamp is NaN, causing
'Invalid time value' RangeError at toISOString().
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI — populate expanded rows, fix Invalid date, fix panel height
- TimeUI.js attachEvents: use _initialStart/_initialEnd on mobile (same
as desktop) instead of L_.TimeControl_ which isn't set yet at init time.
Fixes 'Invalid date' in start/end time inputs.
- TimeUI.js fina: set expanded=true on mobile and call _populateExpandedRows()
so year/month/day/hour rows actually render. Removed position:absolute and
pointer-events:none overrides.
- Toolbar.jsx: set tool panel height to 217px (TimeUI.height) instead of
45% viewport — matches actual TimeUI content height.
- UserInterfaceMobile_.css: expanded content flows naturally (position:relative),
hide start time inputs, allow overflow scroll, flex-wrap topbar.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI justify-content center, restore toolbar border-bottom
- Add justify-content: center to #mmgisTimeUIMain on mobile
- Remove border-bottom: none override so toolbar keeps its default border
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile TimeUI overflow hidden, scalebar/compass fixed at 40px offset
- #timeUI overflow-y: hidden (was auto, causing 2px scroll)
- Scalebar/compass/map controls stay at fixed 40px offset (above toolbar)
regardless of tool panel state — no longer shift up by pxIsTools
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Implement multi-tier knowledge architecture
- Restructure AGENTS.md from 745 lines to 106 lines (Tier 1: essential context)
- Create knowledge/ directory with 30+ wiki-style documentation files (Tier 2: deep knowledge)
- Create knowledge/reference/ with 8 detailed reference files (Tier 3: lookup material)
- Move AI-GETTING-STARTED.md and AI-DEVELOPMENT.md to knowledge/
- Update all file references in .specify/templates and blueprints
- Create knowledge/README.md as the full knowledge base index
- Create knowledge/reference/README.md as reference material index
Three-tier knowledge discovery system:
Tier 1: AGENTS.md (~106 lines) - scannable in <2 minutes
Tier 2: knowledge/*.md - deep knowledge on architecture, tools, APIs, DB, infra
Tier 3: knowledge/reference/*.md - coding conventions, API reference, troubleshooting
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.29-20260501 [version bump]
* fix: mobile toolbar active button style matches desktop, fix icon alignment
- All mobile toolbar buttons (ToolButton, MobileCoordButton, MobileTimeUIToggle)
now use display:flex with align-items/justify-content center for proper
vertical icon centering
- MobileCoordButton: changed 'active' class to 'toolButtonActive' to match
the global CSS active style (color-mmgis + color-i background)
- Removed inline color overrides so CSS .toolButtonActive takes effect
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add Devin knowledge notes from past MMGIS sessions
Include curated lessons learned from past Devin sessions:
- CI/CD: ignore build-arm64/amd64 failures, focus on required checks
- Child sessions: no separate PRs when consolidating
- ENV triple-update rule (.env, sample.env, ENVs.md)
- Error handling: use logger with infrastructure_error for fatal startup errors
- Path traversal security: stay within /Missions, handle subpath serving
- Database initialization architecture and migration patterns
- API authentication behavior across AUTH modes
- Auto-generated MMGIS concept index
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar active button style, icon alignment, tool deactivation
- Active toolbar buttons get desktop-matching margin (1px 0) and
border-radius (8px) via .toolButton.toolButtonActive CSS rule
- Removed line-height: 40px from .toolButton (flex centering handles
vertical alignment, line-height was pushing icons up)
- MobileCoordButton now watches activeToolName store and deactivates
when another tool opens (fixes coords staying active)
- MobileTimeUIToggle sets activeToolName='MobileTimeUI' when opening
so coords/other buttons can detect it and deactivate
- MobileTimeUIToggle clears activeToolName when closing
- Both custom buttons skip self-deactivation via name check
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix cross-references: convert backtick refs to markdown links, add Devin knowledge notes
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon height 40px, button margins for active padding
- #toolbar .toolButton i: height 40px fixes icon vertical alignment
- #toolbar .toolButton: margin 0 2px gives spacing between buttons
- #toolbar .toolButton.toolButtonActive: margin 1px 2px so active
background has visual padding around the icon
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename knowledge/ to .knowledge/ for consistency with .specify/ convention
Dot-prefix signals agent infrastructure (not source code), consistent with
.specify/, .github/, .vscode/ conventions. All cross-references updated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: mobile toolbar icon line-height 40px, active button padding via height
- Coord and TimeUI button <i> icons get line-height: 40px
- Active buttons: height 34px (vs 40px toolbar) creates visual padding
around the active background, centered by flex align-items
- Buttons get margin: 0 1px for horizontal spacing
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix broken cross-reference: 06.2 -> 06.1-configure-rest-api.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: close active tool + cancel deferred cleanup in MobileCoordButton/TimeUI
- MobileCoordButton: call closeActiveTool() before opening, destroy
_pendingCloseTool if set, increment _closeSeq to cancel deferred
tools.innerHTML clear
- MobileTimeUIToggle: same _pendingCloseTool + _closeSeq fix after
closeActiveTool() to prevent 420ms deferred cleanup from wiping
#timeUI after it's placed in #tools
- Removed redundant closeActiveTool() from MobileCoordButton close path
(was being called after destroy, not needed)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: active mobile toolbar buttons 34x34px (square)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Drastically compress .knowledge/ — keep only unique agent content
Remove 33 wiki files that duplicate docs/pages/ content.
Remove 9 reference/ files derivable from source code.
Keep only 5 files (down from 46):
- AI-GETTING-STARTED.md (agent setup walkthrough)
- AI-DEVELOPMENT.md (spec-kit workflow)
- conventions-and-gotchas.md (naming, code style, common issues)
- 12-devin-knowledge-notes.md (CI, auth, DB init, security gotchas)
- README.md (index pointing to docs/pages/ for everything else)
Principle: don't duplicate docs/ — only keep what's uniquely agent-optimized.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Rename to knowledge-notes.md, remove Devin branding and fork-specific CI section
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: hide mmgis-map-logo on mobile
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Restore Database Safety Rules for AI Agents section in AGENTS.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: shift compass and map scale 6px to the right (both mobile and desktop)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add back Important Instructions, code pattern templates, and detailed project structure
- Important Instructions in AGENTS.md: MCP tools, hot-reload, Reference Mission
- .knowledge/code-patterns.md: full directory tree with key directory annotations,
plus copy-paste templates for Express routes, Sequelize models, Tool plugins,
and WebSocket handlers
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Update project structure trees to reflect current filesystem
Add missing directories: tests/, .knowledge/, .specify/, .github/, views/,
private/, spice/, build/, examples/, scripts/middleware.js.
Both abbreviated (AGENTS.md) and detailed (.knowledge/code-patterns.md) trees
now match the actual repo layout.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.30-20260501 [version bump]
* Add Layers_.js to project structure (key singleton L_)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix project structure: correct API layout, frontend modules, code templates
API/Backend/ uses feature-domain modules (Draw/, Users/, Config/, etc.)
with setup.js + routes/ + models/ per feature — not APIs/ or Databases/.
Frontend essence/ has Components/, Helpers/, LandingPage/, mmgisAPI/,
services/ — not Ancillary/. Basics/ includes all singletons (Globe_,
Formulae_, ToolController_, Viewer_, ComponentController_, Test_).
Code templates updated to match actual patterns (setup.js, module.exports).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor: remove test infrastructure (Test_ module, testModules, DrawTool.test)
- Delete src/essence/Basics/Test_/ directory
- Delete src/essence/Tools/Draw/DrawTool.test.js
- Remove Test_ import and Shift+T keydown handler from essence.js
- Remove tests key from Draw tool config.json
- Remove testModules generation logic from API/updateTools.js
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.31-20260501 [version bump]
* style: move Cesium link button to top-right and match Leaflet zoom button styling
- Change control container from top-left to top-right positioning
- Update button size from 26px to 30px to match Leaflet zoom controls
- Use CSS variables (--color-a, --color-f, --color-mmgis) instead of hardcoded colors
- Add border-radius and box-shadow matching Leaflet control appearance
- Update hover/inactive states to use themed colors
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: anchor map logo to viewport instead of Leaflet map panel
- Change MapLogo parent from .leaflet-bottom.leaflet-right to #main-container
- Switch CSS position from absolute to fixed for viewport anchoring
- Add explicit bottom-offset positioning in BottomElementPositioner (desktop)
- Add explicit bottom-offset positioning in BottomElementPositioner (mobile)
- Logo stays at viewport right edge regardless of open side panels
- Retains smooth bottom offset transitions when bottom bar appears
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs: remove references to deleted test infrastructure (Test_, DrawTool.test)
- Remove Test_/ from project structure in .knowledge/code-patterns.md
- Remove DrawTool.test.js references from specs/006 spec, plan, and tasks
- Remove Draw Tool Testing section from tasks.md
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.32-20260501 [version bump]
* fix: append logo to document.body to avoid filter containing block
#main-container has a CSS filter property which creates a new containing
block per the CSS spec, causing position:fixed to behave like absolute.
Appending to document.body ensures true viewport-fixed positioning.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: prevent mobile topBarTitleName text wrapping by replacing max-width with white-space: nowrap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 4.3.33-20260501 [version bump]
* chore: bump version to 5.0.0 and update changelog
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* refactor(ui): move Screenshot/Fullscreen to BottomBar, About to TopBar kebab
TopBar kebab menu now contains only Keyboard Shortcuts, Settings, and About
(About now shows on both desktop and mobile).
BottomBarReact now renders Screenshot, Fullscreen, and Copy Link buttons
(top to bottom) following the same IconButton + Tooltip pattern. The
About button has been removed from BottomBarReact.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(mobile): enforce exclusive panel toggling on mobile in TopBar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* style: reposition LithoSphere globe controls to match Leaflet/Cesium theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* feat(topbar): hide Viewer/Globe toggles based on configured panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): reorder buttons (Copy Link, Screenshot, Fullscreen) and unify size
Reorder the BottomBarReact buttons top-to-bottom to: Copy Link, Screenshot,
Fullscreen.
Move the 24x24 button sizing from the #topBarLink id selector in mmgis.css
into the .barButton class in BottomBarReact.module.css so all three buttons
share the same compact size as the original Copy Link button. Drop the now
redundant #topBarLink rule.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(bottombar): increase padding-bottom to 12px and button margin to 3px 0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: rearrange globe controls — compass top-right circular, nav row, vertical column, panels open left
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* chore: bump version to 5.0.2-20260505 [version bump]
* style: anchor observe settings panel right:34px and float nav hover panels
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(theming): add 5 new themes, --color-shadow variable, and configure ThemePreview
- Add Dark Terra, Dark Nebula, Dark Lunar, Dark Supernova, Light Botanical themes
- Add --color-shadow CSS variable to every theme + :root fallback
- Replace hardcoded rgba shadow colors with var(--color-shadow) in TopBar,
Toolbar, SeparatedTools, ToolPanel, FloatingElements, Dropdown, Modal,
and SplitScreens
- Add Custom shadowcolor color picker in tab-ui-config and apply it via Stylize
- Add ThemePreview component (configure/src) wired through Maker.js as
a new 'themepreview' row type so the configure UI shows a live mini
mockup of the selected theme
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.2-20260505 [version bump]
* fix(configure/ThemePreview): tighten top spacing and live-preview Custom theme
- Pull the preview up by 12px so the gap below the theme dropdown is tighter.
- Read the Custom color pickers (look.primarycolor / secondarycolor /
tertiarycolor / accentcolor / shadowcolor / topbarcolor / toolbarcolor /
mapcolor) from the configuration and overlay them on Dark Default so
the preview reflects Custom theme edits live, matching Stylize.js's
runtime behavior.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260505 [version bump]
* feat(themes): add Dark Heliosphere, Dark Monokai, and Light Solarized
- Dark Heliosphere: deep night purple surface with corona-orange accent.
- Dark Monokai: warm graphite surface with lime accent (Monokai-inspired).
- Light Solarized: classic solarized base3/base02 with blue accent.
Mirror added to configure/src/themes/themes.js for the ThemePreview, and
the three names appended to the Color Theme dropdown options.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(coordinates): respect time.initiallyOpen when live deep-link is set
* chore: bump version to 5.0.3-20260505 [version bump]
* refactor(theming): remove Custom theme + per-field color overrides
- Drop the 'Custom' option from the Color Theme dropdown.
- Remove all Custom Color Options (look.primarycolor, .secondarycolor,
.tertiarycolor, .accentcolor, .bodycolor, .topbarcolor, .toolbarcolor,
.mapcolor, .hightlightcolor, .shadowcolor) from tab-ui-config.json.
- Strip the matching DOM/CSS-variable override block from Stylize.js;
Stylize now just applies the selected preset theme (and the page logo).
- Drop the empty bodycolor/topbarcolor/toolbarcolor/mapcolor/shadowcolor
defaults from API/templates/config_template.js.
- Simplify ThemePreview to render the selected preset directly — no
Custom branch, no overlay logic.
Preset themes cover all the looks we want and keep the configure surface
much smaller.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(time-ui): round corners on TimeUI shell, action wrappers, mode dropdown
- #timeUI: 10px border-radius on the outer time control bar.
- #mmgisTimeUIActionsLeft / #mmgisTimeUIActionsRight: 10px border-radius
so the action clusters sit as rounded chips.
- #mmgisTimeUIActionsRight > div (excluding #mmgisTimeUIPresent): 10px
border-radius on each action button so they match the wrapper.
- #mmgisTimeUIModeDropdown: 40px height + 10px border-radius to align
with the rest of the bar; clear the dropy default border-color so the
rounded edge isn't outlined.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260505 [version bump]
* feat(configure): mark light themes as (experimental) in dropdown label
Light themes still have outstanding contrast issues, so flag them in the
Color Theme dropdown without changing the saved value.
- Maker dropdown now accepts options as either a plain string (current
behavior) or { value, label } so the rendered label can differ from
the persisted value.
- tab-ui-config switches the six light themes to { value, label } form
with '(experimental)' appended to the label only. Existing mission
configs that already saved 'Light Default' etc. continue to match.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Fix timeUI border radius
* fix(mobile): rescue #timeUI before tool make() destroys it
Clicking Layers -> Time -> Layers -> Time on mobile caused the bottom
panel to render LayersTool content with TimeUI height. The #timeUI DOM
element was destroyed when LayersTool.make() called $('#tools').empty(),
before the async React useEffect in MobileTimeUIToggle could rescue it
to its staging container.
- ToolController_.makeTool: synchronously move #timeUI from #tools back
to #timeUIMobileStaging (and reset TimeUI store flags) on mobile,
before invoking the new tool's make().
- MobileTimeUIToggle.handleClick: defensive fallback that re-initializes
TimeUI if #timeUI no longer exists when the toggle is activated.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): move re-initialized #timeUI from staging into #tools
TimeUI.init() on mobile appends the new #timeUI to the hidden
#timeUIMobileStaging container, so the fallback branch must also move
it into #tools — otherwise the user sees an empty tool panel after
the destroyed-element recovery path.
Caught by Devin Review.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): preserve #timeUI when Coordinates tool empties #tools
On mobile, opening or closing the Coordinates tool runs
$('#tools').empty() inside interfaceWithMMWebGIS / separateFromMMWebGIS.
After the previous PR commits, clicking Coordinates -> Time still left
the bottom panel empty because:
- Coordinates.make() empties #tools while #timeUI is in staging (fine
on its own), but the Coordinates teardown that fires after the user
switches to the Time toggle (via MobileCoordButton's useEffect on
activeToolName change) calls Coordinates.destroy() ->
separateFromMMWebGIS(), which empties #tools wholesale and destroys
the freshly-placed #timeUI.
Add a rescueMobileTimeUI() helper that moves #timeUI from #tools back
to #timeUIMobileStaging before each tools.empty() call in Coordinates,
mirroring the rescue already done in ToolController_.makeTool().
Coordinates -> Time now correctly shows the TimeUI.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mobile): harden TimeUI fallback recovery (call fina(), de-dupe popovers)
Devin Review correctly flagged that the safety-net path in
MobileTimeUIToggle.handleClick was producing a partially-broken TimeUI
when it fired:
- TimeUI.init() unconditionally appends a new #timeUIPlayPopover_global
to <body>, so a second init() left two elements with the same id.
- TimeUI.init() alone does not wire up date pickers or per-button click
handlers — that's TimeUI.fina()'s job. Without fina(), the recovered
TimeUI rendered visually but Play / Previous / Next / Fit / Follow /
Present / Expand were all dead.
Before re-initializing, remove the stale #timeUIPlayPopover_global and
#timeUIQuickSelectPopover_global divs to avoid duplicate ids. After the
new #timeUI is moved into #tools, call TimeUI.fina() to populate the
date pickers, attach the button click handlers, build the histogram,
and populate the expanded mobile rows.
Some delegated body/document handlers in attachEvents() will still be
duplicated on this path; that is acceptable for a degraded recovery
that should never run in practice now that the primary rescues in
ToolController_.makeTool() and Coordinates.js cover all known paths.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260505 [version bump]
* fix(mobile): Coordinates teardown only removes its own DOM
The previous Coordinates fix was racing with itself: after the Time
toggle synchronously moved #timeUI into #tools, MobileCoordButton's
useEffect (triggered by the activeToolName change) ran on the next
React tick and called L_.Coordinates.destroy(). That called
separateFromMMWebGIS(), whose rescue moved #timeUI right back into the
hidden staging div before tools.empty() — so the bottom panel ended up
empty even though the time toggle was 'active'.
Make separateFromMMWebGIS selective: only remove the
Coordinates-specific DOM (#coordUIHeader and #CoordinatesDiv) instead
of wiping all of #tools. Any other content already in #tools (e.g.
#timeUI placed there by the Time toggle) is left alone.
interfaceWithMMWebGIS still keeps the rescue + tools.empty() pattern
on the open path so Coordinates always starts from a clean panel.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Bump DrawTool Temporal Drawings upward
* chore: bump version to 5.0.6-20260505 [version bump]
* chore: reset version to 5.0.0
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test(e2e): fix 9 pre-existing failures (test-only changes)
- mmgis-api.spec.js: add form-fill login under AUTH=local; serialize
describe to avoid concurrent-login race in the session store
- coordinates.spec.js: TimeUI toggle was moved from the coordinates bar
to the Settings modal; navigate via topbar kebab menu and assert the
checkbox is rendered
- widgets.spec.js: target .leaflet-control-zoom-in/-out specifically;
the bare .leaflet-control-zoom class is also used by the home/reset
control, so the original assertion was always false
- sites.spec.js: scope panel selector to #toolPanel; both the toolbar
icon and the panel container share id="SitesTool"
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260505 [version bump]
* Revert "chore: bump version to 5.0.1-20260505 [version bump]"
This reverts commit 4880204c1163be5d1d7fa96d14a0ed018c6f586c.
* fix: prevent filter operator dropdown clipping in Layers panel
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.1-20260507 [version bump]
* revert: keep dropy openUp:true for operator dropdowns
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit d67c369ed437e47d658ae051348d377978dc48ed.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 29565ed829a55e9c241a789c9a3901d11cb5ca67.
* chore: bump version to 5.0.1-20260507 [version bump]
* Revert "chore: bump version to 5.0.1-20260507 [version bump]"
This reverts commit 50e357604ebe9378564619b34c508b63cfb62c1d.
* chore: bump version to 5.0.1-20260507 [version bump]
* chore: bump version to 5.0.2-20260511 [version bump]
* fix: render Globe panel immediately on first open without window resize
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.3-20260511 [version bump]
* feat: add theme borders to panels and gradient backgrounds to splitters
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: bump split shadow gradient opacity to 0.4
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal 3-col grid + smaller leaflet zoom button gap
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: prevent hotkey label/value wrapping (ellipsis instead)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: hotkeys modal single column, no wrap, no truncation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.4-20260511 [version bump]
* style: hotkeys modal dividers, invert title/subtitle colors, rename title, margin above subtitles
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style: move splitter gradient to themed CSS class, restore hover feedback
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* style: hotkeys section titles use --color-h (matches rest of app)
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.5-20260511 [version bump]
* fix: guard Globe_.init() inside rAF to prevent double instantiation
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.6-20260512 [version bump]
* feat(plugins): per-plugin deps, lazy tool loading, validation, shared discovery
Phase 3 — Plugin config validation + override warnings:
- New API/pluginValidation.js with validatePluginConfig() for tool, component,
and backend manifests. Validates required fields (name, paths), object/string
shape of paths, dependencies block (npm/python.pip/python.conda), and warns
on unknown top-level fields.
- updateTools()/updateComponents() now skip invalid plugins and emit override
warnings (matching what components already logged for tools).
Phase 2 — Shared discoverPlugins() utility:
- New API/pluginDiscovery.js consolidates the duplicated scanning logic from
updateTools(), updateComponents(), and getBackendSetups(). Supports exact-
name and substring container patterns, JSON/require/no-op loaders, and skips
dot/underscore-prefixed dirs.
- updateTools.js and setups.js refactored on top of the shared helper.
Phase 1 — Per-plugin dependency declaration + build-time aggregation:
- Plugin config.json may now declare a 'dependencies' block (npm + python.pip +
python.conda). validatePluginConfig() also validates this shape.
- New scripts/resolve-plugin-deps.js scans every tool/component/backend plugin
and writes plugin-package.json, plugin-python-requirements.txt, and
plugin-conda-deps.txt. Detects version conflicts and fails loudly.
- scripts/build.js calls resolvePluginDeps() before updateTools().
- Dockerfile installs the aggregated plugin npm and pip deps after the root
npm ci, using --no-save / --no-package-lock / --ignore-scripts so the root
lockfile is untouched.
- Animation tool migrated: ffmpeg/gifshot/html2canvas now declared in its
config.json (kept in root package.json for transitional compat).
- Generated artifacts gitignored.
Phase 4 — Lazy loading of tool bundles:
- updateTools() now emits dynamic-import arrow functions in the generated
src/pre/tools.js with webpackChunkName hints so each tool is split into
its own chunk (Kinds stays static because it's required synchronously).
- ToolController_ gains ensureToolLoaded(name) and getLoadedTool(name) helpers
and makeTool is async; init/finalizeTools and the separated-tool auto-open
flow are updated to handle lazy modules.
- Toolbar.jsx, SeparatedTools.jsx, SitesTool.js, and Layers_.js migrated to
resolve LayersTool/etc. via the new helpers instead of poking toolModules
directly.
Tests & docs:
- tests/fixtures/test-plugin-tools/{TestPlugin,InvalidPlugin,OverridePlugin}
+ tests/helpers/plugin-helpers.js with install/uninstall helpers.
- New unit specs: pluginValidation, pluginDiscovery, updateTools,
resolvePluginDeps, toolLazyLoading (57 tests, all passing).
- CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated with
schema, override behaviour, dependency declaration, build-time aggregation,
conflict detection, and Docker integration.
* chore: bump version to 5.0.7-20260512 [version bump]
* fix: make Globe_.init() idempotent against multi-init
Globe_.init() previously constructed a fresh GlobeRenderer on every call,
which after #71 could happen multiple times for a single toggle (uiStore
setTimeout + TopBar rAF). Each extra construction appends another
.cesium-widget / _lithosphere_scene to #globe and leaves event handlers
wired to dereferenced renderer state, which has been observed to break
LithoSphere globe control buttons on configurations where the globe panel
starts closed at boot.
Add a top-of-init() guard that bails out and calls invalidateSize() when
a renderer already exists. Single small, surgical change; no behavior
change for the !L_.hasGlobe mock-swap path or for first-time construction.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.7-20260512 [version bump]
* test(plugins): generate src/pre/tools.js on demand in toolLazyLoading spec
The Playwright unit-tests CI step runs before `npm run build` so the
gitignored `src/pre/tools.js` artifact does not yet exist on disk.
Add a beforeAll hook that invokes `updateTools()` to regenerate it
when missing, keeping the spec self-contained on both CI and dev
machines that already built locally.
* fix(tools): defensive getTool() + preload flag for cross-referenced tools
Devin Review flagged a behavioural regression introduced by Phase 4:
`ToolController_.getTool(name)` previously always returned a method-
callable object (real module or `{ use(){} }` stub) because every tool
was statically imported. After Phase 4, unresolved lazy loaders are
`() => import(...)` functions, so callers like `Map_.getTool('InfoTool').use(...)`,
`mmgisAPI.getTool('DrawTool').filesOn`, and `LegendTool` calling
`LayersTool.populateCogScale` would crash with TypeError until the
target tool was opened.
Two fixes:
1. **Defensive getTool()**: Returns the legacy fallback stub when the
tool module is still a lazy-loader function, and fires off
`ensureToolLoaded(name)` in the background so subsequent calls see
the resolved module. Prevents all crashes immediately.
2. **`preload: true` config flag**: Tools reached synchronously from
other code paths (Info, Draw, Layers, Chemistry) now declare
`"preload": true` in their `config.json`. `ToolController_.init()`
calls `preloadEagerTools()` which fires `ensureToolLoaded` for
every such tool right after toolbar setup — the chunks download
in parallel with the rest of the page becoming interactive, so by
the time a user clicks a feature the InfoTool module is already
resolved.
`validatePluginConfig` now accepts `preload` as a known tool field;
CONTRIBUTING.md and docs/pages/Contributing/Contributing.md updated to
document when to set it. Added a unit test covering the defensive
getTool behaviour and the `preload` propagation through
`toolConfigs`.
* chore: bump version to 5.0.8-20260512 [version bump]
* revert(plugins): remove Phase 4 lazy tool loading and preload mechanism
Phase 4 lazy emission caused cross-tool consumers (Map_ feature-click,
mmgisAPI, LegendTool) to receive raw '() => import(...)' arrows from
ToolController_.getTool(), breaking InfoTool open. Reverting to the
pre-Phase-4 behavior of static tool imports.
- API/updateTools.js: generated src/pre/tools.js now emits
'import FooTool from ...' for every tool (Kinds stays static too).
- ToolController_.js: getTool/makeTool back to sync; ensureToolLoaded,
getLoadedTool, preloadEagerTools deleted; separated-tool auto-open
flow simplified to direct sync calls.
- Toolbar.jsx, SeparatedTools.jsx, Layers_.js: revert async/lazy
patterns to sync ToolController_.toolModules[name] access.
- API/pluginValidation.js: drop 'preload' from KNOWN_FIELDS.
- src/essence/Tools/{Info,Draw,Layers,Chemistry}/config.json: drop
'preload: true'.
- CONTRIBUTING.md + docs: remove preload documentation.
- tests/unit/toolLazyLoading.spec.js: rewrite to verify static
imports instead of lazy loaders.
Also: log standard backends at startup (parity with plugin backends
and with tools/components), so all backends now produce
'info Loaded backend: <name> from <container>' at boot.
Phases 1-3 (per-plugin dependency aggregation, shared discoverPlugins,
config validation + override warnings) are unaffected.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(logger): new 'loaded' level (purple bg) for tool/component/backend startup
Previously the 'Loaded tool/component/backend: X from Y' lines used
the generic blue 'info' tag. They now use a dedicated 'loaded' level
rendered with a purple (#a855f7) background, so plugin discovery
output is visually distinct from other info messages.
- API/logger.js: add 'loaded' case to the dev-mode switch (white text
on purple bg) and suppress the redundant 'Caller:' echo for it
(matches how 'info' and 'success' are handled).
- API/updateTools.js: registerPlugin now logs at level 'loaded'.
Drops the redundant 'Loaded ' prefix since the level tag now reads
'loaded'.
- API/setups.js: standard and plugin backend startup logs use the
new level, same drop of the 'Loaded ' prefix.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(logs): 'Plugging in Tools/Components/Backends...' headings
Rename the cyan banner messages in scripts/build.js and scripts/server.js
from 'Updating Tools...' / 'Updating Components...' to 'Plugging in
Tools...' / 'Plugging in Components...' so the headings match the
plugin terminology used everywhere else (plugin-package.json,
discoverPlugins, etc.).
Also add a matching 'Plugging in Backends...' banner before
setups.getBackendSetups() in scripts/server.js so backends get an
equivalent title block to tools and components.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* style(logs): cyan banner lines lead with a blank line instead of trailing one
Move the \n from the end to the beginning of every cyan banner in
scripts/build.js and scripts/server.js (Resolving Plugin Dependencies,
Plugging in Tools/Components/Backends, Validating Environment
Variables, Starting websocket, Starting the development server) so
that the blank line visually separates each section above its title
rather than below it.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(plugins): postinstall hook auto-installs plugin npm deps
Plain `npm install` (or `npm ci`) on a fresh clone now resolves and
installs every plugin's declared npm dependencies automatically, so
new developers don't need to remember a second command.
- scripts/install-plugin-deps.js (new): reads plugin-package.json,
filters out deps already declared in root package.json with the
same version specifier (no-op for the Animation transitional case),
installs the remainder with `npm install --no-save --no-package-lock
--ignore-scripts <pkg@ver> ...`. `--no-package-lock` keeps the
root lockfile clean; `--ignore-scripts` prevents the inner install
from re-entering postinstall and matches the Dockerfile.
- package.json: postinstall guards against the Dockerfile's
package.json-only layer (`scripts/` not copied yet) by checking
for the two script files via `node -e` before invoking them.
Adds a `plugins:install` npm script for on-demand runs.
- CONTRIBUTING.md + docs/pages/Contributing/Contributing.md: replace
the manual-install paragraph with a note about the postinstall
hook and the filtering behavior.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs(plugins): manual Python install step for plugin pip/conda deps
Document that the npm postinstall hook only handles plugin npm deps —
plugin pip/conda deps must be installed manually after creating the
Python environment, since there's no portable way to detect which
interpreter or environment to target from a Node script.
- CONTRIBUTING.md: added a 'For local development ... Python' block
with the explicit `node scripts/resolve-plugin-deps.js` +
`micromamba run -n mmgis pip install -r plugin-python-requirements.txt`
+ optional conda install commands.
- docs/pages/Contributing/Contributing.md: matching short blurb in
the user-facing docs.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs(install): plugin Python install step in Installation.md
Add a numbered step in docs/pages/Setup/Installation/Installation.md's
Setup sequence (right after `micromamba activate mmgis`) with the
`pip install -r plugin-python-requirements.txt` and optional
`micromamba install --file plugin-conda-deps.txt` commands, so
non-Docker installs have the step in their main flow rather than
buried in CONTRIBUTING.md.
Also adds a pointer from the Python Environment section earlier in
the same file (after the env-create + activate steps) back to the
numbered Setup step.
CONTRIBUTING.md and docs/pages/Contributing/Contributing.md are
slimmed: instead of duplicating the install commands, both now link
to the Installation page (this matches the user request — the install
commands live in installation docs, not contribution docs).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: serialize concurrent layer reloads and stop mutating layer.url
Concurrent mmgisAPI.reloadLayer() calls for the same layer were
silently dropped by the _layersBeingMade guard, and reloadLayer
mutated layer.url in-place during async work — causing a race where
a second reload would capture the resolved URL as its 'original' and
permanently corrupt the {starttime}/{endtime} template placeholders.
Changes:
- TimeControl.reloadLayer: compute resolvedUrl into a local variable
instead of mutating layer.url. Pass resolvedUrl through to
Map_.refreshLayer.
- Map_.refreshLayer: accept resolvedUrl; temporarily swap layer.url
inside a try/finally for the makeLayer call so the fetched URL is
the resolved one and the placeholder template is always restored.
- Map_.refreshLayer: when a reload is already in flight for the
same layer, queue the request (coalesced by name) instead of
dropping it with a 'Cannot make layer' warning.
- Map_.makeLayer: after releasing the lock, drain any queued reload
for this layer via setTimeout 0.
- TimeControl.reloadTimeLayers: become async and await Promise.all
of every per-layer reload; remove the setTimeout(500) workaround
around active-feature restoration and follow-pan logic.
- mmgisAPI.reloadLayers: new batch API that reloads multiple
time-enabled layers concurrently and returns a Promise<boolean[]>.
- Tests: new tests/e2e/map/concurrent-layer-reload.spec.js covers
the seven scenarios from the plan (single reload, URL preservation,
multi-layer concurrent reload, rapid same-layer reload, reloadLayers
API surface). tests/e2e/api/mmgis-api.spec.js gains a surface
check that reloadLayer/reloadLayers are exposed.
- Docs: Main.md documents the new reloadLayers entry.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.8-20260513 [version bump]
* fix(TimeControl): use Promise.allSettled in reloadTimeLayers
Promise.all short-circuits on first rejection, which would skip the
active-feature restoration and follow-pan logic if any single layer
reload threw (network error, malformed config, etc.). The old
setTimeout(500) approach ran those steps unconditionally; switching
to Promise.allSettled preserves that robustness.
Addresses Devin Review feedback on PR #78.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(mmgisAPI): use Promise.allSettled in reloadLayers batch API
Mirrors the reloadTimeLayers fix: a single failing TimeControl.reloadLayer
call (e.g. unknown layer name throws inside asLayerUUID, network error,
malformed config) no longer rejects the whole batch. The returned array
preserves order and reports failed entries as false instead, matching
the documented Promise<boolean[]> contract.
Addresses second Devin Review finding on PR #78.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* docs: note that reloadTimeLayers is now async (Promise<string[]>)
The implementation changed from sync to async in commit a096cfe9 (the
function now uses await + Promise.allSettled internally to coordinate
per-layer reloads), but the public API JSDoc and Main.md still
documented the old synchronous return type. External consumers using
the old synchronous return value would get a Promise instead of an
array.
Updates JSDoc on mmgisAPI.reloadTimeLayers to declare
Promise<string[]>, and rewrites the Main.md example to use 'await'.
Also fixes the previous example, which had a syntactically-malformed
trailing tuple-style index.
Addresses third Devin Review finding on PR #78.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: split time-related e2e specs into tests/e2e/time/
Moves the two time-feature specs out of tests/e2e/map/ so the map suite
stays focused on map-UI behavior and the time/time-enabled-layer suite
can be run (and reasoned about) on its own:
- tests/e2e/map/time-control.spec.js
-> tests/e2e/time/time-control.spec.js
- tests/e2e/map/concurrent-layer-reload.spec.js
-> tests/e2e/time/concurrent-layer-reload.spec.js
Relative imports (../../helpers, ../../pages, ../../fixtures) are
unchanged because the new directory is the same depth. Playwright
picks the files up automatically via testDir './tests' +
testMatch '**/*.spec.js'.
Adds a matching 'npm run test:e2e:time' script and documents the new
suite in tests/README.md alongside the existing per-suite scripts.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: cap local Playwright workers at 4
The previous `workers: process.env.CI ? 1 : undefined` resolved to
Playwright's default (~half the CPU cores). On higher-core machines
(e.g. 16 cores -> 8 workers) the dev server gets overloaded and the
suite actually runs slower. CI behavior is unchanged (1 worker).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: thread resolvedUrl through makeLayer/captureVector instead of mutating layer.url
The previous fix in Map_.refreshLayer temporarily swapped
`layerObj.url = resolvedUrl` during the `await makeLayer` call and
restored it in a finally block. Since `layerObj` is the shared
`L_.layers.data[name]` object, any concurrent code reading
`layer.url` during that async window could observe the resolved URL
instead of the template. Most importantly, a second
`TimeControl.reloadLayer()` call would then capture the resolved
URL as its 'template' and corrupt the placeholders for every
subsequent reload.
Surfaced by tests/e2e/time/concurrent-layer-reload.spec.js Test 5,
which after Promise.all of two reloads observed
`layer.url ==='geodatasets:...?from=...&to=...'` instead of the
expected `{starttime}/{endtime}` template.
Fix: thread `resolvedUrl` as an explicit parameter through
`Map_.refreshLayer` -> `makeLayer` -> `makeVectorLayer` ->
`captureVector` (via options.resolvedUrl). `captureVector` uses
`options.resolvedUrl` when provided and skips the
`{starttime}`/`{endtime}`/`{customtime.*}` regex replacement
(which TimeControl.reloadLayer already performed). `layer.url` is
NEVER mutated for the duration of the async operation, so the
template is preserved across overlapping reloads.
This also fully resolves the Devin Review #4 finding which flagged
the temporary URL swap as reintroducing the same race the PR was
meant to fix.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: also replace {time} placeholder when computing resolvedUrl
In the previous fix, captureVector skips its time-replacement block when
the caller supplies options.resolvedUrl (because TimeControl.reloadLayer
already performed those replacements). However TimeControl.reloadLayer
was only replacing {starttime}/{endtime}/{customtime.*} on resolvedUrl,
not {time} — which captureVector previously handled at
LayerCapturer.js:97 by mapping {time} -> endTime. This caused vector
layers using the documented {time} placeholder (see
docs/pages/Configure/Layers/Tile/Tile.md:90 and
docs/pages/APIs/JavaScript/Main/Main.md:482) to fetch URLs containing
the literal text '{time}' on time-triggered reloads.
Mirror the existing captureVector behavior: replace {time} with the
formatted end-time value alongside {starttime}/{endtime}, before the
resolved URL is threaded through Map_.refreshLayer -> makeLayer ->
captureVector.
Addresses Devin Review finding on commit fcba8101.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: always run time-placeholder replacement in captureVector (idempotent)
The previous fix gated captureVector's time-placeholder replacement
block on `!hasResolvedUrl`, on the assumption that any caller passing
options.resolvedUrl had already done the replacement.
That assumption only holds for time.type === 'global' / 'requery' /
forceRequery. For other time types that still flow through
Map_.refreshLayer into captureVector (most importantly
time.type === 'local' with endProp == null per TimeControl.js:276-287),
TimeControl.reloadLayer's resolved-URL replacement block at lines 249-273
is skipped, so the resolvedUrl arrives at captureVector still containing
literal {starttime}/{endtime}/{time} placeholders. The fetch then goes
out with unreplaced placeholders.
Fix: drop the !hasResolvedUrl guard and always run the replacement,
reading the source from `layerUrl` (which is already either
options.resolvedUrl or layerObj.url per the choice above). The
.replace(/{starttime}/g, ...) chain is idempotent on URLs that have
already been resolved — the regexes simply don't match — so the
correct path is restored without re-introducing the mutate-in-place
bug fcba8101 fixed.
Addresses Devin Review finding on commit ddb90dbb.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: remove GIBS MODIS time tile test (always skips on external dependency)
The test at tests/e2e/time/time-control.spec.js depended on
gibs.earthdata.nasa.gov being reachable from the test environment and
served only as a placeholder — it skips every run since the test
infrastructure does not have external network access by policy. It
provides no signal in CI or locally, so removing it reduces noise in
the suite output without losing coverage.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: add coverage for {time}, local-endProp-null, stress, and feature-presence cases
Four new e2e tests in tests/e2e/time/concurrent-layer-reload.spec.js,
each targeting a gap the original suite missed:
Test 8 — {time} placeholder preservation. Mirrors Test 2/5 but uses
`{time}` instead of `{starttime}/{endtime}`. Catches the Devin
Review #5 regression where captureVector's gating on !hasResolvedUrl
silently dropped the {time} -> endTime replacement (the literal
'{time}' would have ended up in the fetch URL).
Test 9 — local + endProp==null path. Sets layer.time.type='local'
and layer.time.endProp=null to force the TimeControl.reloadLayer
branch (TimeControl.js:276-287) that bypasses the resolved-URL
placeholder block and falls through to the else clause. Inspects
outgoing /geodatasets/* requests via page.on('request', ...) and
asserts no literal {starttime}/{endtime}/{time} remain. Catches the
Devin Review #6 regression: when this branch hit captureVector with
hasResolvedUrl=true, the !hasResolvedUrl gate previously short-
circuited the only remaining replacement site.
Test 10 — 20-reload stress burst. Extends Test 4's two-reload check
to 20 concurrent reloadLayer() calls, capturing 'Cannot make layer'
warnings to verify the queue coalesces requests instead of silently
dropping them. Also re-asserts layer.url template integrity post-
burst.
Test 11 — Feature-presence after concurrent reload. Captures
L_.layers.layer[key].getLayers().length before and after a 5-reload
burst. Asserts the count is still > 0 afterwards — the user-visible
'gaps where dynamically-appearing data doesn't show up' symptom from
the original bug report.
All four tests skip gracefully when their fixture layer is absent or
the dataset returns no rows, to avoid spurious failures across
mission configurations.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* test: add 5 edge-case tests probing the blast radius of the URL fix
Five additional tests in tests/e2e/time/concurrent-layer-reload.spec.js
covering paths adjacent to the URL-mutation fix that were not exercised
by tests 1-11. Each was chosen because the production code on that path
changed (or now has a different contract) and a regression would not be
caught by the original race-condition tests.
Test 12 — {customtime.N} placeholder preservation. TimeControl.reloadLayer's
customtime replacement loop was migrated from `layer.url = ...` to
`resolvedUrl = ...`. Seeds TimeControl.customTimes.times so the loop
actually runs, then asserts the {customtime.0} placeholder remains
literally on layer.url after reload.
Test 13 — mmgisAPI.reloadTimeLayers() returns a Promise. This is the
backward-incompatible behavior change documented in
docs/pages/APIs/JavaScript/Main/Main.md (previously synchronous).
Asserts the returned value is a thenable that resolves to an array,
pinning the new contract so it does not silently regress.
Test 14 — mmgisAPI.reloadLayers handles unknown layer names. The
Promise.allSettled change requires that a failing per-layer reload
surfaces as `false` at the same array position as its input name,
without throwing. Mixes a valid name + an unknown name + another
valid name to verify the order and the boolean mapping.
Test 15 — Reloading a time-DISABLED vector layer leaves layer.url
unchanged. Discovers a candidate layer from L_.layers.data at runtime
(skips if none exist), reloads it, and asserts the URL is byte-equal
afterwards. Catches any accidental URL mutation introduced for the
non-time code path.
Test 16 — mmgisAPI.reloadLayers handles empty array, null, undefined,
and string inputs without throwing — verifying the Array.isArray guard
at mmgisAPI.js:618. Returns `[]` in all cases.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(makeLayer): wrap dispatch in try/finally so lock + queue drain always run
Address Devin Review finding: a thrown exception from any layer-type
builder (makeVectorLayer, makeVelocityLayer, etc.) inside makeLayer's
switch statement previously left lockRegistry[layerName] set to true
and skipped the queue drain entirely, since the release statement and
queue-drain block both lived AFTER the awaited dispatch.
Effect of the bug: any subsequent refreshLayer call for that layer
would queue against a permanently-locked entry that never drains.
The new queue mechanism inherits this pre-existing issue and makes
the failure mode worse — silent accumulation in _layerReloadQueue
instead of a visible 'Cannot make layer' warning.
Additional concern: the outer 'new Promise(async (resolve, reject) =>
{...})' is the async-executor anti-pattern. A throw inside the async
executor escapes to the unhandled-rejection handler instead of
rejecting the outer Promise — so the caller's 'await makeLayer(...)'
would hang indefinitely, compounding the lock-leak symptom.
Fix:
- Wrap the type-dispatch switch + Filtering.updateGeoJSON/
triggerFilter calls in a try/catch/finally.
- catch logs the error and tracks success via 'madeSuccessfully'.
- finally runs unconditionally: lockRegistry[layerName] = false,
drain L_._layerReloadQueue[layerObj.name] if present, then
resolve(madeSuccessfully). This ensures the lock release and
queue drain happen regardless of whether the inner builder
threw or completed normally.
Test (concurrent-layer-reload.spec.js):
Added probe-style test '_layersBeingMade lock is released after
single and concurrent reloads' that asserts the lock invariant:
1. After mmgisAPI.reloadLayer() resolves +
a 100ms drain window, _layersBeingMade[key] is false.
2. After 5 concurrent mmgisAPI.reloadLayer() calls resolve +
a 1000ms drain window, _layersBeingMade[key] is false.
3. _layerReloadQueue is empty afterwards (otherwise a future
reload would mistakenly trigger an immediate drain instead
of doing its own work).
This is a positive-invariant test — it catches accidental lock
retention even without force-triggering exceptions, which would
require monkey-patching webpack-internal module references that
aren't exposed on window.
Per user direction, NOT addressing Devin Review's separate finding
about the reloadTimeLayers sync->async breaking change at this
time (no version bump requested).
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.9-20260513 [version bump]
* fix(OperationsClock): bump z-index above bottomFloatingBar so clock stays visible
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(plugin-deps): respect override semantics when aggregating dependencies
When a plugin tool/component/backend overrides a standard one by reusing
the same directory name, only the override's deps should contribute to
the aggregated plugin manifests. Previously, gatherDependencies()
concatenated standard + plugin entries and fed both to mergeNpm/
mergePython, which could spuriously flag the same package as
conflicting between the standard and override versions.
Extract winnersByName() (mirroring API/updateTools.js +
API/setups.js override behavior) and use it for all three plugin
kinds. Add unit tests covering the override case and the
spurious-conflict regression.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(Dockerfile): re-install plugin npm deps in runtime stage
Plugin deps installed in the builder via `npm install --no-save
--no-package-lock` aren't recorded in package.json/package-lock.json,
so the runtime stage's `npm ci --only=production` would lose them.
Frontend deps are bundled by webpack into ./build so they're fine, but
backend plugins that `require()` their declared npm dependencies at
runtime would crash with 'Cannot find module'.
Copy plugin-package.json from the builder and re-run the same
conditional install in the runtime stage so backend plugin deps land
in the runtime image's node_modules. `--ignore-scripts` prevents the
inner install from re-entering the root postinstall hook.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(OperationsClock): lift to 58px bottom when TimeUI is open
Add #operationsClock to BottomElementPositioner's reactive positioning
so it shifts to bottom:58px when timeUIActive is true (TimeUI dock
visible) and back to bottom:40px when closed. Avoids overlap with
the bottom floating bar without adding new state to OperationsClock
itself. Mobile path is unchanged — OperationsClock.setupMobilePositioning
manages mobile positioning separately.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(OperationsClock): always sit at bottom:58px
Revert the dynamic positioning in BottomElementPositioner and just
hardcode bottom:58px in OperationsClock.css. Simpler, no cross-cutting
state dependency.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat: add Lunar South Pole reference mission variant (IAU2000:30120)
- Add REFERENCE_MISSION_VARIANTS registry and resolveVariantBlueprintPath
helper to missionTemplates.js for dynamic variant resolution
- Update configs.js to accept referenceMissionVariant parameter and
validate against the registry
- Add variant dropdown to NewMissionModal UI when Reference Mission
checkbox is enabled
- Create blueprint directory with south polar stereographic config
(IAU2000:30120, +proj=stere +lat_0=-90, bounds ±1095700/1095600)
- Add unit tests for variant registry, blueprint path resolution, and
Lunar-SouthPole projection assertions (15 tests)
- Add E2E smoke tests for Lunar-SouthPole mission variant
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* feat(api): pass through reloadLayer flags in reloadLayers
Forward evenIfOff, evenIfControlled, forceRequery, and
skipOrderedBringToFront parameters to TimeControl.reloadLayer()
for each layer in the batch. Fully backward-compatible — existing
callers that pass only layerNames get undefined for all flags.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.10-20260518 [version bump]
* chore: bump version to 5.0.10-20260518 [version bump]
* docs(api): add forceRequery & skipOrderedBringToFront to reloadLayer/reloadLayers docs
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Add SPole basemap to lunar ref mission
* chore: bump version to 5.0.11-20260518 [version bump]
* chore: remove unused blueprints/Missions/Test directory
The Test blueprint is no longer needed — the Reference-Mission and
Reference-Mission-Lunar-SouthPole blueprints serve as the basis for
demo, development, and testing.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: use setupReferenceMission string value as variant key fallback
Address Devin Review feedback: when setupReferenceMission is a non-empty
string (e.g. 'Lunar-SouthPole'), use it as the variant key if
referenceMissionVariant is not provided. Also guard against empty string
triggering reference mission creation.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(measure): rename config DEM field from 'dem' to 'url' and handle HTTP URLs
- Rename 'field': 'dem' → 'url' and 'name': 'DEM Path' → 'DEM URL' in
src/essence/Tools/Measure/config.json (layer-specific DEM objectarray)
- Rename same in configure/src/metaconfigs/layer-tile-config.json
- Add http:// and https:// prefix handling in makeProfile() so external
URLs are not mangled with the mission path prefix
The top-level variables.dem field (primary DEM) is unchanged.
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.11-20260519 [version bump]
* revert: remove http/https URL prefix handling in makeProfile()
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(measure): rename 'dem' to 'url' in Reference Mission layerDems blueprint
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(measure): accept both 'url' and 'dem' fields in layerDems for backward compatibility
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* chore: bump version to 5.0.12-20260519 [version bump]
* feat: add SPole_100m basemap layer to Lunar South Pole blueprint config
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix: show Save to Base Blueprint button for all reference mission variants
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* Update lunar ref mission
* style: add text-align right to mission list buttons in configure sidebar
Co-Authored-By: tariq.k.soliman <tariqksoliman@gmail.com>
* fix(ui): SegmentTool background, clipboard API, context menu test selectors
-…1 parent f93720c commit 4cb7693
3 files changed
Lines changed: 6 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
285 | 285 | | |
286 | 286 | | |
287 | 287 | | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
288 | 292 | | |
289 | 293 | | |
290 | 294 | | |
| |||
0 commit comments