diff --git a/non production apps/EscapeRoomApp/.github/copilot-instructions.md b/non production apps/EscapeRoomApp/.github/copilot-instructions.md new file mode 100644 index 00000000..4a47d818 --- /dev/null +++ b/non production apps/EscapeRoomApp/.github/copilot-instructions.md @@ -0,0 +1,473 @@ +# Copilot Instructions: Creating Escape Room Venues on BCTalent.EscapeRoom + +## What Is This? + +The **BCTalent.EscapeRoom** framework (ID range 73920-73999) enables developers to create gamified, task-validated learning experiences inside Business Central. You build a **venue app** — a separate AL extension — that depends on the framework and adds venues, rooms, and tasks using AL interfaces. + +This file is the complete guide for an agent creating a new escape room venue from scratch. See [Docs/Framework/](Docs/Framework/) for detailed reference. + +--- + +## Part 1: Project Setup + +### app.json + +```json +{ + "id": "your-guid-here", + "name": "My Escape Room Venue", + "publisher": "Your Name", + "version": "1.0.0.0", + "dependencies": [ + { + "id": "f03c0f0c-d887-4279-b226-dea59737ecf8", + "name": "BCTalent.EscapeRoom", + "publisher": "waldo & AJ", + "version": "1.3.0.0" + } + ], + "idRanges": [{ "from": 50000, "to": 50099 }] +} +``` + +Always use the **vjeko-al-objid** tool (`getNextObjectId`) before creating any AL object to reserve a unique object ID. Never hardcode or guess IDs. + +### Recommended Folder Structure + +``` +MyVenueApp/ +├── Venue/ +│ ├── MyVenue.Codeunit.al (implements iEscapeRoomVenue) +│ └── EscapeRoomVenueExt.EnumExt.al +├── Rooms/ +│ ├── Room1MyRoom.Codeunit.al (implements iEscapeRoom) +│ └── EscapeRoomExt.EnumExt.al +├── Tasks/ +│ ├── R1T1MyTask.Codeunit.al (implements iEscapeRoomTask) +│ └── EscapeRoomTaskExt.EnumExt.al +├── Resources/ +│ ├── Room1MyRoomDescription.html +│ ├── Room1MyRoomSolution.html +│ └── RoomCompletedImage.png +└── Install/ + └── InstallVenue.Codeunit.al (registers venue via UpdateVenue) +``` + +--- + +## Part 2: Extending the Enums + +Three enums must be extended — one for each level of the hierarchy. + +```al +enumextension 50000 "My Venue Enum Ext" extends "Escape Room Venue" +{ + value(50000; MyVenue) { Caption = 'My Learning Venue'; } +} + +enumextension 50001 "My Rooms Enum Ext" extends "Escape Room" +{ + value(50001; Room1Introduction) { Caption = 'Room 1: Introduction'; } + value(50002; Room2Challenge) { Caption = 'Room 2: Challenge'; } +} + +enumextension 50002 "My Tasks Enum Ext" extends "Escape Room Task" +{ + value(50010; R1Task1CreateField) { Caption = 'Create Custom Field'; } + value(50011; R1Task2AddLogic) { Caption = 'Add Business Logic'; } + value(50020; R2Task1OptimizeQuery) { Caption = 'Optimize Query'; } +} +``` + +--- + +## Part 3: Implementing the Interfaces + +### 3.1 iEscapeRoomVenue + +```al +codeunit 50000 "My Venue" implements iEscapeRoomVenue +{ + procedure GetVenueRec() VenueRec: Record "Escape Room Venue" + var + Me: ModuleInfo; + begin + NavApp.GetCurrentModuleInfo(Me); + VenueRec.Id := Me.Name; + VenueRec.Name := Me.Name; + VenueRec.Description := 'One-line description of what participants will do'; + VenueRec.Venue := Enum::"Escape Room Venue"::MyVenue; + VenueRec."App ID" := Me.Id; + VenueRec.Publisher := Me.Publisher; + end; + + procedure GetVenue(): Enum "Escape Room Venue" + begin + exit(Enum::"Escape Room Venue"::MyVenue); + end; + + procedure GetRooms() Rooms: List of [Interface iEscapeRoom] + var + Room1: Codeunit "Room1 Introduction"; + Room2: Codeunit "Room2 Challenge"; + begin + Rooms.Add(Room1); + Rooms.Add(Room2); + end; + + procedure GetRoomCompletedImage() InStr: InStream + begin + // Return empty stream or load an image from resources + end; + + procedure GetVenueCompletedImage() InStr: InStream + begin + // Return empty stream or load a completion badge + end; +} +``` + +### 3.2 iEscapeRoom + +```al +codeunit 50010 "Room1 Introduction" implements iEscapeRoom +{ + procedure GetRoomRec() RoomRec: Record "Escape Room" + var + Me: ModuleInfo; + begin + NavApp.GetCurrentModuleInfo(Me); + RoomRec."Venue Id" := Me.Name; + RoomRec.Name := Format(this.GetRoom()); + RoomRec.Description := 'Short plain-text description for quick preview'; + RoomRec.Sequence := 1; + end; + + procedure GetRoom(): Enum "Escape Room" + begin + exit(Enum::"Escape Room"::Room1Introduction); + end; + + procedure GetTasks() Tasks: List of [Interface iEscapeRoomTask] + var + Task1: Codeunit "R1T1 Create Custom Field"; + Task2: Codeunit "R1T2 Add Business Logic"; + begin + Tasks.Add(Task1); + Tasks.Add(Task2); + end; + + procedure GetRoomDescription() Description: Text + begin + // Return HTML content string, or leave empty and load via BLOB + // See Resources/Room1IntroductionDescription.html + end; + + procedure GetRoomSolution() Solution: Text + begin + // Return HTML content string + // See Resources/Room1IntroductionSolution.html + end; + + procedure GetHintImage() InStr: InStream + begin + // Optional — return empty stream if no image + end; +} +``` + +### 3.3 iEscapeRoomTask + +```al +codeunit 50020 "R1T1 Create Custom Field" implements iEscapeRoomTask +{ + SingleInstance = true; // Required when using event subscribers + + var + Room: Codeunit "Room1 Introduction"; + + procedure GetTaskRec() TaskRec: Record "Escape Room Task" + var + Me: ModuleInfo; + begin + NavApp.GetCurrentModuleInfo(Me); + TaskRec."Venue Id" := Me.Name; + TaskRec."Room Name" := Format(Room.GetRoom()); + TaskRec.Name := Format(this.GetTask()); + TaskRec.Description := 'Add an "Information" field to the Customer table.'; + end; + + procedure GetTask(): Enum "Escape Room Task" + begin + exit(Enum::"Escape Room Task"::R1Task1CreateField); + end; + + procedure IsValid(): Boolean + var + Field: Record Field; + begin + Field.SetRange(TableNo, Database::Customer); + Field.SetRange(FieldName, 'Information'); + exit(not Field.IsEmpty()); + end; + + procedure GetHint(): Text + begin + exit('Add a text field named "Information" to the Customer table using a table extension.'); + end; +} +``` + +--- + +## Part 4: Registering the Venue + +The venue must be registered on install (and upgrade). The framework creates all records automatically from the interface implementations. + +```al +codeunit 50001 "Install My Venue" +{ + Subtype = Install; + + trigger OnInstallAppPerCompany() + begin + RegisterVenue(); + end; + + local procedure RegisterVenue() + var + EscapeRoom: Codeunit "Escape Room"; + MyVenue: Codeunit "My Venue"; + begin + EscapeRoom.UpdateVenue(MyVenue); + end; +} +``` + +The `UpdateVenue()` call walks the interface hierarchy (`GetRooms()` → `GetTasks()`) and creates or updates all records in the framework tables. + +--- + +## Part 5: Task Validation Patterns + +See [Docs/Framework/Task-Validation.md](Docs/Framework/Task-Validation.md) for full examples. Summary of the three patterns: + +### Pattern 1: Polling + +`IsValid()` returns `true` when the condition is met. Framework calls it periodically via `UpdateStatus()`. + +**Use for:** Field existence, object existence, configuration checks, app installation. + +```al +procedure IsValid(): Boolean +var + Field: Record Field; +begin + Field.SetRange(TableNo, Database::Customer); + Field.SetRange(FieldName, 'Information'); + exit(not Field.IsEmpty()); +end; +``` + +### Pattern 2: Event Subscriber + +`IsValid()` returns `false`. A separate event subscriber detects when the participant performs the right action and sets a flag. + +**Use for:** Monitoring table inserts/modifies, detecting specific user interactions, tracking data flow. + +**Critical requirements:** +- Codeunit must have `SingleInstance = true` +- Use `OnAfterInsertEvent` for new records (NOT `OnAfterModifyEvent` — inserts and modifies are distinct events) +- Store state in a codeunit-level variable + +```al +codeunit 50021 "R1T2 Add Business Logic" implements iEscapeRoomTask +{ + SingleInstance = true; + + var + TaskCompleted: Boolean; + + procedure IsValid(): Boolean + begin + exit(TaskCompleted); + end; + + [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnAfterInsertEvent, '', false, false)] + local procedure OnSalesHeaderInserted(var Rec: Record "Sales Header"; RunTrigger: Boolean) + begin + if Rec."Document Type" <> Rec."Document Type"::Order then + exit; + TaskCompleted := true; + end; + // ... (GetTaskRec, GetTask, GetHint as usual) +} +``` + +### Pattern 3: Test Codeunit + +Framework runs a test codeunit to validate complex UI or workflow scenarios. + +```al +procedure GetTaskRec() TaskRec: Record "Escape Room Task" +begin + // ... + TaskRec.TestCodeunitId := Codeunit::"My Test Validation"; // Framework runs this +end; +``` + +--- + +## Part 6: Room and Task Design + +### Task Selection: Only Observable Problems + +Only include a task if the performance problem (or challenge) is **viscerally observable** — participants must be able to feel or clearly see the difference before and after. + +- Exclude tasks that only matter at extreme scale not present in the demo environment +- Exclude tasks that are "interesting in theory" but produce no noticeable effect in practice +- A locking/blocking scenario requires **actual concurrent blocking** between two live sessions — a slow sequential query is NOT a locking demonstration + +### For Performance Rooms: Key Rules + +See [Docs/Framework/Performance-Room-Design.md](Docs/Framework/Performance-Room-Design.md) for the full guide. The essential rules: + +1. **Minimum data:** At least 25,000 records are needed for performance differences to be perceptible in a group setting. + +2. **NST caching:** Add `SelectLatestVersion()` to every page action that triggers a demo measurement, with this exact comment: `// DO NOT REMOVE — needed for consistent demo results`. Without it, the BC NST cache makes the second run appear artificially fast. + +3. **Measurement code naming:** Use `R[room]-[SHORT-DESCRIPTION]` (e.g., `R4-ACTIVE`, `R7-N+1`). This code appears in **two places** — the page action that creates the measurement record, and the task's `IsValid()` that reads it. **They must match exactly.** A mismatch means the task can never be completed. + +4. **Use `OnAfterInsertEvent`** when subscribing to Performance Measurement records — they are inserted, never modified. + +5. **Compare vs. previous measurement**, not an absolute threshold. Require improvement in both duration AND SQL statement count. + +6. **Two-extension architecture (code-based venues only):** When participants need to modify AL code, provide a companion code app (e.g., a PTE). Ideally that app depends only on the framework. If it also needs infrastructure from the venue app (e.g., measurement tables or manager codeunits), that dependency is acceptable provided `"dependencyPublishingOption": "Ignore"` is set in the companion app's `launch.json`. Data/configuration venues (e.g., a consultant track where participants work only in BC UI) have no companion app at all. + +7. **Add `"dependencyPublishingOption": "Ignore"` to the companion app's `launch.json`** (code-based venues only) to prevent confusing errors when participants publish from VS Code. + +### Selecting the Best Anti-Pattern Example + +When choosing an example, rank by: (1) clarity of fix — small, targeted code change; (2) educational commonness — genuinely seen in production code; (3) measurability — dramatic, unambiguous improvement in SQL count and duration. + +The canonical N+1 example: **`CalcFields` inside a `repeat...until` loop → `SetAutoCalcFields` before `FindSet`**. It meets all three criteria. + +--- + +## Part 7: HTML Room Content + +Every room has a Description HTML file (what participants receive) and a Solution HTML file (what the facilitator and the post-delay view show). See [Docs/Framework/Room-Content-HTML.md](Docs/Framework/Room-Content-HTML.md) for the complete guide. The essential rules follow. + +### The One Rule That Overrides All Others + +**Descriptions present the MYSTERY. Solutions reveal the ANSWER.** + +The self-test: "Could they copy-paste something from this description to solve the task?" If yes, it is too revealing. + +### BC HTML Viewer Constraints + +No JavaScript, no external CSS, no emoji or emoticons (the BC HTML viewer cannot render them). Use inline styles only. + +### Description File Structure (7 sections, in this order) + +1. **HTML shell + H1** — `Room X: Short Title` +2. **TL;DR** — 2-3 sentences max. What's broken, what they'll do, how they prove success. +3. **The Challenge** — ONE paragraph (2-4 sentences). Sets scene + one-sentence "why this matters" woven in. No learning objectives. +4. **Your Mission** — H3 headings (descriptive names, never "Task 1:"). Each mission item has inline: goal, object/procedure reference, how-to-trigger steps (numbered `
` code blocks, and `Why This Matters:
` explanations +4. **Forbidden in solutions:** Performance Results comparison tables, Profiler Comparisons sections, task number references in headings +5. **Verification** checklist with Update Status reminder +6. **What You've Accomplished** summary + +### File Naming + +- Description: `Room[N][Topic]Description.html` — e.g., `Room2DataTransferDescription.html` +- Solution: `Room[N][Topic]Solution.html` — e.g., `Room2DataTransferSolution.html` + +--- + +## Part 8: Quality Checklists + +### AL Code Checklist + +- [ ] All object IDs reserved via vjeko-al-objid before creating objects +- [ ] Three enum extensions created (Venue, Room, Task) +- [ ] `GetVenueRec()` populates Id, Name, Description, Venue, App ID, Publisher +- [ ] `GetRoomRec()` populates Venue Id, Name, Description, Sequence +- [ ] `GetTaskRec()` populates Venue Id, Room Name, Name, Description +- [ ] Install codeunit calls `EscapeRoom.UpdateVenue()` +- [ ] Task codeunits with event subscribers have `SingleInstance = true` +- [ ] Event subscribers use `OnAfterInsertEvent` (not Modify) where inserting records +- [ ] Measurement codes match exactly between page action writer and `IsValid()` reader +- [ ] `IsValid()` compares vs. previous measurement (not absolute threshold) +- [ ] Both duration AND SQL count required for performance task completion +- [ ] `SelectLatestVersion()` added to all performance demo page actions +- [ ] `// DO NOT REMOVE` comment on `SelectLatestVersion()` calls +- [ ] Companion code app (if any) does NOT depend on venue app (one-way dependency only) +- [ ] `"dependencyPublishingOption": "Ignore"` in companion app's `launch.json` (if applicable) + +### HTML Description Checklist + +- [ ] Complete HTML structure with DOCTYPE +- [ ] TL;DR (2-3 sentences max) +- [ ] The Challenge (ONE paragraph, "why this matters" woven in as one sentence) +- [ ] Your Mission with descriptive H3 headings (no task numbers) +- [ ] Each mission item is self-contained: goal + location + hint + validation inline +- [ ] Hints use the exact BC UI action label; non-toolbar actions include navigation path +- [ ] No standalone Skills Tested, Key Concepts, Validation, or Where to Look sections +- [ ] No Performance Results or Baseline tables +- [ ] Update Status reminder present +- [ ] What's Next mentions only the next room (one sentence) +- [ ] Problem described ONCE (no repetition across sections) +- [ ] MYSTERIOUS — no method names, no solution code, no technical terms that reveal the answer +- [ ] No "you will learn" language +- [ ] No emoticons or emojis + +### HTML Solution Checklist + +- [ ] H1 with "Solution: Room X - Title" +- [ ] Descriptive H2/H3 headings (no task numbers) +- [ ] Complete, copy-pasteable code in `` blocks +- [ ] "Why This Matters" after each major code block +- [ ] No Performance Results comparison tables +- [ ] No Profiler Comparisons sections +- [ ] Verification checklist with Update Status reminder +- [ ] "What You've Accomplished" summary +- [ ] No emoticons or emojis + +--- + +## Reference + +| Doc | Contents | +|---|---| +| [Architecture.md](Docs/Framework/Architecture.md) | Core components, design patterns, status management | +| [Creating-Rooms.md](Docs/Framework/Creating-Rooms.md) | Full step-by-step AL walkthrough | +| [Task-Validation.md](Docs/Framework/Task-Validation.md) | Complete examples for all three validation patterns | +| [Room-Content-HTML.md](Docs/Framework/Room-Content-HTML.md) | Full HTML content guide with all rules and examples | +| [Performance-Room-Design.md](Docs/Framework/Performance-Room-Design.md) | Performance-themed room patterns: NST caching, measurement codes, task selection | +| [Telemetry-Integration.md](Docs/Framework/Telemetry-Integration.md) | Application Insights events and scoring | +| [API-Reference.md](Docs/Dev/API-Reference.md) | Complete interface method specifications | diff --git a/non production apps/EscapeRoomApp/Docs/Framework/Performance-Room-Design.md b/non production apps/EscapeRoomApp/Docs/Framework/Performance-Room-Design.md new file mode 100644 index 00000000..46f6dadb --- /dev/null +++ b/non production apps/EscapeRoomApp/Docs/Framework/Performance-Room-Design.md @@ -0,0 +1,274 @@ +# Performance-Themed Room Design + +## Overview + +This guide covers patterns and rules specific to escape rooms where participants must diagnose and fix **performance problems** — slow queries, N+1 patterns, locking, or suboptimal AL code. + +Performance rooms can be structured in two ways: + +### Code-Based Venues (Two-Extension Architecture) + +Used when participants must **write or modify AL code** to fix the challenge: + +- **Venue app** (e.g., `OptimAL.EscapeRoom1`) — the framework implementation, rooms, tasks, HTML content, and measurement logic +- **Companion code app** (e.g., `OptimAL.PTE`) — the "broken" extension participants download, fix, and republish + +This is the right pattern when the fix requires editing AL source code (codeunit procedures, table extensions, etc.). + +### Data/Configuration Venues (Single-Extension Architecture) + +Used when participants are **Business Central users or consultants** working entirely in the BC UI — configuring settings, creating records, running reports, or analysing data. There is **no companion code app**. Task validation uses the Polling pattern (checking for records, field values, or configuration) or the Event Subscriber pattern (monitoring table inserts/modifies). + +Example: A consultant-track venue that tests knowledge of BC functional areas. Participants set up data, run processes, or configure modules — all inside BC. The venue app is the only extension. + +The rest of this document covers patterns that apply to **both** architectures, with the two-extension-specific rules called out explicitly. + +--- + +## Two-Extension Architecture (Code-Based Venues Only) + +> This section applies only when participants need to modify and republish AL code. Skip this for data/configuration venues. + +### Dependency Direction + +The ideal dependency graph is strictly one-way: + +``` +OptimAL.EscapeRoom1 ──depends on──► BCTalent.EscapeRoom (framework) +OptimAL.PTE ──depends on──► BCTalent.EscapeRoom (framework) +``` + +The companion app ideally depends only on the framework. However, if it also needs infrastructure from the venue app (for example, a performance measurement table or manager codeunit that lives in the venue app), a dependency on the venue app is acceptable — provided `"dependencyPublishingOption": "Ignore"` is set in the companion app's `launch.json` (see below). + +**Why this matters:** Without `"dependencyPublishingOption": "Ignore"`, publishing from VS Code temporarily uninstalls all dependency apps including the venue app, causing "could not be loaded" errors. The `Ignore` setting prevents this. + +### Publishing from VS Code + +Participants publish the companion app from VS Code using F5. Add this to its `launch.json`: + +```json +{ + "dependencyPublishingOption": "Ignore" +} +``` + +Without this, the dependent venue app gets briefly uninstalled during the companion app's publishing cycle, causing confusing errors. + +--- + +## Task Selection: Only Observable Problems + +A performance task earns its place only if attendees can **physically feel the slowness** with the current dataset. + +**Include a task if:** +- The operation takes several seconds with the test dataset +- Participants can trigger it easily from the UI and compare before/after themselves +- The improvement after fixing is dramatic and unambiguous (e.g., 30 seconds → 1 second) + +**Do NOT include a task if:** +- The problem only shows at extreme scale (>1M records) not present in the demo environment +- The performance difference requires instrumentation to notice +- The scenario is "interesting in theory" but produces no observable difference in practice +- The locking scenario requires timing that is impossible to reproduce consistently + +### Locking / Blocking Scenarios + +A "blocking" or "locking" demo requires **actual concurrent blocking** between two live sessions: + +- Two browser tabs logged in as different users +- One tab runs a long-running write (without committing) +- The other tab is blocked attempting to read or write the same records + +A **slow sequential query** does not demonstrate locking. Do not conflate the two — participants will be confused. If you cannot demonstrate actual blocking reliably in a workshop, do not include the task. + +--- + +## Minimum Data Requirements + +At least **25,000 records** are needed for performance problems to register as meaningfully slow in a group workshop setting. Smaller datasets may produce imperceptible differences. + +Include a **"Generate More Test Data"** action in the PTE app or venue app. If the environment has fewer than 25,000 records, the description Warning box must instruct participants to run this first. + +--- + +## Measuring Performance: The Measurement Pattern + +Performance rooms use a custom measurement table in the venue app to record operation metrics. The key types are duration and SQL statement count. + +### Measurement Code Naming Convention + +Format: `R[room]-[SHORT-DESCRIPTION]` + +Examples: +- `R4-ACTIVE` — Room 4, Active Customer Report +- `R7-N+1` — Room 7, N+1 query operation +- `R2-TRANSFER` — Room 2, table transfer operation + +The measurement code is the **primary key** that connects: +1. The page action that **creates** the measurement record +2. The task codeunit's `IsValid()` that **reads** the measurement record + +**These two strings must match exactly.** A mismatch means the task can never be completed — participants will see a task stuck permanently on "Open" with no error message. + +**Verification step after implementing any task:** Search the codebase for both occurrences of the measurement code string and confirm they are identical. + +--- + +## NST Caching: The SelectLatestVersion Rule + +The BC NST (NAV Service Tier) caches data between requests. On the **second run** of a performance measurement, the NST cache can make the operation appear dramatically faster than it actually is. This destroys the demo — participants will think they have "fixed" the problem when they have not. + +**Fix:** Add `SelectLatestVersion()` to every page action that triggers a performance measurement demo: + +```al +action(RunActiveCustomerReport) +{ + ApplicationArea = All; + Caption = 'Run Active Customer Report'; + + trigger OnAction() + begin + // DO NOT REMOVE — SelectLatestVersion needed for consistent demo results + // Without it, NST caching makes the second run appear artificially fast. + CurrPage.SetSelectionFilter(Rec); + Rec.SelectLatestVersion(); + + PerformanceMeasurementMgr.StartMeasurement('R4-ACTIVE'); + RunActiveCustomerReport(); + PerformanceMeasurementMgr.StopMeasurement('R4-ACTIVE'); + end; +} +``` + +Add the comment `// DO NOT REMOVE — ... needed for demo purposes` so that future developers do not clean it up. + +--- + +## Task Validation for Performance Rooms + +Performance rooms use `IsValid()` on a task codeunit to decide whether a task is complete. The two common approaches are polling the measurement table directly or subscribing to table events. + +### Which Event to Subscribe To + +The Performance Measurement pattern has two phases: + +- `StartMeasurement()` **inserts** a new record (the "start" entry). +- `StopMeasurement()` **modifies** that same record to write Duration, SQL count, and other metrics. + +Choose the event based on what you want to detect: + +| Goal | Event | +|---|---| +| Detect that the participant ran the operation at all | `OnAfterInsertEvent` — fires when `StartMeasurement()` inserts the record | +| Validate the actual performance metrics (duration, SQL count) | `OnAfterModifyEvent` — fires when `StopMeasurement()` writes the results | + +Example — subscribe to metrics written by `StopMeasurement()`: + +```al +[EventSubscriber(ObjectType::Table, Database::"Performance Measurement", OnAfterModifyEvent, '', false, false)] +local procedure OnMeasurementCompleted(var Rec: Record "Performance Measurement") +begin + if Rec.Code <> 'R4-ACTIVE' then + exit; + if Rec.SqlStatementsCount < 10 then + TaskCompleted := true; +end; +``` + +Example — subscribe to record existence inserted by `StartMeasurement()`: + +```al +[EventSubscriber(ObjectType::Table, Database::"Performance Measurement", OnAfterInsertEvent, '', false, false)] +local procedure OnMeasurementStarted(var Rec: Record "Performance Measurement") +begin + if Rec.Code <> 'R1-BASELINE-UPGRADE' then + exit; + TaskCompleted := true; // Participant triggered the operation +end; +``` + +### Validation Approach + +Two approaches work well for validating performance improvements: + +**Absolute threshold** — require the metric to fall below a fixed value (e.g., SQL count < 10). Simpler, deterministic, and works without a prior baseline. Better for workshop settings where participants may not have a baseline measurement recorded. + +**Relative comparison** — compare the latest measurement against the previous measurement for the same code. Requires the participant to run the operation twice. More flexible but fails if no baseline exists. + +Example of absolute threshold (simpler for workshops): + +```al +procedure IsValid(): Boolean +var + Measurement: Record "Performance Measurement"; +begin + Measurement.SetRange(Code, 'R4-ACTIVE'); + Measurement.SetCurrentKey(EntryNo); + if not Measurement.FindLast() then + exit(false); + exit(Measurement.SqlStatementsCount < 10); +end; +``` + +Example of relative comparison (if a baseline is always present): + +```al +local procedure ValidateCurrentMeasurement(NewMeasurement: Record "Performance Measurement"): Boolean +var + PreviousMeasurement: Record "Performance Measurement"; +begin + PreviousMeasurement.SetRange(Code, NewMeasurement.Code); + PreviousMeasurement.SetFilter(EntryNo, '<%1', NewMeasurement.EntryNo); + if not PreviousMeasurement.FindLast() then + exit(false); // No baseline yet + exit( + (NewMeasurement.SqlStatementsCount < PreviousMeasurement.SqlStatementsCount * 0.1) and + (NewMeasurement.DurationMs < PreviousMeasurement.DurationMs * 0.5) + ); +end; +``` + +### SingleInstance and Event Subscribers + +If the task codeunit uses event subscribers and stores state in codeunit-level variables, add `SingleInstance = true`: + +```al +codeunit 74250 "R4 Active Customer Task" implements iEscapeRoomTask +{ + SingleInstance = true; + // SingleInstance required: event subscriber sets TaskCompleted flag on this instance + +--- + +## Selecting the Best Anti-Pattern Example + +When choosing which performance anti-pattern to demonstrate in a room, rank candidates by: + +1. **Clarity of fix** — The correct solution is a small, targeted code change. Not a restructuring. +2. **Educational commonness** — This pattern genuinely appears in real production AL code. Developers recognise it. +3. **Measurability** — Fixing it produces a dramatic, unambiguous drop in SQL statement count and duration. + +### Canonical Examples by Room Type + +| Room Type | Anti-Pattern | Fix | Why It Works | +|---|---|---|---| +| N+1 Queries | `CalcFields` in a loop | `SetAutoCalcFields` before `FindSet` | Clear one-liner fix; N queries → 1; extremely common in production | +| Bulk Transfer | Record-by-record Copy loop | `DataTransfer` object | Small change; thousands of inserts → 1 operation | +| Bulk Update | Field update in a loop | `ModifyAll` | One call replaces the entire loop | +| Profiler | Any slow procedure | Identify via AL Profiler | Teaches the discovery process, not just the fix | + +**The best N+1 example:** `CalcFields` inside a `repeat...until` loop. It has a clear, elegant fix (`SetAutoCalcFields`), produces dramatic measurable improvement (N queries → 1), and is the most common N+1 pattern real developers write. + +--- + +## Companion App Starting State (Code-Based Venues Only) + +> This section applies only when a companion code app exists. + +The companion app ships with **deliberately un-optimized code** — that is the escape room starting state. The Solution HTML files describe exactly what code changes solve each problem. After designing all rooms: + +1. Write the Solution HTML files with the exact "good" code +2. Implement the "bad" code in the companion app (the opposite of the solution) +3. Verify that running the operations in a fresh environment produces the expected slowness + +**Do not add "good" code to the companion app during room design.** The version shipped to attendees must have the unoptimized code. Participants replace it when they fix each room. diff --git a/non production apps/EscapeRoomApp/Docs/Framework/Room-Content-HTML.md b/non production apps/EscapeRoomApp/Docs/Framework/Room-Content-HTML.md new file mode 100644 index 00000000..a22ce33a --- /dev/null +++ b/non production apps/EscapeRoomApp/Docs/Framework/Room-Content-HTML.md @@ -0,0 +1,402 @@ +# Room Content: HTML Description and Solution Files + +## Overview + +Every room has two HTML files that are loaded and displayed inside Business Central: + +- **Description file** — Presents the challenge. Tells participants WHAT to fix and WHERE to look, without revealing HOW. +- **Solution file** — Provides the complete, step-by-step answer after the solution delay expires. + +These files are loaded via the `GetRoomDescription()` and `GetRoomSolution()` methods on `iEscapeRoom`, or embedded as BLOB fields on the Escape Room record. They render in the **Rich Text Box Page** inside BC. + +> **BC HTML Viewer Constraints:** No JavaScript, no external CSS, no emoji or emoticons. Use inline styles only. Plain HTML with ``, ``, ``, ``, ``, `
`, `
`, and `
` with inline `style`. + +--- + +## Description Files + +### The One Rule That Overrides All Others + +**Descriptions present the MYSTERY. Solutions reveal the ANSWER.** + +Participants must figure out HOW to solve the challenge — that is the entire point. The description exists to make the problem tangible and send them to the right place. It must never save them the thinking. + +**The self-test:** "Could they copy-paste something from this description to solve the task?" If yes, the description is too revealing. Move it to the Solution file. + +--- + +### Tone and Language + +| Do | Do NOT | +|---|---| +| "Operations are slow" | "Record-by-record loops cause slowness" | +| "We test your ability to..." | "You will learn about..." | +| "Find a better approach" | "Use SetLoadFields to fix this" | +| "Something about how the data is loaded..." | "CalcFields fires a SQL query per record" | +| "Research what AL offers for bulk operations" | "Use DataTransfer for this scenario" | + +- **Tone = "we test your skills"**, not "you will learn" +- Describe the **symptom**, not the cause +- Describe the **problem**, not the technique that fixes it +- Never name the AL method, class, or pattern that IS the solution + +--- + +### Required Structure (7 sections, in this order) + +#### 1. HTML Shell + H1 + +```html + + + + +Room X: Short Title + + + +Room X: Short Title
+ + + + +``` + +Use a clear, descriptive title. Examples: `Room 2: Bulk Data Operations`, `Room 3: AL Profiler`. + +--- + +#### 2. TL;DR (H2) + +2-3 sentences maximum. The elevator pitch: what's broken, what you'll do, how you prove it's fixed. Participants who read nothing else should still know what the room is about. + +```html +TL;DR
+Fix the slow upgrade in OptimAL.PTE by finding better approaches for bulk table + transfers and field updates. Prove your optimization uses dramatically fewer database operations.
+``` + +--- + +#### 3. The Challenge (H2) + +ONE short paragraph (2-4 sentences) that sets the scene. If the problem was experienced in a previous room, reference it. End with a single-sentence "why this matters" statement woven into the paragraph. **Do NOT list learning objectives, future rooms, or skills here.** + +```html +The Challenge
+In Room 1, you experienced slow data operations taking far too long for 10,000 records. There + must be a better way to transfer and update large amounts of data. Slow migrations directly + impact upgrade windows and user downtime.
+``` + +**Rules:** +- ONE paragraph — never split into sub-sections +- "Why this matters" = ONE sentence, woven in, not a separate section or subsection +- Reference previous room context if applicable +- Do NOT write "This room teaches you...", "By the end of this room you will understand...", or any similar learning-objective framing + +--- + +#### 4. Your Mission (H2) + +The core of the description. Each mission item gets an **H3 with a descriptive title** — never task numbers ("Task 1:", "Task 2:"). Items may be grouped or split based on what makes logical sense, not one-to-one with validators. + +Each mission item contains (as flowing paragraphs, NOT separate sub-sections): + +- **What to do** — the goal, the problem to solve (described mysteriously) +- **Where to look** — exact object name, procedure name, page action (inline, as part of the prose) +- **How to trigger/reproduce** — numbered `` steps only when navigating BC UI +- **Hint** — a research nudge (use `Hint:`) +- **Validation** — how the system confirms success (use ``) + +Hints must be **VAGUE about technique** and **PRECISE about location**. When referencing a BC UI action, use the **exact label as it appears in Business Central, case-sensitive**. For non-toolbar actions, include the full navigation path: + +```html +
Hint: Navigate to Actions → Analytics & Reporting and run +the Customer Order Analytics action. Something about how it retrieves data might +surprise you.
+``` + +```html +Your Mission
+ +Optimize Table Transfer
+The upgrade code in Codeunit 74391 "Upgrade OptimAL PTE" copies records from + Performance Test Customer to a Customer Archive table in a way that is extremely slow. Find a + better approach that can transfer all records much more efficiently.
+Hint: AL must have better tools for data migration scenarios. Research what is + available.
+Republish OptimAL.PTE (bump version, F5) and check the Performance Measurements page for + the upgrade entry. The system validates that total database operations drop below 100.
+``` + +**Rules for Your Mission:** +- H3 headings are **descriptive names** — never "Task 1:", "Task 2:", "Step 1:", etc. +- Never build separate "Where to Look", "Validation", "Key Concepts", or "Research Topics" sections — all of that content is inline inside the mission items +- Each item is self-contained: goal + location + hint + validation in one place +- Keep numbered `` steps ONLY for BC UI navigation (how to reproduce the problem) + +--- + +#### 5. Warning / Gotcha Box (OPTIONAL) + +Only for critical setup issues that would waste significant time if missed. Maximum one per room. + +```html +
++``` + +--- + +#### 6. Update Status Reminder + +Every description must end with this (just before "What's Next", or at the very end for the final room): + +```html +Important: Make sure your launch.json includes +
+"dependencyPublishingOption": "Ignore"before pressing F5.Update Status: Not all steps are captured automatically. Hit the + Update Status button on the room page to check if you have completed + steps that were not registered yet.
+``` + +--- + +#### 7. What's Next (H2) + +ONE sentence about the next room only. Omit for the final room in the venue. + +```html +What's Next
+Room 3: Learn to use profiling tools to discover performance bottlenecks yourself.
+``` + +--- + +### Sections FORBIDDEN in Descriptions + +These sections create redundancy or reveal the answer. Their content either belongs inline in mission items, belongs only in Solution files, or should be removed entirely: + +| Forbidden Section | Why | +|---|---| +| "How Task Validation Works" | Too much explanation — breaks mystery | +| "Business Impact" | Redundant filler | +| "Performance Results" / "Performance Baseline" table | Belongs in Solution files only | +| "Profiler Comparisons" | Belongs in Solution files only | +| "Skills Tested / Learning Objectives" | The mission items ARE the skills being tested | +| "Key Concepts / Research Topics" | Fold hints and research nudges into mission items | +| "Tips for Success" | Fold into mission items | +| "Real-World Application" | Filler — remove entirely | +| "Where to Look" as a standalone section | Put inline in mission items | +| "Validation" as a standalone section | Put inline in mission items | +| "Why This Room Matters" as a standalone section | One sentence in The Challenge paragraph | +| "What's Next" listing all remaining rooms | Only mention the next room, one sentence | +| Task numbers ("Task 1:", "Task 2:") in any heading | Use descriptive H3 names instead | +| "You will learn" / "This will teach you" language | Tone is "we test your" — not educational | +| Emoticons / emojis | BC HTML viewer cannot display them | +| Naming the solution technique | Keeps challenge mysterious | + +--- + +## Solution Files + +### Purpose + +Solution files provide **exact, copy-pasteable, step-by-step instructions** for completing all tasks. They teach participants exactly how to solve the challenge while explaining WHY each step matters. + +**Solution = EXACT HOW + WHY.** No ambiguity. No "try something like this." Every code block must be complete and working. + +--- + +### Required Structure + +#### 1. Main Heading + +```html +Solution: Room X - Task Title
+``` + +#### 2. Introduction (optional) + +Brief paragraph explaining the overall approach. + +#### 3. Task Sections + +Each task gets its own section: + +```html +Optimizing Table Transfer
+Replace the slow record-by-record loop with a bulk operation.
+ +Step 1: Locate the Code
++
+ +- Open Codeunit 74390 "Upgrade OptimAL PTE"
+- Find the
+TransferDataRecordByRecord()procedureStep 2: Implement the Solution
++local procedure TransferDataBulk() +var + DataTransfer: DataTransfer; +begin + DataTransfer.SetTables(Database::"Performance Test Customer", Database::"Customer Archive"); + DataTransfer.CopyFields(); + DataTransfer.CopyRows(); +end; ++ +Why This Matters:
++
+``` + +**Section heading rules:** +- Use descriptive H2 names — never "Task 1:", "Task 2:", etc. +- Sub-steps use H3; explanations use H4 +- Show "Why This Matters" after each major code block + +--- + +#### 4. Forbidden Content in Solutions + +| Forbidden | Why | +|---|---| +| "Performance Results" comparison tables | Removed in practice — numbers vary by environment | +| "Profiler Comparisons" sections | Removed in practice — misleading without live data | +| Task number references in headings | Use descriptive names | + +--- + +#### 5. Code Examples + +- **Always complete and working** — participants should be able to copy-paste +- Use `- Uses a set-based operation instead of a loop
+- Reduces 10,000 queries to 1
+- Dramatically improves performance at scale
+` for code blocks +- Include full procedure signatures, not fragments +- Include inline code comments for complex logic +- Show "before" (the problem) and "after" (the solution) where helpful + +--- + +#### 6. Alternative Approaches + +When multiple approaches are valid, show the recommended one first: + +```html +Option 1: Bulk Transfer with DataTransfer (Recommended)
+...+ +Option 2: Manual Loop with Commit
+...+``` + +--- + +#### 7. Warning Boxes in Solutions + +```html +++``` + +--- + +#### 8. Verification Section + +```html +Security Warning: Hardcoding API keys is NOT a + production pattern. This is a training shortcut only.
+Verification
+After completing all tasks:
++
+- The Performance Measurements page shows the upgrade entry with fewer than 100 DB operations
+- Both duration and SQL statement count have dropped significantly
+Important: Don't forget to click the Update Status button!
+``` + +--- + +#### 9. What You've Accomplished + +```html +What You've Accomplished
+By completing this room, you have:
++
+- Replaced record-by-record loops with set-based operations
+- Proven the improvement using built-in measurement tooling
+- Learned to recognise this class of performance problem in real code
+Ready for Room 3: Now you will use the AL Profiler to discover bottlenecks yourself.
+``` + +--- + +## Tables + +For comparisons, metrics, or structured data in solutions: + +```html ++
+``` + +--- + +## File Naming Convention + +- Description: `Room[N][Topic]Description.html` — e.g., `Room2DataTransferDescription.html` +- Solution: `Room[N][Topic]Solution.html` — e.g., `Room2DataTransferSolution.html` + +--- + +## Quality Checklists + +### Description File Checklist + +- [ ] Complete HTML structure with DOCTYPE +- [ ] Clear H1 title with room number +- [ ] TL;DR (2-3 sentences max) +- [ ] The Challenge (ONE paragraph, "why this matters" woven in as one sentence) +- [ ] Your Mission with descriptive H3 headings (no task numbers) +- [ ] Each mission item is self-contained: goal + location + hint + validation inline +- [ ] Hints use the exact BC UI action label (case-sensitive); non-toolbar actions include full nav path +- [ ] No standalone Skills Tested, Key Concepts, Research Topics, Validation, or Where to Look sections +- [ ] No Performance Results or Baseline tables +- [ ] Update Status reminder present (just before What's Next) +- [ ] What's Next mentions only the next room (one sentence) +- [ ] Problem described ONCE — no repetition across sections +- [ ] Tone is "we test your skills" — no "you will learn" language +- [ ] MYSTERIOUS — no method names, no code solutions, no technical terms that reveal the answer +- [ ] No emoticons or emojis + +### Solution File Checklist + +- [ ] Clear H1 with "Solution: Room X - Title" +- [ ] Step-by-step instructions for each task +- [ ] Complete, copy-pasteable code examples +- [ ] "Why This Matters" explanations after each code block +- [ ] Verification checklist +- [ ] "What You've Accomplished" summary +- [ ] Tutorial-style, explanatory tone +- [ ] Specific object names, procedure names, and exact UI navigation +- [ ] No Performance Results comparison tables +- [ ] No Profiler Comparisons sections +- [ ] No task number references in headings (use descriptive names) +- [ ] No emoticons or emojis diff --git a/non production apps/EscapeRoomApp/Docs/README.md b/non production apps/EscapeRoomApp/Docs/README.md index f5531ef9..17adce76 100644 --- a/non production apps/EscapeRoomApp/Docs/README.md +++ b/non production apps/EscapeRoomApp/Docs/README.md @@ -25,6 +25,8 @@ Core framework architecture and patterns for developers extending the system: - [Architecture Overview](Framework/Architecture.md) - Core components and design patterns - [Creating New Rooms](Framework/Creating-Rooms.md) - Step-by-step guide for building rooms - [Task Validation System](Framework/Task-Validation.md) - How task validation and interfaces work +- [Room Content (HTML)](Framework/Room-Content-HTML.md) - Writing Description and Solution HTML files +- [Performance Room Design](Framework/Performance-Room-Design.md) - Patterns for performance-themed rooms - [Telemetry Integration](Framework/Telemetry-Integration.md) - Scoring, tracking, and Application Insights ### 🎯 [Facilitator Documentation](Facilitator/) diff --git a/non production apps/EscapeRoomApp/ReadMe.md b/non production apps/EscapeRoomApp/ReadMe.md index 8ecab417..2afa9b6f 100644 --- a/non production apps/EscapeRoomApp/ReadMe.md +++ b/non production apps/EscapeRoomApp/ReadMe.md @@ -25,6 +25,8 @@ Core framework architecture and patterns for developers extending the system: - [Architecture Overview](Docs/Framework/Architecture.md) - Core components and design patterns - [Creating New Rooms](Docs/Framework/Creating-Rooms.md) - Step-by-step guide for building rooms - [Task Validation System](Docs/Framework/Task-Validation.md) - How task validation and interfaces work +- [Room Content (HTML)](Docs/Framework/Room-Content-HTML.md) - Writing Description and Solution HTML files +- [Performance Room Design](Docs/Framework/Performance-Room-Design.md) - Patterns for performance-themed rooms - [Telemetry Integration](Docs/Framework/Telemetry-Integration.md) - Scoring, tracking, and Application Insights ### 🎯 [Facilitator Documentation](Docs/Facilitator/) @@ -58,7 +60,9 @@ Complete chronological history of all changes to the framework across all develo 1. Understand the [Architecture Overview](Docs/Framework/Architecture.md) 2. Follow [Creating New Rooms](Docs/Framework/Creating-Rooms.md) guide 3. Implement [Task Validation System](Docs/Framework/Task-Validation.md) for challenges -4. Integrate [Telemetry](Docs/Framework/Telemetry-Integration.md) for scoring +4. Write HTML content following [Room Content (HTML)](Docs/Framework/Room-Content-HTML.md) +5. For performance rooms, see [Performance Room Design](Docs/Framework/Performance-Room-Design.md) +6. Integrate [Telemetry](Docs/Framework/Telemetry-Integration.md) for scoring ### For Framework Contributors 1. Review [Developer Guide](Docs/Dev/README.md) @@ -82,4 +86,4 @@ Complete chronological history of all changes to the framework across all develo For questions, issues, or contributions related to the framework, see the project repository. -**Last Updated:** January 7, 2026 +**Last Updated:** February 22, 2026 diff --git a/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Page.al b/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Page.al index faffd24a..4f1b3f40 100644 --- a/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Page.al +++ b/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Page.al @@ -136,6 +136,16 @@ page 73922 "Escape Room" CurrPage.Update(false); end; } + action(ResetRoom) + { + Caption = 'Reset Room'; + Image = Undo; + trigger OnAction() + begin + Rec.ResetRoom(); + CurrPage.Update(false); + end; + } } area(Promoted) { @@ -151,6 +161,10 @@ page 73922 "Escape Room" { Visible = true; } + actionref(ResetRoomRef; ResetRoom) + { + Visible = true; + } } } diff --git a/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Table.al b/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Table.al index 912c9946..befb5d9b 100644 --- a/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Table.al +++ b/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Table.al @@ -90,6 +90,11 @@ table 73920 "Escape Room" var Task: Record "Escape Room Task"; begin + if Rec.Status = Rec.Status::Completed then begin + OpenNextRoom(); + exit; + end; + if Rec.Status <> Rec.Status::InProgress then exit; Task.Setrange("Venue Id", Rec."Venue Id"); @@ -261,5 +266,33 @@ table 73920 "Escape Room" exit(true); end; + procedure ResetRoom() + var + Task: Record "Escape Room Task"; + EscapeRoomMgt: Codeunit "Escape Room"; + CurrentRoom: Interface iEscapeRoom; + ConfirmManagement: Codeunit "Confirm Management"; + ResetRoomQst: Label 'Are you sure you want to reset this room? All task progress and hints will be permanently lost and there is no way back.'; + begin + if Rec.Status = Rec.Status::Locked then exit; + + if not ConfirmManagement.GetResponseOrDefault(ResetRoomQst, false) then + exit; + + Task.SetRange("Venue Id", Rec."Venue Id"); + Task.SetRange("Room Name", Rec.Name); + Task.DeleteAll(); + + CurrentRoom := Rec.Room; + EscapeRoomMgt.RefreshTasks(CurrentRoom); + + Rec.Status := Rec.Status::InProgress; + Rec."Start DateTime" := CurrentDateTime(); + Rec."Stop DateTime" := 0DT; + Rec."Solution DateTime" := 0DT; + Rec.Modify(); + Commit(); + end; + } \ No newline at end of file+ +Operation +Before +After +Improvement ++ +Transfer 10,000 records +~120 seconds +<5 seconds +24x faster +