diff --git a/src/TeamsISO.App/Services/ControlPanelHtml.cs b/src/TeamsISO.App/Services/ControlPanelHtml.cs
new file mode 100644
index 0000000..242ed1c
--- /dev/null
+++ b/src/TeamsISO.App/Services/ControlPanelHtml.cs
@@ -0,0 +1,196 @@
+namespace TeamsISO.App.Services;
+
+///
+/// The HTML / CSS / JS for the embedded control panel served at
+/// GET /ui. Single self-contained string — no external CDN deps, no
+/// build step, no React. Just enough to give operators a phone-friendly
+/// remote that connects via WebSocket to /ws and posts to the
+/// existing REST endpoints.
+///
+/// Visual language matches the WPF host: dark canvas, cyan accent, mono
+/// font for codey labels. Keeping the styling minimal so a future iteration
+/// can swap in a fancier UI without breaking operator workflows that already
+/// bookmark the URL.
+///
+internal static class ControlPanelHtml
+{
+ private const string Html = @"
+
+
+
+
+TeamsISO Control
+
+
+
+
TeamsISO control surface
+
+
+
+ connecting…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+";
+
+ public static string Get() => Html;
+}
diff --git a/src/TeamsISO.App/Services/NotesService.cs b/src/TeamsISO.App/Services/NotesService.cs
new file mode 100644
index 0000000..7bbd280
--- /dev/null
+++ b/src/TeamsISO.App/Services/NotesService.cs
@@ -0,0 +1,60 @@
+using System.IO;
+using System.Text;
+
+namespace TeamsISO.App.Services;
+
+///
+/// Append-only show-notes log. Each call writes a timestamped line to a daily
+/// markdown file at %LOCALAPPDATA%\TeamsISO\Notes\<YYYY-MM-DD>.md.
+/// Operators stamp notes via the REST POST /notes endpoint or the OSC
+/// /teamsiso/notes "..." address — typically wired to a Stream Deck
+/// button so a note can be left without leaving the show.
+///
+/// We deliberately don't surface the notes inside the WPF UI: the file is
+/// trivial to open in any editor, and inline note-taking would be a much
+/// bigger feature (textarea, scrollback, autosave). The endpoint is the
+/// minimum-viable affordance for live note capture.
+///
+public static class NotesService
+{
+ private static readonly object _gate = new();
+
+ private static string NotesDirectory =>
+ Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "TeamsISO", "Notes");
+
+ /// Today's notes file path (created lazily on first append).
+ public static string TodayPath =>
+ Path.Combine(NotesDirectory, $"{DateTimeOffset.Now:yyyy-MM-dd}.md");
+
+ ///
+ /// Append a single timestamped line. Concurrent callers serialize through
+ /// the static gate so we don't end up with interleaved writes from the
+ /// REST handler thread vs. the OSC dispatcher.
+ ///
+ public static bool Append(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text)) return false;
+ try
+ {
+ lock (_gate)
+ {
+ Directory.CreateDirectory(NotesDirectory);
+ var path = TodayPath;
+ var line = $"- **{DateTimeOffset.Now:HH:mm:ss}** — {text.Trim()}{Environment.NewLine}";
+ if (!File.Exists(path))
+ {
+ var header = $"# TeamsISO show notes — {DateTimeOffset.Now:yyyy-MM-dd}{Environment.NewLine}{Environment.NewLine}";
+ File.WriteAllText(path, header, Encoding.UTF8);
+ }
+ File.AppendAllText(path, line, Encoding.UTF8);
+ }
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}