From f5de1babef6b3f7ce2166be3629fb63181843607 Mon Sep 17 00:00:00 2001 From: waldo1001 Date: Thu, 19 Feb 2026 15:55:16 +0100 Subject: [PATCH 1/7] feat: Add Reset Room button to Escape Room page --- .../Src/2.Room/EscapeRoom.Page.al | 14 ++++++++++ .../Src/2.Room/EscapeRoom.Table.al | 28 +++++++++++++++++++ 2 files changed, 42 insertions(+) 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..4ef5257b 100644 --- a/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Table.al +++ b/non production apps/EscapeRoomApp/Src/2.Room/EscapeRoom.Table.al @@ -261,5 +261,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 From 5b4751926a7d32400d82a4f40b2940b42ba98f34 Mon Sep 17 00:00:00 2001 From: waldo1001 Date: Sat, 21 Feb 2026 12:01:57 +0100 Subject: [PATCH 2/7] feat: Add condition to open next room if status is completed in ResetRoom procedure --- .../EscapeRoomApp/Src/2.Room/EscapeRoom.Table.al | 5 +++++ 1 file changed, 5 insertions(+) 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 4ef5257b..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"); From f63dc61569a4f5d99e15c0ac8de5e91a6d39d3d3 Mon Sep 17 00:00:00 2001 From: waldo1001 Date: Sun, 22 Feb 2026 09:45:28 +0100 Subject: [PATCH 3/7] Add performance-themed room design and HTML content guidelines - Introduced a new document for Performance Room Design detailing patterns and rules for diagnosing performance issues in escape rooms. - Added a comprehensive guide for writing Description and Solution HTML files for each room, emphasizing the importance of maintaining mystery and clarity. - Updated README files to include links to the new documentation on Room Content (HTML) and Performance Room Design. --- .../.github/copilot-instructions.md | 473 ++++++++++++++++++ .../Docs/Framework/Performance-Room-Design.md | 274 ++++++++++ .../Docs/Framework/Room-Content-HTML.md | 402 +++++++++++++++ .../EscapeRoomApp/Docs/README.md | 2 + non production apps/EscapeRoomApp/ReadMe.md | 8 +- 5 files changed, 1157 insertions(+), 2 deletions(-) create mode 100644 non production apps/EscapeRoomApp/.github/copilot-instructions.md create mode 100644 non production apps/EscapeRoomApp/Docs/Framework/Performance-Room-Design.md create mode 100644 non production apps/EscapeRoomApp/Docs/Framework/Room-Content-HTML.md 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 `
    ` for BC UI only), `Hint:`, and `` validation note. +5. **Warning box** — Optional. Only for critical setup gotchas. +6. **Update Status reminder** — Required, just before What's Next. +7. **What's Next** — One sentence about the next room only. Omit for the final room. + +**Hints must be VAGUE about technique and PRECISE about location.** Use the exact BC action label as it appears in the UI. For non-toolbar actions, include the full navigation path: `Actions → Analytics & Reporting → Customer Order Analytics`. + +### Description Tone + +| 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" | +| "Research what AL offers" | "Use DataTransfer for this scenario" | + +### Sections Forbidden in Descriptions + +`Skills Tested / Learning Objectives` — `Key Concepts / Research Topics` — `Business Impact` — `Performance Results / Baseline tables` — `Profiler Comparisons` — `How Task Validation Works` — `"You will learn"` language — Task numbers in headings — Emoticons / emojis — Standalone `Where to Look` / `Validation` / `What's Next listing all rooms` sections. + +### Solution File Structure + +1. **H1:** `Solution: Room X - Title` +2. **Introduction** (optional brief paragraph) +3. **Task sections** with descriptive H2 names (not "Task 1:"), step-by-step `
      ` instructions, complete `
      ` 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 +
              +

              Important: Make sure your launch.json includes + "dependencyPublishingOption": "Ignore" before pressing F5.

              +
              +``` + +--- + +#### 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 +

              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

              +
                +
              1. Open Codeunit 74390 "Upgrade OptimAL PTE"
              2. +
              3. Find the TransferDataRecordByRecord() procedure
              4. +
              + +

              Step 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:

              +
                +
              • Uses a set-based operation instead of a loop
              • +
              • Reduces 10,000 queries to 1
              • +
              • Dramatically improves performance at scale
              • +
              +``` + +**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 `
              ` 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 +
              +

              Security Warning: Hardcoding API keys is NOT a + production pattern. This is a training shortcut only.

              +
              +``` + +--- + +#### 8. Verification Section + +```html +

              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 +
          + + + + + + + + + + + + +
          OperationBeforeAfterImprovement
          Transfer 10,000 records~120 seconds<5 seconds24x faster
          +``` + +--- + +## 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 From bdf0ab5249d702165c3d92058bcf7409c344600d Mon Sep 17 00:00:00 2001 From: waldo1001 Date: Fri, 13 Mar 2026 17:24:32 +0100 Subject: [PATCH 4/7] fix: Update filter condition for completed room status in CloseVenueIfCompleted procedure --- .../EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al b/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al index 09941fea..b59600eb 100644 --- a/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al +++ b/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al @@ -124,7 +124,7 @@ table 73926 "Escape Room Venue" Room: Record "Escape Room"; begin Room.Setrange("Venue Id", Rec.Id); - Room.SetRange(Status, Room.Status::Completed); + Room.SetFilter(Status, '<>%1', Room.Status::Completed); if not Room.IsEmpty then exit; Rec.Stop(); From 7467e56897774271b54fb3608e576098ec103ca4 Mon Sep 17 00:00:00 2001 From: waldo1001 Date: Fri, 13 Mar 2026 17:55:48 +0100 Subject: [PATCH 5/7] chore: Update application version and Application Insights connection string in app.json --- non production apps/EscapeRoomApp/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/non production apps/EscapeRoomApp/app.json b/non production apps/EscapeRoomApp/app.json index e95661e6..655c4a57 100644 --- a/non production apps/EscapeRoomApp/app.json +++ b/non production apps/EscapeRoomApp/app.json @@ -2,7 +2,7 @@ "id": "f03c0f0c-d887-4279-b226-dea59737ecf8", "name": "BCTalent.EscapeRoom", "publisher": "waldo & AJ", - "version": "1.3.10026.0", + "version": "1.3.10026.2", "brief": "", "description": "", "privacyStatement": "", @@ -29,5 +29,5 @@ "features": [ "NoImplicitWith" ], - "applicationInsightsConnectionString": "" + "applicationInsightsConnectionString": "InstrumentationKey=bc3a5124-6f0a-4961-b755-d3fde0f2a563;IngestionEndpoint=https://westeurope-5.in.applicationinsights.azure.com/;LiveEndpoint=https://westeurope.livediagnostics.monitor.azure.com/;ApplicationId=b4dabdd7-07f8-4166-a6cc-6693e95921fe" } \ No newline at end of file From 2a3deb6c475c2a28e914bd9031db517d6cfbf2f0 Mon Sep 17 00:00:00 2001 From: waldo Date: Sat, 21 Mar 2026 07:05:31 +0100 Subject: [PATCH 6/7] refactor: Update folder structure in Copilot instructions for clarity and organization --- .../.github/copilot-instructions.md | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/non production apps/EscapeRoomApp/.github/copilot-instructions.md b/non production apps/EscapeRoomApp/.github/copilot-instructions.md index 4a47d818..1fbc2359 100644 --- a/non production apps/EscapeRoomApp/.github/copilot-instructions.md +++ b/non production apps/EscapeRoomApp/.github/copilot-instructions.md @@ -36,23 +36,32 @@ Always use the **vjeko-al-objid** tool (`getNextObjectId`) before creating any A ``` 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) +├── Src/ +│ ├── MyVenue.Codeunit.al (implements iEscapeRoomVenue) +│ ├── EscapeRoomVenueExt.EnumExt.al +│ ├── EscapeRoomExt.EnumExt.al +│ ├── EscapeRoomTaskExt.EnumExt.al +│ ├── InstallVenue.Codeunit.al (registers venue via UpdateVenue) +│ └── Rooms/ +│ ├── Room1MyRoom/ +│ │ ├── Room1MyRoom.Codeunit.al (implements iEscapeRoom) +│ │ └── Tasks/ +│ │ └── R1T1MyTask.Codeunit.al (implements iEscapeRoomTask) +│ └── Room2MyRoom/ +│ ├── Room2MyRoom.Codeunit.al +│ └── Tasks/ +│ └── R2T1MyTask.Codeunit.al +└── Resources/ + ├── Room1MyRoomDescription.html + ├── Room1MyRoomSolution.html + └── RoomCompletedImage.png ``` +Key conventions: +- Enum extensions, venue codeunit, and install codeunit live flat at `Src/` root — no `Venue/` or `Install/` subfolders. +- Each room has its own subfolder under `Src/Rooms/`. +- Each room subfolder contains the room codeunit plus a `Tasks/` subfolder with all task codeunits for that room. + --- ## Part 2: Extending the Enums From 79825758ed51eb28e1a898204b381d98f4e9c805 Mon Sep 17 00:00:00 2001 From: waldo Date: Sun, 22 Mar 2026 15:25:39 +0100 Subject: [PATCH 7/7] feat: Add ResetVenue procedure and associated UI action to reset venue state --- .../dashboard-BCTalent.EscapeRooms.json | 36 ++++++++++-------- .../Src/1.Venue/EscapeRoomVenue.Table.al | 37 +++++++++++++++++++ .../Src/1.Venue/EscapeRoomVenueCard.Page.al | 20 ++++++++++ .../Src/EscapeRoomNotifications.Codeunit.al | 2 +- non production apps/EscapeRoomApp/app.json | 2 +- 5 files changed, 79 insertions(+), 18 deletions(-) diff --git a/non production apps/EscapeRoomApp/Leaderboard/dashboard-BCTalent.EscapeRooms.json b/non production apps/EscapeRoomApp/Leaderboard/dashboard-BCTalent.EscapeRooms.json index ef56f421..b5c8abe4 100644 --- a/non production apps/EscapeRoomApp/Leaderboard/dashboard-BCTalent.EscapeRooms.json +++ b/non production apps/EscapeRoomApp/Leaderboard/dashboard-BCTalent.EscapeRooms.json @@ -1,9 +1,13 @@ { - "$schema": "https://dataexplorer.azure.com/static/d/schema/63/dashboard.json", + "$schema": "https://dataexplorer.azure.com/static/d/schema/69/dashboard.json", "id": "272c2198-052c-4de4-a986-7d7c6046127d", - "eTag": "bdf41fb3-1203-4d62-96ba-d05e0945a3b2", + "eTag": "6a3d4f81-d4c4-4568-abb5-fca48cc32627", "title": "BCTalent.EscapeRooms", - "schema_version": 63, + "schema_version": 69, + "autoRefresh": { + "enabled": true, + "minInterval": "1m" + }, "tiles": [ { "id": "6ff8eab3-009f-482c-a75c-8cd9f170ecbc", @@ -334,7 +338,7 @@ "x": 0, "y": 0, "width": 21, - "height": 9 + "height": 13 }, "queryRef": { "kind": "query", @@ -377,7 +381,7 @@ "pageId": "871aa6d5-30ed-40bc-9acf-0b8b4f611186", "layout": { "x": 0, - "y": 22, + "y": 28, "width": 21, "height": 7 }, @@ -403,9 +407,9 @@ "pageId": "871aa6d5-30ed-40bc-9acf-0b8b4f611186", "layout": { "x": 0, - "y": 16, + "y": 20, "width": 21, - "height": 6 + "height": 8 }, "queryRef": { "kind": "query", @@ -429,7 +433,7 @@ "pageId": "871aa6d5-30ed-40bc-9acf-0b8b4f611186", "layout": { "x": 0, - "y": 9, + "y": 13, "width": 21, "height": 7 }, @@ -527,12 +531,12 @@ ], "dataSources": [ { - "id": "803c0786-1793-49b9-8c52-5d977ac0c74c", "kind": "manual-kusto", - "scopeId": "kusto", + "id": "803c0786-1793-49b9-8c52-5d977ac0c74c", "name": "BCTalent", "clusterUri": "https://ade.applicationinsights.io/subscriptions/313d31ed-d6ca-42ce-b2a3-676dfff76144", - "database": "BCTelemetry" + "database": "BCTelemetry", + "scopeId": "kusto" } ], "pages": [ @@ -680,7 +684,7 @@ "kind": "inline", "dataSourceId": "803c0786-1793-49b9-8c52-5d977ac0c74c" }, - "text": "EscapeRoomData\n| where isnotempty(Attendee) and isnotempty(Score) and isnotempty( Partner)\n| extend AttendeePartner = strcat(Attendee,\" (\",Partner,\")\")\n| summarize \n TotalScore = sum(Score),\n TasksCompleted = countif(eventId == \"ALEscapeRoomTaskFinished\"),\n RoomsCompleted = countif(eventId == \"ALEscapeRoomCompleted\"),\n VenuesCompleted = countif(eventId == \"ALEscapeRoomVenueCompleted\"),\n HintsUsed = countif(eventId == \"ALEscapeRoomHintRequested\"),\n SolutionsUsed = countif(eventId == \"ALEscapeRoomSolutionRequested\"),\n LastActivity = max(taskStopDateTime)\n by AttendeePartner, userId, Partner, environmentName\n| order by TotalScore desc\n| extend Rank = row_number()\n// | where Rank <= 5\n| project \n Rank,\n AttendeePartner,\n Partner,\n TotalScore,\n TasksCompleted,\n RoomsCompleted,\n VenuesCompleted,\n HintsUsed,\n SolutionsUsed,\n LastActivity\n| order by TotalScore desc", + "text": "EscapeRoomData\n| where isnotempty(Attendee) and isnotempty(Score) and isnotempty( Partner)\n| extend AttendeePartner = strcat(Attendee,\" (\",Partner,\")\")\n| summarize \n TotalScore = sum(Score),\n TasksCompleted = countif(eventId == \"ALEscapeRoomTaskFinished\"),\n RoomsCompleted = countif(eventId == \"ALEscapeRoomCompleted\"),\n VenuesCompleted = countif(eventId == \"ALEscapeRoomVenueCompleted\"),\n HintsUsed = countif(eventId == \"ALEscapeRoomHintRequested\"),\n SolutionsUsed = countif(eventId == \"ALEscapeRoomSolutionRequested\"),\n LastActivity = max(taskStopDateTime)\n by AttendeePartner, userId, Partner, environmentName\n| order by TotalScore desc\n| extend Rank = row_number()\n| where Rank <= 10\n| project \n Rank,\n AttendeePartner,\n Partner,\n TotalScore,\n TasksCompleted,\n RoomsCompleted,\n VenuesCompleted,\n HintsUsed,\n SolutionsUsed,\n LastActivity\n| order by TotalScore desc", "id": "8a6d557a-34b9-4cad-8f4b-82a33fb0b883", "usedVariables": [ "EscapeRoomData" @@ -691,7 +695,7 @@ "kind": "inline", "dataSourceId": "803c0786-1793-49b9-8c52-5d977ac0c74c" }, - "text": "EscapeRoomData\n| where eventId == \"ALEscapeRoomVenueCompleted\"\n| where isnotempty(Attendee) and isnotempty(companyName) and VenueCompletionTimeMinutes > 0\n| extend \n CompletionTimeMinutes = round(VenueCompletionTimeMinutes, 2),\n CompletionTimeDisplay = strcat(tostring(floor(VenueCompletionTimeMinutes / 60, 1)), \"h \", tostring(round(VenueCompletionTimeMinutes, 2) % 60), \"m\"),\n CompletionDate = taskStopDateTime,\n FinalScore = Score\n| order by CompletionTimeMinutes asc\n| extend Rank = row_number()\n| where Rank <= 5\n| project \n Rank,\n Attendee,\n companyName,\n Partner,\n environmentName,\n venueName,\n CompletionTimeDisplay,\n CompletionTimeMinutes,\n FinalScore,\n CompletionDate\n| order by CompletionTimeMinutes asc", + "text": "EscapeRoomData\n| where eventId == \"ALEscapeRoomVenueCompleted\"\n| where isnotempty(Attendee) and isnotempty(companyName) and VenueCompletionTimeMinutes > 0\n| summarize arg_max(VenueCompletionTimeMinutes, *) by Attendee\n| extend \n CompletionTimeMinutes = round(VenueCompletionTimeMinutes, 2),\n CompletionTimeDisplay = strcat(tostring(floor(VenueCompletionTimeMinutes / 60, 1)), \"h \", tostring(round(VenueCompletionTimeMinutes, 2) % 60), \"m\"),\n CompletionDate = taskStopDateTime,\n FinalScore = Score\n| order by CompletionTimeMinutes asc\n| extend Rank = row_number()\n| where Rank <= 5\n| project \n Rank,\n Attendee,\n companyName,\n Partner,\n environmentName,\n venueName,\n CompletionTimeDisplay,\n CompletionTimeMinutes,\n FinalScore,\n CompletionDate\n| order by CompletionTimeMinutes asc", "id": "4d1f4a51-5747-4134-a214-db990b774afd", "usedVariables": [ "EscapeRoomData" @@ -702,7 +706,7 @@ "kind": "inline", "dataSourceId": "803c0786-1793-49b9-8c52-5d977ac0c74c" }, - "text": "EscapeRoomData\n| where isnotempty(Attendee) and isnotempty(Score) and isnotempty(Partner)\n| summarize \n TaskPoints = sumif(Score, eventId == \"ALEscapeRoomTaskFinished\"),\n RoomBonusPoints = sumif(Score, eventId == \"ALEscapeRoomCompleted\"),\n VenueBonusPoints = sumif(Score, eventId == \"ALEscapeRoomVenueCompleted\"),\n HintPenalty = sumif(Score, eventId == \"ALEscapeRoomHintRequested\"),\n SolutionPenalty = sumif(Score, eventId == \"ALEscapeRoomSolutionRequested\"),\n TotalScore = sum(Score),\n TasksCompleted = countif(eventId == \"ALEscapeRoomTaskFinished\"),\n RoomsCompleted = countif(eventId == \"ALEscapeRoomCompleted\"),\n VenuesCompleted = countif(eventId == \"ALEscapeRoomVenueCompleted\"),\n HintsUsed = countif(eventId == \"ALEscapeRoomHintRequested\"),\n SolutionsUsed = countif(eventId == \"ALEscapeRoomSolutionRequested\"),\n FastestVenueCompletion = min(VenueCompletionTimeMinutes)\n by Attendee, Partner, environmentName\n| extend \n ScoreEfficiency = iif((TasksCompleted + HintsUsed + SolutionsUsed) > 0, round((todouble(TotalScore) / (TasksCompleted + HintsUsed + SolutionsUsed)) * 100) / 100, todouble(0)),\n CompletionTimeMinutes = iff(isnull(FastestVenueCompletion), real(null), round(FastestVenueCompletion, 2))\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last\n| extend Rank = row_number()\n| where Rank <= 10\n| project \n Rank,\n Attendee,\n Partner,\n TotalScore,\n TaskPoints,\n RoomBonusPoints,\n VenueBonusPoints,\n HintPenalty,\n SolutionPenalty,\n ScoreEfficiency,\n CompletionTimeMinutes,\n TasksCompleted,\n HintsUsed,\n SolutionsUsed\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last\n", + "text": "EscapeRoomData\n| where isnotempty(Attendee) and isnotempty(Score) and isnotempty(Partner)\n| summarize \n TaskPoints = sumif(Score, eventId == \"ALEscapeRoomTaskFinished\"),\n RoomBonusPoints = sumif(Score, eventId == \"ALEscapeRoomCompleted\"),\n VenueBonusPoints = sumif(Score, eventId == \"ALEscapeRoomVenueCompleted\"),\n HintPenalty = sumif(Score, eventId == \"ALEscapeRoomHintRequested\"),\n SolutionPenalty = sumif(Score, eventId == \"ALEscapeRoomSolutionRequested\"),\n TotalScore = sum(Score),\n TasksCompleted = countif(eventId == \"ALEscapeRoomTaskFinished\"),\n RoomsCompleted = countif(eventId == \"ALEscapeRoomCompleted\"),\n VenuesCompleted = countif(eventId == \"ALEscapeRoomVenueCompleted\"),\n HintsUsed = countif(eventId == \"ALEscapeRoomHintRequested\"),\n SolutionsUsed = countif(eventId == \"ALEscapeRoomSolutionRequested\"),\n FastestVenueCompletion = min(VenueCompletionTimeMinutes)\n by Attendee, Partner, environmentName\n| extend \n ScoreEfficiency = iif((TasksCompleted + HintsUsed + SolutionsUsed) > 0, round((todouble(TotalScore) / (TasksCompleted + HintsUsed + SolutionsUsed)) * 100) / 100, todouble(0)),\n CompletionTimeMinutes = iff(isnull(FastestVenueCompletion), real(null), round(FastestVenueCompletion, 2))\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last\n| extend Rank = row_number()\n| where Rank <= 10\n| project \n Rank,\n Attendee,\n Partner,\n TotalScore,\n TaskPoints,\n RoomBonusPoints,\n VenueBonusPoints,\n HintPenalty,\n SolutionPenalty,\n ScoreEfficiency,\n CompletionTimeMinutes,\n TasksCompleted,\n HintsUsed,\n SolutionsUsed\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last\n", "id": "e56b8546-96f6-4a9a-b0d8-6fdf90363676", "usedVariables": [ "EscapeRoomData" @@ -713,7 +717,7 @@ "kind": "inline", "dataSourceId": "803c0786-1793-49b9-8c52-5d977ac0c74c" }, - "text": "EscapeRoomData\n| where isnotempty(Attendee) and isnotempty(Score) and isnotempty(Partner)\n| summarize \n TotalScore = sum(Score),\n TasksCompleted = countif(eventId == \"ALEscapeRoomTaskFinished\"),\n RoomsCompleted = countif(eventId == \"ALEscapeRoomCompleted\"),\n VenuesCompleted = countif(eventId == \"ALEscapeRoomVenueCompleted\"),\n HintsUsed = countif(eventId == \"ALEscapeRoomHintRequested\"),\n SolutionsUsed = countif(eventId == \"ALEscapeRoomSolutionRequested\"),\n LastActivity = max(taskStopDateTime),\n FastestVenueCompletion = min(VenueCompletionTimeMinutes)\n by Attendee, userId, Partner, environmentName\n| extend CompletionTimeMinutes = iff(isnull(FastestVenueCompletion), real(null), round(FastestVenueCompletion, 2))\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last\n| extend Rank = row_number()\n| where Rank <= 5\n| project \n Rank,\n Attendee,\n Partner,\n TotalScore,\n TasksCompleted,\n RoomsCompleted,\n VenuesCompleted,\n HintsUsed,\n SolutionsUsed,\n CompletionTimeMinutes,\n LastActivity\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last", + "text": "EscapeRoomData\n| where isnotempty(Attendee) and isnotempty(Score) and isnotempty(Partner)\n| summarize \n TotalScore = sum(Score),\n TasksCompleted = countif(eventId == \"ALEscapeRoomTaskFinished\"),\n RoomsCompleted = countif(eventId == \"ALEscapeRoomCompleted\"),\n VenuesCompleted = countif(eventId == \"ALEscapeRoomVenueCompleted\"),\n HintsUsed = countif(eventId == \"ALEscapeRoomHintRequested\"),\n SolutionsUsed = countif(eventId == \"ALEscapeRoomSolutionRequested\"),\n LastActivity = max(taskStopDateTime),\n FastestVenueCompletion = min(VenueCompletionTimeMinutes)\n by Attendee, userId, Partner, environmentName\n| extend CompletionTimeMinutes = iff(isnull(FastestVenueCompletion), real(null), round(FastestVenueCompletion, 2))\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last\n| extend Rank = row_number()\n| where Rank <= 100\n| project \n Rank,\n Attendee,\n Partner,\n TotalScore,\n TasksCompleted,\n RoomsCompleted,\n VenuesCompleted,\n HintsUsed,\n SolutionsUsed,\n CompletionTimeMinutes,\n LastActivity\n| order by TotalScore desc, CompletionTimeMinutes asc nulls last", "id": "bc7c2e02-ff93-443d-93c0-a1e0db76e80c", "usedVariables": [ "EscapeRoomData" @@ -725,7 +729,7 @@ "kind": "inline", "dataSourceId": "803c0786-1793-49b9-8c52-5d977ac0c74c" }, - "text": "traces\n| where timestamp between (_startTime .. _endTime)\n| where customDimensions.eventId startswith \"ALEscapeRoom\"\n| extend \n company = tostring(customDimensions.alCompany),\n venueName = tostring(customDimensions.alVenueName),\n userId = tostring(customDimensions.alUserId),\n userRole = tostring(customDimensions.alUserRole),\n objectType = tostring(customDimensions.alObjectType),\n eventId = tostring(customDimensions.eventId),\n companyName = tostring(customDimensions.companyName),\n callerPublisher = tostring(customDimensions.alCallerPublisher),\n description = tostring(customDimensions.alDescription),\n environmentName = tostring(customDimensions.environmentName),\n roomName = tostring(customDimensions.alRoomName),\n isEvaluationCompany = tostring(customDimensions.alIsEvaluationCompany),\n callerAppVersionMinor = tostring(customDimensions.alCallerAppVersionMinor),\n callerAppVersionMajor = tostring(customDimensions.alCallerAppVersionMajor),\n taskName = tostring(customDimensions.alTaskName),\n isAdmin = tostring(customDimensions.alIsAdmin),\n objectName = tostring(customDimensions.alObjectName),\n environmentType = tostring(customDimensions.environmentType),\n Attendee = tostring(customDimensions.alVenueUserName),\n Partner = tostring(customDimensions.alVenuePartner),\n venueId = tostring(customDimensions.alVenueId),\n taskStopDateTime = todatetime(customDimensions.alTaskStopDateTime),\n Hint = tostring(customDimensions.alHint),\n HintDateTime = todatetime(customDimensions.alHintDateTime),\n roomStartDateTime = todatetime(customDimensions.alRoomStartDateTime),\n roomStopDateTime = todatetime(customDimensions.alRoomStopDateTime),\n Score = toint(customDimensions.alScorePoints),\n RoomCompletionTimeMinutes = todecimal(datetime_diff('minute', todatetime(customDimensions.alRoomStopDateTime), todatetime(customDimensions.alRoomStartDateTime))),\n VenueCompletionTimeMinutes = todecimal(customDimensions.alVenueCompletionTimeMinutes)\n| where venueName has_any (_venue)\n| where roomName has_any (_room)", + "text": "traces\n| where timestamp between (_startTime .. _endTime)\n| where customDimensions.eventId startswith \"ALEscapeRoom\"\n| extend \n company = tostring(customDimensions.alCompany),\n venueName = tostring(customDimensions.alVenueName),\n userId = tostring(customDimensions.alUserId),\n userRole = tostring(customDimensions.alUserRole),\n objectType = tostring(customDimensions.alObjectType),\n eventId = tostring(customDimensions.eventId),\n companyName = tostring(customDimensions.companyName),\n callerPublisher = tostring(customDimensions.alCallerPublisher),\n description = tostring(customDimensions.alDescription),\n environmentName = tostring(customDimensions.environmentName),\n roomName = tostring(customDimensions.alRoomName),\n isEvaluationCompany = tostring(customDimensions.alIsEvaluationCompany),\n callerAppVersionMinor = tostring(customDimensions.alCallerAppVersionMinor),\n callerAppVersionMajor = tostring(customDimensions.alCallerAppVersionMajor),\n taskName = tostring(customDimensions.alTaskName),\n isAdmin = tostring(customDimensions.alIsAdmin),\n objectName = tostring(customDimensions.alObjectName),\n environmentType = tostring(customDimensions.environmentType),\n Attendee = tostring(customDimensions.alVenueUserName),\n Partner = tostring(customDimensions.alVenuePartner),\n venueId = tostring(customDimensions.alVenueId),\n taskStopDateTime = todatetime(customDimensions.alTaskStopDateTime),\n Hint = tostring(customDimensions.alHint),\n HintDateTime = todatetime(customDimensions.alHintDateTime),\n roomStartDateTime = todatetime(customDimensions.alRoomStartDateTime),\n roomStopDateTime = todatetime(customDimensions.alRoomStopDateTime),\n Score = toint(customDimensions.alScorePoints),\n RoomCompletionTimeMinutes = todecimal(datetime_diff('minute', todatetime(customDimensions.alRoomStopDateTime), todatetime(customDimensions.alRoomStartDateTime))),\n VenueCompletionTimeMinutes = todecimal(replace_string(tostring(customDimensions.alVenueCompletionTimeMinutes),',','.'))\n| where venueName has_any (_venue)\n| where roomName has_any (_room)", "usedVariables": [ "_endTime", "_room", diff --git a/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al b/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al index b59600eb..871dc2d7 100644 --- a/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al +++ b/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenue.Table.al @@ -136,4 +136,41 @@ table 73926 "Escape Room Venue" begin EscapeRoom.UpdateVenue(Rec.Venue); end; + + procedure ResetVenue() + var + Room: Record "Escape Room"; + Task: Record "Escape Room Task"; + EscapeRoomMgt: Codeunit "Escape Room"; + CurrentRoom: Interface iEscapeRoom; + ConfirmManagement: Codeunit "Confirm Management"; + ResetVenueQst: Label 'Are you sure you want to reset this venue? All progress, rooms, tasks, and hints will be permanently lost and there is no way back.'; + begin + if not ConfirmManagement.GetResponseOrDefault(ResetVenueQst, false) then + exit; + + Room.SetRange("Venue Id", Rec."Id"); + if Room.FindSet(true) then + repeat + Task.SetRange("Venue Id", Room."Venue Id"); + Task.SetRange("Room Name", Room.Name); + Task.DeleteAll(); + + CurrentRoom := Room.Room; + EscapeRoomMgt.RefreshTasks(CurrentRoom); + + Room.Status := Room.Status::Locked; + Room."Start DateTime" := 0DT; + Room."Stop DateTime" := 0DT; + Room."Solution DateTime" := 0DT; + Room.Modify(); + until Room.Next() = 0; + + Rec."Full Name" := ''; + Rec."Partner Name" := ''; + Rec."Start DateTime" := 0DT; + Rec."Stop DateTime" := 0DT; + Rec.Modify(); + Commit(); + end; } \ No newline at end of file diff --git a/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenueCard.Page.al b/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenueCard.Page.al index 183f32ba..da5be182 100644 --- a/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenueCard.Page.al +++ b/non production apps/EscapeRoomApp/Src/1.Venue/EscapeRoomVenueCard.Page.al @@ -90,12 +90,32 @@ page 73928 "Escape Room Venue Card" ToolTip = 'View the escape rooms for this venue.'; } } + area(Processing) + { + action(ResetVenue) + { + ApplicationArea = All; + Caption = 'Reset Venue'; + Image = Restore; + ToolTip = 'Resets the venue to its initial state. All progress, rooms, tasks, and hints will be permanently lost.'; + + trigger OnAction() + begin + Rec.ResetVenue(); + CurrPage.Update(false); + end; + } + } area(Promoted) { actionref(EscapeRoomsRef; EscapeRooms) { Visible = true; } + actionref(ResetVenueRef; ResetVenue) + { + Visible = true; + } } } diff --git a/non production apps/EscapeRoomApp/Src/EscapeRoomNotifications.Codeunit.al b/non production apps/EscapeRoomApp/Src/EscapeRoomNotifications.Codeunit.al index 54f5f681..c6833b7d 100644 --- a/non production apps/EscapeRoomApp/Src/EscapeRoomNotifications.Codeunit.al +++ b/non production apps/EscapeRoomApp/Src/EscapeRoomNotifications.Codeunit.al @@ -56,7 +56,7 @@ codeunit 73923 EscapeRoomNotifications Base64Image := ''; PictureViewer.SetImage(Base64Image); - PictureViewer.Run(); + PictureViewer.RunModal(); end; Procedure Warning(Message: Text) diff --git a/non production apps/EscapeRoomApp/app.json b/non production apps/EscapeRoomApp/app.json index 655c4a57..367417c2 100644 --- a/non production apps/EscapeRoomApp/app.json +++ b/non production apps/EscapeRoomApp/app.json @@ -2,7 +2,7 @@ "id": "f03c0f0c-d887-4279-b226-dea59737ecf8", "name": "BCTalent.EscapeRoom", "publisher": "waldo & AJ", - "version": "1.3.10026.2", + "version": "1.3.10026.4", "brief": "", "description": "", "privacyStatement": "",