diff --git a/.github/workflows/deploy-satisfactory.yml b/.github/workflows/deploy-satisfactory.yml index 38eca57..2078268 100644 --- a/.github/workflows/deploy-satisfactory.yml +++ b/.github/workflows/deploy-satisfactory.yml @@ -2,6 +2,23 @@ name: Deploy Satisfactory Planner on: workflow_dispatch: + pull_request: + branches: + - main + paths: + - ".github/workflows/deploy-satisfactory.yml" + - "angular.json" + - "package.json" + - "package-lock.json" + - "playwright.config.ts" + - "tsconfig.base.json" + - "vitest.config.mjs" + - "apps/web/**" + - "e2e/**" + - "packages/**" + - "scripts/extract-satisfactory-data/**" + - "data/generated/**" + - "data/resource-limits/**" push: branches: - main @@ -10,18 +27,20 @@ on: - "angular.json" - "package.json" - "package-lock.json" + - "playwright.config.ts" - "tsconfig.base.json" - "vitest.config.mjs" - "apps/web/**" + - "e2e/**" - "packages/**" - "scripts/extract-satisfactory-data/**" - "data/generated/**" - "data/resource-limits/**" jobs: - build_and_deploy: + validate: runs-on: ubuntu-latest - name: Build and deploy + name: Validate permissions: contents: read steps: @@ -40,9 +59,47 @@ jobs: - name: Run tests run: npm test + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Run browser smoke tests + run: npm run test:smoke + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: playwright-report/ + if-no-files-found: ignore + retention-days: 7 + - name: Build app run: npm run build + - name: Upload app build + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: satisfactory-web-build + path: dist/apps/web/browser + if-no-files-found: error + retention-days: 1 + + deploy: + runs-on: ubuntu-latest + name: Deploy + needs: validate + if: github.event_name != 'pull_request' + permissions: + contents: read + steps: + - name: Download app build + uses: actions/download-artifact@v4 + with: + name: satisfactory-web-build + path: dist/apps/web/browser + - name: Deploy to Azure Static Web Apps uses: Azure/static-web-apps-deploy@v1 with: diff --git a/docs/architecture.md b/docs/architecture.md index ceaff77..9d5286e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -106,7 +106,12 @@ Feature code should depend on the capability that owns the use case instead of r Do not reintroduce broad convenience command forwards on `PlannerStoreService`. New tests should cover the capability interface used by the consumer; runtime-store tests should stay focused on composition, persistence/solver wiring, lifecycle hooks, and workspace initialization. -Future optional capabilities should consider plugin-shaped extension seams before they become built-in planner behavior. See [Plugin-Shaped Extension Seams](./rfc/plugin-extension-seams.md) for the current guidance. Runtime third-party plugins are not an accepted architecture decision. +Future session-scale features, such as linked plan contracts, should add focused +session/link capabilities instead of expanding `PlannerStoreService` back into a +feature facade. Future optional capabilities should consider plugin-shaped +extension seams before they become built-in planner behavior. See +[Plugin-Shaped Extension Seams](./rfc/plugin-extension-seams.md) for the current +guidance. Runtime third-party plugins are not an accepted architecture decision. The generated data pipeline is build-time only. Raw `en-US.json` is read by `scripts/extract-satisfactory-data` and normalized into `apps/web/public/data/satisfactory-current.json`, which is what the Angular app serves. diff --git a/docs/data-model.md b/docs/data-model.md index 1b25950..ce87069 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -58,14 +58,23 @@ Sessions currently group plans only. Session defaults, save metadata, linked plans, logistics routes, map pins/locations, session-wide production balance, and session import/export are future extensions. +The planned linked-plan model should store links on the session rather than on +individual plan exports. A destination plan should keep its normal external +input requirement by item and rate; session links can cover all or part of that +requirement by reserving output from another plan. This keeps standalone plan +JSON/share payloads portable while letting a session explain where an external +input is expected to come from. Link health, source shortages, destination +overcoverage, and session-wide balance remain derived state. + Individual plans can be exported as readable Beltwise JSON files with `kind: beltwise.plan` and `formatVersion: 1`. A plan export contains one persisted project payload plus dataset metadata for mismatch warnings, including product targets, power targets, sink rules, recipe/machine/resource settings, external inputs, objective settings, graph display/layout, notes, and build state. It does not include global user defaults, whole-session state, save-game data, -share links, or solver output. Imported plans are added as separate local -projects in the active session and are solved again with the current app dataset. +share links, linked-plan contracts, or solver output. Imported plans are added +as separate local projects in the active session and are solved again with the +current app dataset. Plan sharing uses a separate compact `bw.p` payload. It stores deltas against Beltwise's schema-defined defaults for the current dataset, then compresses that diff --git a/docs/development.md b/docs/development.md index fed446d..0c7bf91 100644 --- a/docs/development.md +++ b/docs/development.md @@ -30,6 +30,12 @@ Run focused web app tests: npm.cmd run test:web ``` +Run browser smoke tests: + +```powershell +npm.cmd run test:smoke +``` + Run workspace typechecks: ```powershell @@ -75,6 +81,7 @@ docs architecture, ADRs, RFCs, and product notes - Keep `planner-core` graph and project models renderer-neutral. - Persist user configuration, not solver output. - Add focused tests when changing parser, solver, resource-limit, graph-model, transfer, persistence, or planner capability behavior. +- Use `npm.cmd run test:smoke` for shallow browser coverage of the assembled Angular planner, graph shell, simple solving path, and local persistence. - Test Angular planner behavior through the owning capability (`PlannerWorkspaceSlice`, `PlannerPlanConfigStore`, `PlannerDefaultsStore`, `PlannerGraphStore`, transfer, solving, or workbench) instead of rebuilding broad root-store coverage. - Run `npm.cmd test`, `npm.cmd run typecheck`, and `npm.cmd run build` before handing off meaningful changes. - If a sandboxed run of `npm.cmd test` fails while loading `vitest.config.mjs` with `Access is denied`, rerun the same command with the appropriate permissions before changing test configuration. diff --git a/docs/product-spec.md b/docs/product-spec.md index 9c760ba..2c4b848 100644 --- a/docs/product-spec.md +++ b/docs/product-spec.md @@ -1093,12 +1093,14 @@ Completed baseline: Near-term follow-up: -1. Design a workspace dashboard/navigation entry point for sessions, plans, defaults, transfer, and future save-wide views. -2. Add browser smoke tests for graph rendering, planner editing, persistence reload, share/import flows, and infeasible/error states. -3. Improve responsive workbench behavior on narrow screens without trying to make the full graph experience equivalent to desktop. -4. Improve graph connection display controls and selected-flow readability behind renderer-neutral display settings. -5. Profile full-data solves and larger graph layout; move solver/layout work to Web Workers only if the UI visibly stalls. -6. Decide whether Dagre remains sufficient or whether ELK should replace it behind the existing renderer-neutral graph boundary. -7. Continue power planning from explicit generator/fuel targets toward solver-selected fuels, maximize-power objectives, and richer nuclear waste handling. -8. Draft the linked-plan contract model before adding logistics-backed links or session-wide production balance. -9. Keep save-file import, randomized node seeds, session-scale logistics/map planning, and assistant/tooling integrations in RFC/future-work space unless explicitly pulled forward. +1. Add browser smoke tests for graph rendering, planner editing, persistence reload, share/import flows, and infeasible/error states. +2. Add a selected-node action tray for existing safe actions such as done state, notes, path focus, relevant workbench controls, and current sink actions. +3. Implement session-scoped linked-plan contracts for partial output reservations and destination external-input coverage before adding logistics-backed links. +4. Add manual linked-plan editing, then node-driven plan extraction once manual partial links are stable. +5. Design a workspace dashboard/navigation entry point for sessions, plans, defaults, transfer, and future save-wide views once sessions have link status worth summarizing. +6. Improve responsive workbench behavior on narrow screens without trying to make the full graph experience equivalent to desktop. +7. Improve graph connection display controls and selected-flow readability behind renderer-neutral display settings. +8. Profile full-data solves and larger graph layout; move solver/layout work to Web Workers only if the UI visibly stalls. +9. Decide whether Dagre remains sufficient or whether ELK should replace it behind the existing renderer-neutral graph boundary. +10. Continue power planning from explicit generator/fuel targets toward solver-selected fuels, maximize-power objectives, and richer nuclear waste handling. +11. Keep save-file import, randomized node seeds, session-scale logistics/map planning, and assistant/tooling integrations in RFC/future-work space unless explicitly pulled forward. diff --git a/docs/rfc/linked-plan-contracts.md b/docs/rfc/linked-plan-contracts.md new file mode 100644 index 0000000..cf54f63 --- /dev/null +++ b/docs/rfc/linked-plan-contracts.md @@ -0,0 +1,289 @@ +# RFC: Linked Plan Contracts And Node Extraction + +Status: Draft backlog + +## Summary + +Linked plans are the first concrete step from single-factory planning toward +session-scale planning. The core idea is simple: one plan can reserve part of +its output for another plan, and the receiving plan can treat that amount as an +external input. + +The model should start with explicit, partial, manually edited links before +logistics, save imports, route capacity, or automatic session-wide solving. A +link is user intent stored at the session layer. It references plans and item +rates, then derives its current healthy, short, or overcommitted state from the +latest solves. It must not persist copied solver output as authoritative state. + +This RFC also covers the selected-node action tray and the "extract this +production node into another plan" workflow, because those actions are likely +to become the most natural way users create linked factories from an already +solved graph. + +## Goals + +- Let users connect an output from one plan to an input of another plan. +- Support partial amounts, such as exporting 80 of a 240/min output. +- Keep each plan understandable as its own factory plan. +- Keep links session-scoped so plan JSON/share payloads stay portable. +- Show source shortages, destination under-supply, and overcommitted exports. +- Provide a small selected-node action tray for common graph actions. +- Let users split a complex solved production node into a new or existing plan. +- Prepare the model for future logistics routes without implementing routes + first. + +## Non-Goals + +- Do not add train, truck, drone, belt, or pipe capacity in the first link pass. +- Do not solve circular plan networks automatically. +- Do not make save import required for linked plans. +- Do not turn session links into persisted solver output. +- Do not make the graph renderer own link domain state. +- Do not replace the existing full inspector with the node action tray. + +## Product Model + +Current plans already have `itemInputs`, which represent materials supplied by +another factory. Linked plans should build on that mental model instead of +inventing a second kind of destination demand. + +Recommended first model: + +- A destination plan keeps an external input requirement by item and rate. +- A session link can satisfy all or part of that external input requirement. +- If inbound links provide less than the input requirement, the remaining amount + is shown as manual or unlinked external supply. +- If inbound links provide more than the input requirement, the destination is + overcovered and should warn instead of silently changing the plan. +- The destination plan remains solvable as a standalone plan because the + external input requirement is still normal plan intent. +- Session links explain where that input is supposed to come from when the plan + is viewed inside its session. + +For source plans, links should reserve output without changing the source +production target: + +- A fixed output target can export part or all of its requested rate. +- A maximize output target can export part of the solved rate when solved. +- A surplus or byproduct can be exported only when the current solved plan + produces enough surplus. +- Link reservations and target-output sink reservations should be displayed + against the same available output so users see when they overcommit a target. + +## Contract Shape + +A first implementation should keep the stored contract small and versioned: + +```ts +interface PlannerSessionLink { + id: string; + itemId: ItemId; + amountPerMinute: number; + source: PlannerSessionLinkSource; + destination: PlannerSessionLinkDestination; + note?: string; + paused?: boolean; +} + +type PlannerSessionLinkSource = + | { + kind: 'target-output'; + projectId: string; + targetId: string; + } + | { + kind: 'surplus'; + projectId: string; + }; + +interface PlannerSessionLinkDestination { + kind: 'external-input'; + projectId: string; + itemId: ItemId; +} +``` + +Keep source references precise where possible. Multiple output targets can +request the same item, so a target-output link should reference `targetId`, not +only `itemId`. Surplus links can remain item-based because current surplus is +already aggregated by item. + +Likely later extensions: + +- `name` or `label` for user-facing logistics names. +- `routeId` once logistics capacity exists. +- `poolId` for named item pools or depots. +- `sourceNodeId` for graph-node-derived links if stable enough. +- `createdAt` and `updatedAt` if session history becomes useful. +- `localUseAmountPerMinute` if output reservation becomes richer than links and + sinks. + +## Derived Link State + +The stored link says what the user wants. Derived state says what is currently +true. + +For each link, derive: + +- source plan name and destination plan name +- requested amount per minute +- source available amount per minute +- destination input requirement amount per minute +- amount currently covered by this link +- short amount per minute +- source overcommit amount per minute +- destination overcoverage amount per minute +- status: `healthy`, `source-short`, `destination-overcovered`, + `source-overcommitted`, `paused`, `missing-source`, or + `missing-destination` + +Session balance can then aggregate: + +- produced by plan +- exported by plan +- imported by plan +- manually supplied or unlinked external input +- sunk output +- unused output or surplus +- short linked demand + +The balance is derived from plan intent, solve results, and session links. It is +not persisted as authoritative state. + +## Solver Behavior + +The first link pass should avoid making the solver coordinate multiple plans. +Each plan still solves independently. + +Recommended behavior: + +- Destination plans continue to solve from their own `itemInputs`. +- Links validate whether those inputs have a declared session source. +- Creating a link may create or increase the destination `itemInputs` amount, but + the link itself remains session-scoped. +- Editing a destination input should show linked coverage and manual remainder. +- Editing a link amount should optionally sync the destination input upward when + the link would otherwise exceed the input requirement. +- Cycles between plans should be detected in the session graph and reported. + They should not trigger recursive solving in the first pass. + +This keeps the implementation compatible with the current solve input shape and +preserves plan export/share behavior. + +## UX Surfaces + +Minimum useful surfaces: + +- Inputs panel: show each external input with linked coverage, manual remainder, + and actions to link or unlink supply. +- Output target rows or inspector: show how much of a target is reserved for + links, sinks, and unreserved output. +- Sinks panel: keep target-output sinks visible as a reservation that can + conflict with linked exports. +- Plan dock or workspace dashboard: show plan-level import/export badges and + warnings. +- Inspector: show linked input or export details for selected output, byproduct, + and external input nodes. +- Session-level view later: show plans as nodes and item links as edges. + +Avoid making the first UI depend on a full dashboard. A dashboard will become +more useful once links exist, but the link editor can start inside existing +Plan, Inputs, Sinks, and Inspector surfaces. + +## Selected-Node Action Tray + +The selected-node action tray should be a compact graph-adjacent control surface +for fast actions. It should complement the inspector, not replace it. + +Initial action candidates: + +- mark selected node done or not done +- edit node note +- focus upstream or downstream path +- sink surplus or target output when the existing sink rules support it +- open the owning workbench section + +Link-related action candidates: + +- expose selected output as a linkable export +- link selected output to another plan input +- create an external input from a selected assumed input +- extract selected production node into a new plan +- send selected production node to an existing plan + +Implementation boundary: + +- The graph renderer can display the tray and capture clicks. +- The available actions should be computed as renderer-neutral action + descriptors from planner state selectors. +- Commands should call the owning capability, such as `PlannerGraphStore`, + `PlannerPlanConfigStore`, or `PlannerWorkspaceSlice`. +- Do not add broad convenience forwards back to `PlannerStoreService`. + +## Extract Production Node Workflow + +The "extract to plan" action is the most ambitious link creation flow. It should +start conservative and explain what it changed. + +For a selected recipe node, `Extract to new plan` can: + +1. Derive target outputs from the selected node's outgoing solved product flows. +2. Create a new plan in the active session. +3. Add product targets to the new plan for those output rates. +4. Add or increase matching external inputs in the original destination plan. +5. Create session links from the new plan target outputs to the original plan + inputs. +6. Re-solve both plans. +7. Warn if the original plan still chooses to make the same item locally. + +`Send to existing plan` can follow the same shape, but add targets and links to +an existing selected source plan. + +Hard cases to handle explicitly: + +- Recipe nodes with multiple products or important byproducts. +- Recipe loops and self-flows. +- Power generator nodes that represent energy production rather than item + production. +- Nodes whose output is also requested as a final target. +- Existing target-output sink reservations for the same item. +- Original plans that still produce the item after external input is added. + +The first version does not need to perfectly remove every local production path. +It can create the split, re-solve, and surface any remaining local production as +something the user can address through recipe, input, or target settings. + +## Suggested Sequence + +1. Add browser smoke coverage for the planner basics before introducing more + stateful cross-plan behavior. +2. Build the selected-node action tray with existing safe actions: done, note, + focus path, open workbench, and existing sink actions. +3. Add session link types, persistence migration, hydration, and pure derived + balance selectors in `planner-core`. +4. Add manual link editing between a source output target and a destination + external input, including partial amounts and warning states. +5. Extend links to source surplus/byproduct outputs once target-output links are + stable. +6. Add graph and inspector affordances for creating links from selected output + or external input nodes. +7. Add `Extract to new plan` for recipe nodes, starting with straightforward + single-product nodes. +8. Add `Send to existing plan` and multi-output handling. +9. Add a schematic session overview after users can create meaningful links. +10. Add logistics route capacity and route nodes after manual links have proven + the contract. + +## Open Questions + +- Should destination inputs remain item-based, or should `itemInputs` become a + list with stable input IDs before links are implemented? +- Should link creation automatically increase a destination input, or ask first? +- Should target-output sinks and linked exports share one generalized output + reservation model? +- Should links reserve output before sinks, after sinks, or only report + overcommit without choosing a priority? +- What is the smallest useful session balance view before a full session graph? +- Should extracting a recipe node also disable that recipe in the original plan + when doing so is valid, or should it leave that decision to the user? +- How should linked plans behave when exported as standalone JSON files? diff --git a/docs/rfc/planner-next-roadmap.md b/docs/rfc/planner-next-roadmap.md index 1934b93..9988671 100644 --- a/docs/rfc/planner-next-roadmap.md +++ b/docs/rfc/planner-next-roadmap.md @@ -35,6 +35,7 @@ The first supporting layer is now in place: stronger panels, icons, defaults, pl - [Workbench UX Polish](./workbench-ux-polish.md) covers existing recipe/resource panel polish notes. - [Resource Providers, Save Imports, And Randomized Nodes](./resource-providers.md) covers save-file import and randomized-node research. - [Sinks, Disposal, And Power Targets](./sinks-disposal-and-power-targets.md) captures the shipped first passes for direct sinks, target-output sinks, and explicit power targets, plus follow-up work for conversion-to-sink, nuclear waste, solver-selected fuels, and maximize-power planning. +- [Linked Plan Contracts And Node Extraction](./linked-plan-contracts.md) defines the proposed session link contract, partial export/import behavior, selected-node action tray, and split-node-into-plan workflow. - [Plugin-Shaped Extension Seams](./plugin-extension-seams.md) captures when expanded features should become optional extension seams instead of built-in planner behavior. - [Product Spec](../product-spec.md) remains the north star for current scope and MVP boundaries. - [Architecture](../architecture.md) defines current package boundaries and renderer isolation. @@ -74,14 +75,16 @@ The initial workspace-priority set is implemented: These are plausible next steps before the larger save/logistics systems. Some are cleanup passes that can reduce risk before adding more stateful features. -1. Design a workspace dashboard/navigation entry point that can lead to factory plans, session views, defaults, transfer actions, and future save-wide planning. -2. Add browser smoke tests for graph rendering, planner editing, persistence reload, plan transfer, and infeasible/error states. -3. Improve graph connection display controls. -4. Add drill-in graph views for special production loops. -5. Design the linked-plan contract model before implementing logistics. -6. Extend sessions with only the metadata needed for save imports, linked plans, or notes once one of those features is pulled forward. -7. Continue sink/disposal/power planning from the shipped first passes toward conversion-to-sink, nuclear waste handling, solver-selected fuel sets, and maximize-power objectives. -8. Keep doing small technical refactors only where a capability or workbench slice has become hard to test or review; avoid recreating a broad planner facade. +1. Add browser smoke tests for graph rendering, planner editing, persistence reload, plan transfer, and infeasible/error states before adding more cross-plan state. +2. Add a selected-node action tray that exposes existing safe actions first: done state, notes, path focus, open relevant controls, and current sink actions. +3. Design and implement the linked-plan contract model for partial output reservations and destination external-input coverage before implementing logistics. +4. Add manual linked-plan editing between output targets and external inputs, including partial amounts and overcommit warnings. +5. Add recipe-node extraction into a new or existing plan once manual links are stable. +6. Design a workspace dashboard/navigation entry point that can lead to factory plans, session views, defaults, transfer actions, and future save-wide planning. +7. Improve graph connection display controls and add drill-in graph views for special production loops. +8. Extend sessions with only the metadata needed for save imports, linked plans, or notes once one of those features is pulled forward. +9. Continue sink/disposal/power planning from the shipped first passes toward conversion-to-sink, nuclear waste handling, solver-selected fuel sets, and maximize-power objectives. +10. Keep doing small technical refactors only where a capability or workbench slice has become hard to test or review; avoid recreating a broad planner facade. ## Future Systems @@ -112,7 +115,9 @@ This index keeps the original brainstorm traceable while the rest of the RFC gro | Save importing for defaults/plan settings | Sessions/save import | [Sessions And Saves](#sessions-and-saves) | | Randomized node maps | Resource providers | [Sessions And Saves](#sessions-and-saves) and [resource-provider RFC](./resource-providers.md) | | Game sessions | Persistence/product model | [Sessions And Saves](#sessions-and-saves) | -| Linked plans | Session-scale planning | [Linked Plans](#linked-plans) | +| Linked plans | Session-scale planning | [Linked Plans](#linked-plans) and [linked-plan RFC](./linked-plan-contracts.md) | +| Node action tray | Graph UX/session planning | [Inspector](#inspector) and [linked-plan RFC](./linked-plan-contracts.md) | +| Pull recipe node into another plan | Session-scale planning UX | [Linked Plans](#linked-plans) and [linked-plan RFC](./linked-plan-contracts.md) | | Train/vehicle logistics | Session-scale logistics | [Logistics](#logistics) | | Save-wide logistics overview | Session-scale logistics UX | [Session Logistics Overview](#session-logistics-overview) | | Solver priorities | Solver/UI | [Solver Objectives](#solver-objectives) | @@ -317,6 +322,19 @@ Concept: - A linked export can supply all or part of another plan's demand. - Session-level balance can show surplus, shortages, and overcommitted production. +The first implementation should treat links as session-scoped contracts layered +over existing plan intent: + +- Destination plans keep normal external input requirements. +- Links satisfy all or part of those requirements. +- Any input amount not covered by links remains manual or unlinked external supply. +- Source plans reserve part of a target output or solved surplus without + changing the source production target. +- Fixed targets can export from the configured amount; maximize targets and + surplus exports depend on the latest solved amount. +- Links and target-output sink rules should be shown as competing reservations + against the same available output when they touch the same item. + Possible link types: - Manual link: user says `Plan A exports 120 Rubber/min to Plan B`. @@ -333,6 +351,11 @@ Rules to protect clarity: - Warn when downstream demand exceeds upstream available export. - Avoid circular dependency solving at first; detect cycles and explain them. +The dedicated [Linked Plan Contracts And Node Extraction](./linked-plan-contracts.md) +RFC is the working contract for partial links, destination input coverage, +selected-node action tray behavior, and the workflow that extracts a solved +production node into a new or existing plan. + ## Logistics Logistics can eventually answer whether a session-wide plan is physically supportable, not just mathematically producible. @@ -607,17 +630,20 @@ Recommended path: ## Suggested Sequence -1. Design and implement a workspace dashboard/navigation entry point so users can choose plans, defaults, session-level surfaces, and future save views without landing directly in the last graph. -2. Add browser smoke tests around graph rendering, planner editing, persistence reload, plan transfer, and infeasible/error states. -3. Add graph connection display controls and drill-in views. -4. Write a focused RFC for linked-plan contracts: exports, imports, item pools, and how those should interact with manual external inputs. -5. Continue power planning with solver-selected fuel sets and maximize-power objectives once manual generator/fuel targets have enough UX mileage. -6. Extend the session data model only with fields needed by the linked-plan or save-import feature selected next. -7. Add session import/export after session-scoped data exists beyond plan grouping. -8. Prototype linked plans with manual links before logistics-backed links. -9. Add a schematic session logistics overview once linked plans exist. -10. Research save-derived logistics only after save import has a reliable parser boundary. -11. Treat planned locations and top-down factory layout as separate future RFCs before implementation. +1. Add browser smoke tests around graph rendering, planner editing, persistence reload, plan transfer, and infeasible/error states. +2. Build a selected-node action tray with existing safe graph actions so the interaction pattern is proven before link creation depends on it. +3. Add session-scoped linked-plan contracts, persistence migration, hydration, and pure balance selectors. +4. Add manual partial links from source output targets to destination external inputs, with source and destination warning states. +5. Extend links to solved surplus/byproduct outputs after target-output links are stable. +6. Add node-driven link creation from selected output and external input nodes. +7. Add recipe-node extraction into a new plan, then into an existing plan, starting with straightforward single-product recipe nodes. +8. Design and implement a workspace dashboard/navigation entry point once sessions have link status worth summarizing. +9. Add graph connection display controls and drill-in views. +10. Continue power planning with solver-selected fuel sets and maximize-power objectives once manual generator/fuel targets have enough UX mileage. +11. Add session import/export after session-scoped data exists beyond plan grouping. +12. Add a schematic session logistics overview once linked plans exist. +13. Research save-derived logistics only after save import has a reliable parser boundary. +14. Treat planned locations and top-down factory layout as separate future RFCs before implementation. ## Open Questions @@ -627,7 +653,9 @@ Recommended path: - Which icon sizes should be committed: `64x64`, `128x128`, or both? - How should the extracted map PNG and default node catalog be schema-validated and versioned before session/map planning uses them? - How should public distribution handle extracted game icons and asset licensing/community guidelines? -- Should plan links be one-to-one explicit connections, named item pools, or both? +- Should plan links start as one-to-one explicit connections and add named item pools later, or should both ship together? +- Should target-output sink rules and linked exports share one generalized output reservation model? +- Should extracting a production node also disable that recipe in the original plan when valid, or should Beltwise leave that choice to the user? - How much save-derived data should be allowed to update an existing plan automatically? - Are logistics routes best modeled as capacities first, or as physical objects first? - Should the inspector own navigation actions into panels, or should panel controls remain independent and merely react to selection? diff --git a/docs/rfc/sinks-disposal-and-power-targets.md b/docs/rfc/sinks-disposal-and-power-targets.md index 4942a6c..30d01ec 100644 --- a/docs/rfc/sinks-disposal-and-power-targets.md +++ b/docs/rfc/sinks-disposal-and-power-targets.md @@ -94,6 +94,7 @@ Remaining questions: - Should target sinking also be editable directly on the target row, or should the Sinks panel remain the only add/edit surface for now? - Should the UI add an `all remaining target output` mode, or keep the current amount-based rule only? - How should target-output sink allocations interact with future linked-plan exports, local-use reservations, and session balance? +- Should target-output sink allocations and linked-plan exports eventually share one output reservation model, or remain separate rules with shared overcommit warnings? ## Conversion-To-Sink @@ -212,6 +213,7 @@ Node action drawer idea: - Initial actions might include sink surplus, mark done, and note. - Future actions could include move node to its own plan, create linked export, focus path, or open disposal conversion. - The drawer should complement the inspector, not replace it. +- Link and extraction actions should follow the [linked-plan contract RFC](./linked-plan-contracts.md) so the graph surface does not own session domain state. ## Suggested Sequence diff --git a/e2e/planner-smoke.spec.ts b/e2e/planner-smoke.spec.ts new file mode 100644 index 0000000..d19d007 --- /dev/null +++ b/e2e/planner-smoke.spec.ts @@ -0,0 +1,55 @@ +import { expect, type Locator, type Page, test } from '@playwright/test'; + +test.describe('planner browser smoke', () => { + test('loads the planner shell with an empty graph prompt', async ({ page }) => { + await openFreshPlanner(page); + + await expect(page.getByText('Beltwise', { exact: true })).toBeVisible(); + await expect(page.getByRole('region', { name: 'Production graph' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Production Targets' })).toBeVisible(); + await expect(page.locator('.empty-graph')).toContainText('Add a target to build a plan.'); + }); + + test('solves an Iron Plate target and keeps it after reload', async ({ page }) => { + await openFreshPlanner(page); + await configureIronPlateTarget(page); + + const graphNodes = page.locator('.production-node'); + await expectAtLeast(graphNodes, 2); + await expect(graphNodes.filter({ hasText: 'Iron Plate' }).first()).toBeVisible(); + await expect(page.locator('.edge-label').filter({ hasText: 'Iron Ore' }).first()).toBeVisible(); + + await page.reload(); + + await expect(page.getByText('Beltwise', { exact: true })).toBeVisible(); + await expect(page.locator('.production-node').filter({ hasText: 'Iron Plate' }).first()) + .toBeVisible(); + await expect(page.getByRole('button', { name: /Choose active plan: .*Iron Plate/i })) + .toBeVisible(); + }); +}); + +async function openFreshPlanner(page: Page): Promise { + await page.goto('/'); + await page.evaluate(() => window.localStorage.clear()); + await page.reload(); + + await expect(page.getByText('Beltwise', { exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Production Targets' })).toBeVisible(); +} + +async function configureIronPlateTarget(page: Page): Promise { + await page.getByRole('button', { name: 'Add product target' }).first().click(); + await page.getByRole('button', { name: /Select an item/i }).click(); + + const searchInput = page.getByRole('searchbox', { name: 'Search target items' }); + await searchInput.fill('Iron Plate'); + await page.getByRole('option', { name: /^Iron Plate$/ }).click(); + + const amountInput = page.getByLabel('Amount per minute').first(); + await amountInput.fill('30'); +} + +async function expectAtLeast(locator: Locator, expectedCount: number): Promise { + await expect.poll(async () => locator.count()).toBeGreaterThanOrEqual(expectedCount); +} diff --git a/package-lock.json b/package-lock.json index 4e6351e..fcb1c2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@angular-devkit/build-angular": "^21.2.11", "@angular/cli": "^21.2.11", "@angular/compiler-cli": "^21.2.13", + "@playwright/test": "^1.60.0", "@types/node": "^22.15.0", "jsdom": "^29.1.1", "prettier": "^3.5.3", @@ -16295,6 +16296,69 @@ "bin": { "extract-satisfactory-data": "src/index.ts" } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } } } } diff --git a/package.json b/package.json index 5b40e4d..c39d0bb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "build": "npm --workspace @beltwise/game-data run build && npm --workspace @beltwise/planner-core run build && npm --workspace @beltwise/solver run build && npm --workspace @beltwise/extract-satisfactory-data run build && npm --workspace @beltwise/web run build", "dev": "npm --workspace @beltwise/web run start", "test": "vitest run --config vitest.config.mjs", + "test:smoke": "playwright test --config playwright.config.ts", "test:web": "vitest run --config vitest.config.mjs apps/web/src", "typecheck": "npm run typecheck --workspaces --if-present", "data:extract": "npm --workspace @beltwise/game-data run build && npm --workspace @beltwise/extract-satisfactory-data run start --", @@ -31,6 +32,7 @@ "@angular-devkit/build-angular": "^21.2.11", "@angular/cli": "^21.2.11", "@angular/compiler-cli": "^21.2.13", + "@playwright/test": "^1.60.0", "@types/node": "^22.15.0", "jsdom": "^29.1.1", "prettier": "^3.5.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..705e747 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from '@playwright/test'; + +const devServerCommand = `${process.platform === 'win32' ? 'npm.cmd' : 'npm'} run dev`; + +export default defineConfig({ + testDir: './e2e', + testMatch: /.*smoke\.spec\.ts/, + fullyParallel: false, + timeout: 60_000, + expect: { + timeout: 10_000, + }, + reporter: process.env.CI ? [['dot'], ['html', { open: 'never' }]] : [['list']], + use: { + baseURL: 'http://127.0.0.1:4200', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + }, + webServer: { + command: devServerCommand, + url: 'http://127.0.0.1:4200', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +});