Skip to content

Commit 4cb7693

Browse files
tariqksolimandevin-ai-integration[bot]github-actions[bot]
authored
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

File tree

configure/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "configure",
3-
"version": "5.0.13-20260521",
3+
"version": "5.0.14-20260526",
44
"homepage": "./configure/build",
55
"private": true,
66
"dependencies": {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mmgis",
3-
"version": "5.0.13-20260521",
3+
"version": "5.0.14-20260526",
44
"description": "A web-based mapping and localization solution for science operation on planetary missions.",
55
"homepage": "build",
66
"repository": {

src/essence/Basics/Map_/Map_.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ let Map_ = {
285285
//Remove attribution
286286
$('.leaflet-control-attribution').remove()
287287

288+
// Expose Map_ on L_ early so that AJAX callbacks from makeLayers
289+
// can safely access L_.Map_.map (e.g. getZoom()) before L_.fina() runs.
290+
L_.Map_ = this
291+
288292
//Make our layers
289293
makeLayers(L_.layers.dataFlat)
290294

0 commit comments

Comments
 (0)