diff --git a/Scenes/ConceptGlossary.tscn b/Scenes/ConceptGlossary.tscn new file mode 100644 index 0000000..d189832 --- /dev/null +++ b/Scenes/ConceptGlossary.tscn @@ -0,0 +1,105 @@ +[gd_scene load_steps=2 format=3 uid="uid://concept_glossary"] + +[ext_resource type="Script" path="res://Scripts/ConceptGlossary.gd" id="1"] + +[node name="ConceptGlossary" type="Control"] +visible = false +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") + +[node name="CanvasLayer" type="CanvasLayer" parent="."] +layer = 100 +visible = false + +[node name="Background" type="ColorRect" parent="CanvasLayer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.7) + +[node name="Panel" type="Panel" parent="CanvasLayer"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -400.0 +offset_top = -300.0 +offset_right = 400.0 +offset_bottom = 300.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="MarginContainer" type="MarginContainer" parent="CanvasLayer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 20 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 20 +theme_override_constants/margin_bottom = 20 + +[node name="VBoxContainer" type="VBoxContainer" parent="CanvasLayer/Panel/MarginContainer"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Title" type="Label" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "📖 Knative Concepts" +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 24 + +[node name="SearchBox" type="LineEdit" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +placeholder_text = "Search concepts..." + +[node name="ScrollContainer" type="ScrollContainer" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ResultsList" type="ItemList" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="ConceptPanel" type="VBoxContainer" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 10 + +[node name="ConceptName" type="Label" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel"] +layout_mode = 2 +theme_override_font_sizes/font_size = 20 +horizontal_alignment = 1 + +[node name="Definition" type="Label" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel"] +layout_mode = 2 +autowrap_mode = 3 + +[node name="GameExample" type="Label" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel"] +layout_mode = 2 +autowrap_mode = 3 + +[node name="KnativeExample" type="Label" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel"] +layout_mode = 2 +autowrap_mode = 3 + +[node name="DocsButton" type="Button" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel"] +layout_mode = 2 +text = "📚 Read Official Docs" + +[node name="CloseButton" type="Button" parent="CanvasLayer/Panel/MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Close" + +[connection signal="item_selected" from="CanvasLayer/Panel/MarginContainer/VBoxContainer/ScrollContainer/ResultsList" to="." method="_on_results_list_item_selected"] diff --git a/Scenes/message_display.tscn b/Scenes/message_display.tscn index aaca9ee..b1f5dd0 100644 --- a/Scenes/message_display.tscn +++ b/Scenes/message_display.tscn @@ -11,13 +11,50 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_y0tj3") -[node name="Label" type="Label" parent="."] -layout_mode = 0 -offset_right = 1153.0 -offset_bottom = 649.0 +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 0.7) + +[node name="CenterContainer" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Panel" type="Panel" parent="CenterContainer"] +custom_minimum_size = Vector2(900, 500) +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="CenterContainer/Panel"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 40 +theme_override_constants/margin_top = 40 +theme_override_constants/margin_right = 40 +theme_override_constants/margin_bottom = 40 + +[node name="ScrollContainer" type="ScrollContainer" parent="CenterContainer/Panel/MarginContainer"] +layout_mode = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer/Panel/MarginContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 20 + +[node name="Label" type="Label" parent="CenterContainer/Panel/MarginContainer/ScrollContainer/VBoxContainer"] +layout_mode = 2 theme_override_colors/font_color = Color(0, 0.286275, 0.556863, 1) -theme_override_colors/font_shadow_color = Color(1, 0.94902, 0.870588, 1) -theme_override_font_sizes/font_size = 100 +theme_override_font_sizes/font_size = 28 horizontal_alignment = 1 -vertical_alignment = 1 -autowrap_mode = 2 +autowrap_mode = 3 diff --git a/Scenes/sink.tscn b/Scenes/sink.tscn index fac64be..296734e 100644 --- a/Scenes/sink.tscn +++ b/Scenes/sink.tscn @@ -29,6 +29,13 @@ script = ExtResource("2_16xt0") position = Vector2(0, -5.83331) shape = SubResource("RectangleShape2D_7l3ci") +[node name="IndicatorLight" type="ColorRect" parent="."] +offset_left = -10.0 +offset_top = -220.0 +offset_right = 10.0 +offset_bottom = -200.0 +color = Color(0, 1, 0, 1) +z_index = 1 [node name="hoverlabel" type="RichTextLabel" parent="."] visible = false offset_left = 180.0 diff --git a/Scripts/ConceptGlossary.gd b/Scripts/ConceptGlossary.gd new file mode 100644 index 0000000..2b33067 --- /dev/null +++ b/Scripts/ConceptGlossary.gd @@ -0,0 +1,140 @@ +extends Control + +@onready var search_box = $CanvasLayer/Panel/MarginContainer/VBoxContainer/SearchBox +@onready var results_list = $CanvasLayer/Panel/MarginContainer/VBoxContainer/ScrollContainer/ResultsList +@onready var concept_panel = $CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel +@onready var concept_name = $CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel/ConceptName +@onready var definition = $CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel/Definition +@onready var game_example = $CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel/GameExample +@onready var knative_example = $CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel/KnativeExample +@onready var docs_button = $CanvasLayer/Panel/MarginContainer/VBoxContainer/ConceptPanel/DocsButton +@onready var close_button = $CanvasLayer/Panel/MarginContainer/VBoxContainer/CloseButton + +var concepts = { + "source": { + "name": "Event Source", + "definition": "A source generates events and sends them to a broker", + "game_example": "The event boxes at the start of each level", + "knative_example": "PingSource, ApiServerSource, KafkaSource", + "docs": "https://knative.dev/docs/eventing/sources/" + }, + "broker": { + "name": "Broker", + "definition": "A broker receives events from sources and delivers them to sinks via triggers", + "game_example": "The invisible routing system that moves events in the game", + "knative_example": "MTChannelBasedBroker, RabbitMQBroker", + "docs": "https://knative.dev/docs/eventing/brokers/" + }, + "trigger": { + "name": "Trigger", + "definition": "A trigger filters events and routes matching ones to a specific sink", + "game_example": "The connection you create from event to sink", + "knative_example": "Trigger with CloudEvents attribute filters", + "docs": "https://knative.dev/docs/eventing/triggers/" + }, + "filter": { + "name": "Filter", + "definition": "Rules that determine which events match a trigger", + "game_example": "The colored filters you drag onto the conveyor", + "knative_example": "CloudEvents attribute filters (type, source, etc.)", + "docs": "https://knative.dev/docs/eventing/triggers/#trigger-filtering" + }, + "sink": { + "name": "Sink", + "definition": "A sink receives and processes events", + "game_example": "The colored boxes where events are delivered", + "knative_example": "Knative Service, Channel, URI endpoint", + "docs": "https://knative.dev/docs/eventing/sinks/" + }, + "dlq": { + "name": "Dead Letter Queue", + "definition": "A special sink that catches events that fail to process", + "game_example": "The DLS that catches blocked events in Level 4", + "knative_example": "DeadLetterSink in delivery spec", + "docs": "https://knative.dev/docs/eventing/event-delivery/" + }, + "cloudevents": { + "name": "CloudEvents", + "definition": "A standard format for describing events", + "game_example": "The event boxes with different colors (types)", + "knative_example": "CloudEvents spec with type, source, id, etc.", + "docs": "https://cloudevents.io/" + }, + "channel": { + "name": "Channel", + "definition": "A messaging channel that can be used as a sink or source", + "game_example": "Not directly shown in current levels", + "knative_example": "InMemoryChannel, KafkaChannel", + "docs": "https://knative.dev/docs/eventing/channels/" + }, + "subscription": { + "name": "Subscription", + "definition": "Routes events from a channel to a sink", + "game_example": "Similar to the trigger concept in the game", + "knative_example": "Subscription connecting Channel to Sink", + "docs": "https://knative.dev/docs/eventing/channels/subscriptions/" + }, + "retry": { + "name": "Retry Policy", + "definition": "Configuration for retrying failed event deliveries", + "game_example": "Related to DLQ pattern in Level 4", + "knative_example": "Retry count and backoff delay in delivery spec", + "docs": "https://knative.dev/docs/eventing/event-delivery/" + } +} + +func _ready(): + add_to_group("Glossary") + search_box.text_changed.connect(_on_search_changed) + close_button.pressed.connect(_on_close_pressed) + docs_button.pressed.connect(_on_docs_pressed) + concept_panel.visible = false + populate_all_concepts() + +func populate_all_concepts(): + results_list.clear() + for key in concepts: + results_list.add_item(concepts[key].name) + +func _on_search_changed(query: String): + if query == "": + populate_all_concepts() + return + + results_list.clear() + for key in concepts: + if key.contains(query.to_lower()) or \ + concepts[key].name.to_lower().contains(query.to_lower()): + results_list.add_item(concepts[key].name) + +func _on_results_list_item_selected(index: int): + var item_text = results_list.get_item_text(index) + for key in concepts: + if concepts[key].name == item_text: + show_concept(key) + break + +func show_concept(concept_id: String): + var concept = concepts.get(concept_id) + if concept: + concept_name.text = concept.name + definition.text = concept.definition + game_example.text = "🎮 In game: " + concept.game_example + knative_example.text = "📦 In Knative: " + concept.knative_example + docs_button.set_meta("url", concept.docs) + concept_panel.visible = true + +func _on_close_pressed(): + $CanvasLayer.visible = false + +func _on_docs_pressed(): + var url = docs_button.get_meta("url") + OS.shell_open(url) + +func open_to_term(term: String): + $CanvasLayer.visible = true + if concepts.has(term): + show_concept(term) + +func open(): + $CanvasLayer.visible = true diff --git a/Scripts/HoverLabel.gd b/Scripts/HoverLabel.gd new file mode 100644 index 0000000..d4d4eb5 --- /dev/null +++ b/Scripts/HoverLabel.gd @@ -0,0 +1,45 @@ +extends Label + +var hover_text: String = "" +var element_type: String = "" + +func _ready(): + mouse_entered.connect(_on_mouse_entered) + mouse_exited.connect(_on_mouse_exited) + mouse_filter = Control.MOUSE_FILTER_PASS + +func set_hover_info(type: String, description: String): + element_type = type + hover_text = description + +func _on_mouse_entered(): + if hover_text != "": + var tooltip = create_tooltip() + add_child(tooltip) + +func _on_mouse_exited(): + for child in get_children(): + if child.name == "HoverTooltip": + child.queue_free() + +func create_tooltip() -> Control: + var tooltip = Panel.new() + tooltip.name = "HoverTooltip" + tooltip.custom_minimum_size = Vector2(200, 60) + tooltip.position = Vector2(0, -70) + + var label = Label.new() + label.text = hover_text + label.autowrap_mode = TextServer.AUTOWRAP_WORD + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + + var margin = MarginContainer.new() + margin.add_theme_constant_override("margin_left", 10) + margin.add_theme_constant_override("margin_right", 10) + margin.add_theme_constant_override("margin_top", 10) + margin.add_theme_constant_override("margin_bottom", 10) + margin.add_child(label) + + tooltip.add_child(margin) + return tooltip diff --git a/Scripts/TutorialManager.gd b/Scripts/TutorialManager.gd new file mode 100644 index 0000000..2830107 --- /dev/null +++ b/Scripts/TutorialManager.gd @@ -0,0 +1,74 @@ +extends Node + +var current_tutorial: Dictionary = {} +var tutorials_completed: Dictionary = {} +var show_tutorials: bool = true +var current_step: int = 0 + +signal tutorial_started(level: String) +signal tutorial_step_completed(step: String) +signal tutorial_finished(level: String) + +func start_level_tutorial(level_name: String): + if tutorials_completed.get(level_name, false): + return + + var tutorial_path = "res://tutorial_data/" + level_name + ".json" + var tutorial_file = FileAccess.open(tutorial_path, FileAccess.READ) + if tutorial_file: + var tutorial_text = tutorial_file.get_as_text() + current_tutorial = JSON.parse_string(tutorial_text) + current_step = 0 + emit_signal("tutorial_started", level_name) + show_current_step() + +func show_current_step(): + if current_step >= current_tutorial.steps.size(): + finish_tutorial() + return + + var step = current_tutorial.steps[current_step] + show_tooltip(step.text, step.get("highlight", "")) + +func show_tooltip(text: String, highlight_target: String = ""): + if not ResourceLoader.exists("res://Scenes/TutorialTooltip.tscn"): + print("TutorialTooltip scene not found - skipping tutorial step") + return + + var tooltip_scene = load("res://Scenes/TutorialTooltip.tscn") + if tooltip_scene: + var tooltip = tooltip_scene.instantiate() + tooltip.set_text(text) + if highlight_target != "": + var target = get_tree().get_first_node_in_group(highlight_target) + if target: + tooltip.point_to(target) + add_child(tooltip) + +func next_step(): + current_step += 1 + show_current_step() + +func skip_tutorial(): + finish_tutorial() + +func finish_tutorial(): + tutorials_completed[current_tutorial.level] = true + emit_signal("tutorial_finished", current_tutorial.level) + current_tutorial = {} + +func open_glossary(term: String = ""): + var glossary = get_tree().get_first_node_in_group("Glossary") + + if not glossary: + # Auto-instantiate if missing + var glossary_scene = load("res://Scenes/ConceptGlossary.tscn") + if glossary_scene: + glossary = glossary_scene.instantiate() + get_tree().root.add_child(glossary) + + if glossary: + if term != "": + glossary.open_to_term(term) + else: + glossary.open() diff --git a/Scripts/TutorialTooltip.gd b/Scripts/TutorialTooltip.gd new file mode 100644 index 0000000..826cccd --- /dev/null +++ b/Scripts/TutorialTooltip.gd @@ -0,0 +1,20 @@ +extends Control + +@onready var label = $Panel/MarginContainer/VBoxContainer/Label +@onready var next_button = $Panel/MarginContainer/VBoxContainer/HBoxContainer/NextButton +@onready var skip_button = $Panel/MarginContainer/VBoxContainer/HBoxContainer/SkipButton + +func set_text(text: String): + label.text = text + +func point_to(target: Node2D): + # Position tooltip near target + global_position = target.global_position + Vector2(50, -100) + +func _on_next_button_pressed(): + TutorialManager.next_step() + queue_free() + +func _on_skip_button_pressed(): + TutorialManager.skip_tutorial() + queue_free() diff --git a/Scripts/level.gd b/Scripts/level.gd index 5565b4c..7ea0688 100644 --- a/Scripts/level.gd +++ b/Scripts/level.gd @@ -15,6 +15,35 @@ func initialise(): totalbox=0 nextLevel=false dlsUsed=false + +func _ready(): + _create_help_button() + +func _create_help_button(): + # Create a CanvasLayer so button stays on top of everything + var canvas = CanvasLayer.new() + add_child(canvas) + + var help_btn = Button.new() + help_btn.text = "?" + help_btn.name = "GlobalHelpButton" + + # Style the button to look like a circle/help icon + help_btn.custom_minimum_size = Vector2(40, 40) + help_btn.position = Vector2(get_viewport().get_visible_rect().size.x - 60, 20) # Top right + + # Connect signal + help_btn.pressed.connect(_on_help_pressed) + + canvas.add_child(help_btn) + +func _on_help_pressed(): + print("Help button pressed!") + # Try access via singleton name directly first (if autoloaded) + if has_node("/root/TutorialManager"): + get_node("/root/TutorialManager").open_glossary() + else: + print("CRITICAL ERROR: TutorialManager not found in /root. Please add 'Scripts/TutorialManager.gd' to Project Settings -> Autoload as 'TutorialManager'") func next_level(): if sinkUsed: @@ -47,8 +76,44 @@ func next_level(): print("End of Levels.") get_tree().change_scene_to_file("res://Scenes/end_of_all_levels.tscn") else: + var failure_data = analyze_failure() + show_educational_failure(failure_data, message_display) + await message_display.show_message_for_duration(4.0) print("Failed. Try Again") AudioManager.play_level_fail() message_display.show_message("Failed. Try Again") await message_display.show_message_for_duration(2.0) message_display.visible = false + +func analyze_failure() -> Dictionary: + var reason = "" + var hint = "" + var lesson = "" + + if not sinkUsed: + reason = "No events were delivered to any sink" + hint = "Click an event to select it, then click a sink to route it" + lesson = "In Knative, events must be routed from sources to sinks through triggers" + + elif sinkBoxMatchNeeded[levelind] and not sinkBoxMatchPresent: + reason = "Wrong event type delivered to sink" + hint = "Use filters to match event colors with sink colors" + lesson = "Knative Triggers use filters to ensure events reach the correct subscribers based on event attributes" + + elif dlsRequired[levelind] and not dlsUsed: + reason = "Failed events were not routed to Dead Letter Sink" + hint = "Click the DLS to catch events that hit the blockage" + lesson = "Dead Letter Sinks prevent data loss by catching events that fail to process" + + return { + "reason": reason, + "hint": hint, + "lesson": lesson + } + +func show_educational_failure(data: Dictionary, message_display: Control): + message_display.show_failure_with_lesson( + data.reason, + data.hint, + data.lesson + ) diff --git a/Scripts/message_display.gd b/Scripts/message_display.gd index 0692202..912d782 100644 --- a/Scripts/message_display.gd +++ b/Scripts/message_display.gd @@ -2,7 +2,7 @@ extends Control func show_message(text): print("Message displayed") - $Label.text=text + $CenterContainer/Panel/MarginContainer/ScrollContainer/VBoxContainer/Label.text = text func show_message_for_duration(duration: float) -> void: var timer := Timer.new() @@ -12,3 +12,10 @@ func show_message_for_duration(duration: float) -> void: timer.start() await timer.timeout timer.queue_free() + +func show_failure_with_lesson(reason: String, hint: String, lesson: String): + var full_message = "❌ Level Failed\n\n" + full_message += "Why: " + reason + "\n\n" + full_message += "💡 Hint: " + hint + "\n\n" + full_message += "🎓 Knative Concept:\n" + lesson + $CenterContainer/Panel/MarginContainer/ScrollContainer/VBoxContainer/Label.text = full_message diff --git a/Scripts/sink.gd b/Scripts/sink.gd index 22c93bd..45289a2 100644 --- a/Scripts/sink.gd +++ b/Scripts/sink.gd @@ -1,15 +1,30 @@ extends Sprite2D @export var expectedType: String +@export var available: bool = true + +var blink_timer: float = 0.0 +var blink_interval: float = 0.5 +var indicator_light: ColorRect = null # Called when the node enters the scene tree for the first time. func _ready() -> void: - pass # Replace with function body. + indicator_light = get_node_or_null("IndicatorLight") + if indicator_light == null: + push_warning("IndicatorLight not found in sink scene") # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: + if indicator_light != null: + if available: + blink_timer += delta + if blink_timer >= blink_interval: + blink_timer = 0.0 + indicator_light.visible = !indicator_light.visible + else: + indicator_light.visible = false pass diff --git a/tutorial_data/basicEventFlow.json b/tutorial_data/basicEventFlow.json new file mode 100644 index 0000000..3bd156d --- /dev/null +++ b/tutorial_data/basicEventFlow.json @@ -0,0 +1,21 @@ +{ + "level": "basicEventFlow", + "steps": [ + { + "text": "Welcome! In Knative, events flow from Sources to Sinks through Brokers.", + "highlight": "" + }, + { + "text": "Click this event to select it", + "highlight": "EventBox" + }, + { + "text": "Now click the sink to route the event", + "highlight": "Sink" + }, + { + "text": "Great! You just created a basic Knative event flow. This is like a Trigger connecting a Broker to a Sink.", + "highlight": "" + } + ] +} diff --git a/tutorial_data/boxClick.json b/tutorial_data/boxClick.json new file mode 100644 index 0000000..cbf4a3b --- /dev/null +++ b/tutorial_data/boxClick.json @@ -0,0 +1,25 @@ +{ + "level": "boxClick", + "steps": [ + { + "text": "Level 2: Triggers & Filters. In Knative, Triggers use filters to route events based on their attributes.", + "highlight": "" + }, + { + "text": "Notice the colored events. Each has a different 'type' attribute.", + "highlight": "EventBox" + }, + { + "text": "Drag a filter onto the conveyor to filter events by color", + "highlight": "Filter" + }, + { + "text": "Route matching events to the correct colored sink", + "highlight": "Sink" + }, + { + "text": "Perfect! Knative Triggers work the same way - filtering events by attributes like type, source, or custom fields.", + "highlight": "" + } + ] +} \ No newline at end of file diff --git a/tutorial_data/dlqPattern.json b/tutorial_data/dlqPattern.json new file mode 100644 index 0000000..a025cba --- /dev/null +++ b/tutorial_data/dlqPattern.json @@ -0,0 +1,21 @@ +{ + "level": "dlqPattern", + "steps": [ + { + "text": "Level 4: Dead Letter Queue. When events fail to process, they need somewhere to go.", + "highlight": "" + }, + { + "text": "Notice the blockage - events will fail here", + "highlight": "Blockage" + }, + { + "text": "Click the Dead Letter Sink (DLS) to route failed events there", + "highlight": "DLS" + }, + { + "text": "Perfect! In Knative, Dead Letter Sinks prevent data loss by catching failed events for debugging or retry.", + "highlight": "" + } + ] +} \ No newline at end of file diff --git a/tutorial_data/multiSink.json b/tutorial_data/multiSink.json new file mode 100644 index 0000000..66c5d27 --- /dev/null +++ b/tutorial_data/multiSink.json @@ -0,0 +1,21 @@ +{ + "level": "multiSink", + "steps": [ + { + "text": "Level 3: Multiple Sinks. In Knative, one Broker can route events to multiple subscribers.", + "highlight": "" + }, + { + "text": "You have multiple sinks. Each needs matching events.", + "highlight": "Sink" + }, + { + "text": "Use filters to route each event type to its matching sink", + "highlight": "Filter" + }, + { + "text": "Excellent! This is how Knative enables fan-out patterns - one event source, multiple subscribers.", + "highlight": "" + } + ] +} \ No newline at end of file