From 557f631da2d190ed7e9b6a62dab95e887dc2c72a Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 14:20:44 -0700 Subject: [PATCH 01/10] feat: add pre-push gate (build + test before push) - Created scripts/install-hooks.sh to install git hooks - Pre-push hook runs dotnet build + dotnet test before allowing push - Created CONTRIBUTING.md with setup instructions and pre-push gate docs - Hook skips when CI=true (CI already validates) - Emergency bypass: git push --no-verify Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 120 +++++++++++++++++++++++++++++++++++++++ scripts/install-hooks.sh | 71 +++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100755 scripts/install-hooks.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..2ae09b17 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Contributing to MyBlog + +Thank you for your interest in contributing to MyBlog! This document provides guidelines for setting up your development environment and submitting contributions. + +## Initial Setup + +### 1. Clone the Repository + +```bash +git clone https://github.com/mpaulosky/MyBlog.git +cd MyBlog +``` + +### 2. Install Git Hooks + +**IMPORTANT:** After cloning the repository, run the hook installation script: + +```bash +./scripts/install-hooks.sh +``` + +This installs a **pre-push gate** that validates your code before it reaches GitHub. + +### What the Pre-Push Gate Does + +The pre-push hook automatically runs before every `git push`: + +1. **Build** — `dotnet build MyBlog.slnx --no-incremental -c Release` +2. **Test** — `dotnet test MyBlog.slnx --no-build -c Release` + +If either step fails, the push is aborted. This prevents broken code from reaching GitHub and ensures CI stays green. + +### Bypassing the Gate (Emergency Only) + +In rare cases where you need to push despite build/test failures: + +```bash +git push --no-verify +``` + +⚠️ **Use sparingly** — This bypasses local validation. CI will still catch issues, but it's better to fix problems locally. + +## Development Workflow + +### Prerequisites + +- **.NET 10 SDK** — [Download](https://dotnet.microsoft.com/en-us/download) +- **Auth0 account** — See [AUTH0_SETUP.md](docs/AUTH0_SETUP.md) for configuration + +### Building and Testing + +```bash +# Restore dependencies +dotnet restore + +# Build the solution +dotnet build + +# Run all tests +dotnet test + +# Run the application (via Aspire AppHost) +cd src/AppHost +dotnet run +``` + +### Branch Strategy + +- **`main`** — Release-only branch, protected with strict rules +- **`dev`** — Primary development branch (default) +- **`squad/*`** — Feature branches for squad members and Copilot agents + +Create feature branches from `dev`: + +```bash +git checkout dev +git pull origin dev +git checkout -b squad/my-feature +``` + +### Pull Requests + +1. Create a PR from your `squad/*` branch to `dev` +2. Ensure all CI checks pass (build, tests, coverage) +3. Address code review feedback +4. Squash and merge once approved + +## Code Standards + +### .NET Conventions + +- Follow .NET naming conventions (PascalCase for types, camelCase for locals) +- Use C# 14 features (file-scoped namespaces, record types, pattern matching) +- Keep methods focused and testable + +### Testing + +- Write unit tests for new domain logic +- Maintain or improve code coverage (currently >90%) +- Use xUnit, FluentAssertions, and NSubstitute + +### Architecture Rules + +- Domain layer must not depend on Web layer +- Use repository pattern for data access +- Keep Blazor components focused (presentation only) + +## Resources + +- [ARCHITECTURE.md](docs/ARCHITECTURE.md) — Solution structure and design decisions +- [AUTH0_SETUP.md](docs/AUTH0_SETUP.md) — Auth0 configuration guide +- [README.md](README.md) — Project overview and getting started + +## Questions? + +Open an issue or reach out to @mpaulosky. + +--- + +**Remember:** Install git hooks with `./scripts/install-hooks.sh` after cloning! diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100755 index 00000000..b4069d5a --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Script to install git hooks for MyBlog project +# Run this after cloning the repository to set up local development hooks + +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HOOKS_DIR="$REPO_ROOT/.git/hooks" +PRE_PUSH_HOOK="$HOOKS_DIR/pre-push" + +echo "Installing pre-push hook..." + +# Create the pre-push hook +cat > "$PRE_PUSH_HOOK" << 'HOOK_CONTENT' +#!/bin/bash + +# Pre-push hook: Runs build and tests before allowing push +# Ensures broken code doesn't reach GitHub + +# Skip if running in CI (CI already validates) +if [ "$CI" = "true" ]; then + exit 0 +fi + +echo "🔍 Pre-push gate: Running build and tests..." +echo "" + +# Run build +echo "▶️ Building solution (dotnet build MyBlog.slnx --no-incremental -c Release)..." +if ! dotnet build MyBlog.slnx --no-incremental -c Release; then + echo "" + echo "❌ Build FAILED. Push aborted." + echo "💡 Fix build errors, commit, and try again." + echo "⚠️ To skip this check (emergency only): git push --no-verify" + exit 1 +fi + +echo "" +echo "✅ Build passed" +echo "" + +# Run tests +echo "▶️ Running tests (dotnet test MyBlog.slnx --no-build -c Release)..." +if ! dotnet test MyBlog.slnx --no-build -c Release; then + echo "" + echo "❌ Tests FAILED. Push aborted." + echo "💡 Fix failing tests, commit, and try again." + echo "⚠️ To skip this check (emergency only): git push --no-verify" + exit 1 +fi + +echo "" +echo "✅ All tests passed" +echo "" +echo "🚀 Push allowed - build and tests successful!" +echo "" +HOOK_CONTENT + +# Make it executable +chmod +x "$PRE_PUSH_HOOK" + +echo "✅ Pre-push hook installed at .git/hooks/pre-push" +echo "" +echo "The hook will:" +echo " 1. Build the solution (dotnet build MyBlog.slnx)" +echo " 2. Run all tests (dotnet test MyBlog.slnx)" +echo " 3. Abort push if either fails" +echo "" +echo "To bypass in emergencies: git push --no-verify" +echo "" From 8b0b72a2ec5fc0bfe76ef73e5664618339e2b770 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:25:15 -0700 Subject: [PATCH 02/10] fix(infra): align pre-push gate with actual MyBlog project structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix solution name: IssueTrackerApp.slnx → MyBlog.slnx in hook/docs/template - Fix Gate 3 test projects: replace 6 nonexistent projects with tests/Architecture.Tests and tests/Unit.Tests (actual projects) - Fix Gate 4 integration tests: replace 4 nonexistent projects with tests/Integration.Tests (actual project using Testcontainers + Aspire) - Rewrite scripts/install-hooks.sh: copies .github/hooks/pre-push instead of embedding an outdated script; idempotent (skip if same, backup if different); uses git rev-parse --git-path hooks - Update CONTRIBUTING.md: table describing all 5 gates with correct cmds - Update .copilot/skills/pre-push-test-gate/SKILL.md: correct project lists - Update .github/pull_request_template.md: correct solution name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .copilot/skills/pre-push-test-gate/SKILL.md | 23 ++---- .github/hooks/pre-push | 15 +--- .github/pull_request_template.md | 4 +- CONTRIBUTING.md | 13 +++- scripts/install-hooks.sh | 82 +++++++-------------- 5 files changed, 48 insertions(+), 89 deletions(-) diff --git a/.copilot/skills/pre-push-test-gate/SKILL.md b/.copilot/skills/pre-push-test-gate/SKILL.md index 4f50ebd0..acf69662 100644 --- a/.copilot/skills/pre-push-test-gate/SKILL.md +++ b/.copilot/skills/pre-push-test-gate/SKILL.md @@ -22,31 +22,24 @@ The pre-push hook (`.github/hooks/pre-push`) enforces **5 gates** that mirror CI |------|------|-------------|-----------| | **0** | Branch protection | Checks current branch | Push is to `main` or `dev` | | **1** | Untracked source files | Scans for untracked `.razor`/`.cs` files | Untracked source files found (prompts y/N) | -| **2** | Release build | `dotnet build IssueTrackerApp.slnx --configuration Release` | Build fails (3 retries) | -| **3** | Unit/Arch/bUnit tests | Runs 6 test projects in Release mode | Any test project fails (3 retries) | -| **4** | Integration tests | Runs 4 integration test projects (Docker required) | Any test project fails (3 retries) | +| **2** | Release build | `dotnet build MyBlog.slnx --configuration Release` | Build fails (3 retries) | +| **3** | Unit/Arch tests | Runs 2 test projects in Release mode | Any test project fails (3 retries) | +| **4** | Integration tests | Runs 1 integration test project (Docker required) | Any test project fails (3 retries) | -### Gate 3 — Unit Test Projects (6 total) +### Gate 3 — Unit Test Projects (2 total) ``` tests/Architecture.Tests/Architecture.Tests.csproj -tests/Domain.Tests/Domain.Tests.csproj -tests/Web.Tests.Bunit/Web.Tests.Bunit.csproj -tests/Persistence.MongoDb.Tests/Persistence.MongoDb.Tests.csproj -tests/Web.Tests/Web.Tests.csproj -tests/Persistence.AzureStorage.Tests/Persistence.AzureStorage.Tests.csproj +tests/Unit.Tests/Unit.Tests.csproj ``` -### Gate 4 — Integration Test Projects (4 total, Docker required) +### Gate 4 — Integration Test Projects (1 total, Docker required) ``` -tests/Persistence.MongoDb.Tests.Integration/Persistence.MongoDb.Tests.Integration.csproj -tests/Web.Tests.Integration/Web.Tests.Integration.csproj -tests/Persistence.AzureStorage.Tests.Integration/Persistence.AzureStorage.Tests.Integration.csproj -tests/AppHost.Tests/AppHost.Tests.csproj +tests/Integration.Tests/Integration.Tests.csproj ``` -These use Testcontainers (mongo:7.0, Azurite) and Aspire DCP. Docker daemon MUST be running. +These use Testcontainers (MongoDb) and Aspire DCP. Docker daemon MUST be running. ### Retry Behavior diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push index 6caa6860..4249d552 100755 --- a/.github/hooks/pre-push +++ b/.github/hooks/pre-push @@ -43,12 +43,12 @@ while [[ $BUILD_ATTEMPT -lt $MAX_ATTEMPTS ]]; do BUILD_ATTEMPT=$((BUILD_ATTEMPT + 1)) echo -e "\n${CYAN}🔨 Release build (attempt $BUILD_ATTEMPT/$MAX_ATTEMPTS)...${RESET}" - dotnet build IssueTrackerApp.slnx --configuration Release + dotnet build MyBlog.slnx --configuration Release BUILD_EXIT=$? if [[ $BUILD_EXIT -ne 0 ]]; then echo -e " ↻ Auto-retrying build once (clearing incremental state)..." - dotnet build IssueTrackerApp.slnx --configuration Release + dotnet build MyBlog.slnx --configuration Release BUILD_EXIT=$? fi @@ -73,11 +73,7 @@ fi # ── Gate 3: Unit + bUnit + Architecture tests ────────────────────────────── TEST_PROJECTS=( "tests/Architecture.Tests/Architecture.Tests.csproj" - "tests/Domain.Tests/Domain.Tests.csproj" - "tests/Web.Tests.Bunit/Web.Tests.Bunit.csproj" - "tests/Persistence.MongoDb.Tests/Persistence.MongoDb.Tests.csproj" - "tests/Web.Tests/Web.Tests.csproj" - "tests/Persistence.AzureStorage.Tests/Persistence.AzureStorage.Tests.csproj" + "tests/Unit.Tests/Unit.Tests.csproj" ) TEST_ATTEMPT=0 TESTS_OK=false @@ -112,10 +108,7 @@ fi # Requires Docker daemon. Uses Testcontainers (mongo:7.0, Azurite) and Aspire DCP. # AppHost.Tests runs here — Aspire boots internally via DistributedApplicationTestingBuilder. INTEGRATION_PROJECTS=( - "tests/Persistence.MongoDb.Tests.Integration/Persistence.MongoDb.Tests.Integration.csproj" - "tests/Web.Tests.Integration/Web.Tests.Integration.csproj" - "tests/Persistence.AzureStorage.Tests.Integration/Persistence.AzureStorage.Tests.Integration.csproj" - "tests/AppHost.Tests/AppHost.Tests.csproj" + "tests/Integration.Tests/Integration.Tests.csproj" ) echo -e "\n${CYAN}🐋 Checking Docker availability for integration tests...${RESET}" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c5b7a29a..af13a7b3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -35,8 +35,8 @@ Closes # ### Code Quality -- [ ] I ran `dotnet build IssueTrackerApp.slnx --configuration Release` — 0 errors, 0 warnings -- [ ] I ran `dotnet test IssueTrackerApp.slnx --configuration Release --no-build` — all pass +- [ ] I ran `dotnet build MyBlog.slnx --configuration Release` — 0 errors, 0 warnings +- [ ] I ran `dotnet test MyBlog.slnx --configuration Release --no-build` — all pass - [ ] No TODO/FIXME left unless tracked in a follow-up issue (link it) - [ ] No secrets, API keys, or credentials committed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ae09b17..cf34e6e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,12 +23,17 @@ This installs a **pre-push gate** that validates your code before it reaches Git ### What the Pre-Push Gate Does -The pre-push hook automatically runs before every `git push`: +The pre-push hook automatically runs before every `git push` and enforces **5 gates**: -1. **Build** — `dotnet build MyBlog.slnx --no-incremental -c Release` -2. **Test** — `dotnet test MyBlog.slnx --no-build -c Release` +| Gate | Name | Action | +|------|------|--------| +| **0** | Branch protection | Blocks direct pushes to `main` or `dev` | +| **1** | Untracked source files | Warns about untracked `.razor`/`.cs` files | +| **2** | Release build | `dotnet build MyBlog.slnx --configuration Release` | +| **3** | Unit/Arch tests | `tests/Architecture.Tests`, `tests/Unit.Tests` | +| **4** | Integration tests | `tests/Integration.Tests` (Docker required) | -If either step fails, the push is aborted. This prevents broken code from reaching GitHub and ensures CI stays green. +Gates 2–4 allow up to 3 attempts — the hook pauses between failures so you can fix and retry without re-running from scratch. ### Bypassing the Gate (Emergency Only) diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index b4069d5a..629b23f0 100755 --- a/scripts/install-hooks.sh +++ b/scripts/install-hooks.sh @@ -1,71 +1,39 @@ #!/bin/bash - -# Script to install git hooks for MyBlog project -# Run this after cloning the repository to set up local development hooks +# Installs the pre-push gate hook from .github/hooks/pre-push into the local Git hooks directory. +# Safe to re-run: skips if already up-to-date, backs up any differing hook before overwriting. set -e REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -HOOKS_DIR="$REPO_ROOT/.git/hooks" -PRE_PUSH_HOOK="$HOOKS_DIR/pre-push" - -echo "Installing pre-push hook..." - -# Create the pre-push hook -cat > "$PRE_PUSH_HOOK" << 'HOOK_CONTENT' -#!/bin/bash - -# Pre-push hook: Runs build and tests before allowing push -# Ensures broken code doesn't reach GitHub +SOURCE="$REPO_ROOT/.github/hooks/pre-push" +HOOKS_DIR="$(git -C "$REPO_ROOT" rev-parse --git-path hooks)" +DEST="$HOOKS_DIR/pre-push" -# Skip if running in CI (CI already validates) -if [ "$CI" = "true" ]; then - exit 0 -fi - -echo "🔍 Pre-push gate: Running build and tests..." -echo "" - -# Run build -echo "▶️ Building solution (dotnet build MyBlog.slnx --no-incremental -c Release)..." -if ! dotnet build MyBlog.slnx --no-incremental -c Release; then - echo "" - echo "❌ Build FAILED. Push aborted." - echo "💡 Fix build errors, commit, and try again." - echo "⚠️ To skip this check (emergency only): git push --no-verify" +if [[ ! -f "$SOURCE" ]]; then + echo "❌ Source hook not found: $SOURCE" exit 1 fi -echo "" -echo "✅ Build passed" -echo "" - -# Run tests -echo "▶️ Running tests (dotnet test MyBlog.slnx --no-build -c Release)..." -if ! dotnet test MyBlog.slnx --no-build -c Release; then - echo "" - echo "❌ Tests FAILED. Push aborted." - echo "💡 Fix failing tests, commit, and try again." - echo "⚠️ To skip this check (emergency only): git push --no-verify" - exit 1 +if [[ -f "$DEST" ]] && cmp -s "$SOURCE" "$DEST"; then + echo "✅ Pre-push hook is already up-to-date. Nothing to do." + exit 0 fi -echo "" -echo "✅ All tests passed" -echo "" -echo "🚀 Push allowed - build and tests successful!" -echo "" -HOOK_CONTENT - -# Make it executable -chmod +x "$PRE_PUSH_HOOK" +if [[ -f "$DEST" ]]; then + BACKUP="$DEST.bak.$(date +%Y%m%d%H%M%S)" + echo "⚠️ Existing hook differs — backing up to: $BACKUP" + cp "$DEST" "$BACKUP" +fi -echo "✅ Pre-push hook installed at .git/hooks/pre-push" -echo "" -echo "The hook will:" -echo " 1. Build the solution (dotnet build MyBlog.slnx)" -echo " 2. Run all tests (dotnet test MyBlog.slnx)" -echo " 3. Abort push if either fails" +cp "$SOURCE" "$DEST" +chmod +x "$DEST" +echo "✅ Pre-push hook installed at $DEST" echo "" -echo "To bypass in emergencies: git push --no-verify" +echo "The hook enforces 5 gates on every 'git push':" +echo " 0. Blocks direct pushes to main/dev" +echo " 1. Warns about untracked .razor/.cs source files" +echo " 2. Release build (dotnet build MyBlog.slnx --configuration Release)" +echo " 3. Unit/arch tests (tests/Architecture.Tests, tests/Unit.Tests)" +echo " 4. Integration tests (tests/Integration.Tests — Docker required)" echo "" +echo "To skip in an emergency: git push --no-verify" From c7024a5eb2841bfd6ab56569ef403ad507c7a66a Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:35:07 -0700 Subject: [PATCH 03/10] docs(squad): sync pre-push hook history and comments - align inline hook comments with the actual Gate 3 and Gate 4 project lists - rewrite Boromir's pre-push gate history entry to match the shipped hook and installer behavior - record Ralph's maintenance handoff for the pre-push gate follow-up Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/hooks/pre-push | 9 ++++----- .squad/agents/boromir/history.md | 34 ++++++++++++++++++++++++++++++++ .squad/agents/ralph/history.md | 7 +++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push index 4249d552..58465f9c 100755 --- a/.github/hooks/pre-push +++ b/.github/hooks/pre-push @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Pre-push gate: mirrors CI checks to catch failures before they reach GitHub -# Runs: untracked-file check → Release build → unit tests (loop until pass or abort) +# Runs: branch protection → untracked-file check → Release build → unit/architecture tests → integration tests # NOTE: git provides refspecs on stdin; interactive prompts must use /dev/tty. set -uo pipefail @@ -70,7 +70,7 @@ if [[ "$BUILD_OK" != true ]]; then exit 1 fi -# ── Gate 3: Unit + bUnit + Architecture tests ────────────────────────────── +# ── Gate 3: Unit + Architecture tests ────────────────────────────────────── TEST_PROJECTS=( "tests/Architecture.Tests/Architecture.Tests.csproj" "tests/Unit.Tests/Unit.Tests.csproj" @@ -104,9 +104,8 @@ if [[ "$TESTS_OK" != true ]]; then exit 1 fi -# ── Gate 4: Docker integration tests + Playwright E2E ───────────────────── -# Requires Docker daemon. Uses Testcontainers (mongo:7.0, Azurite) and Aspire DCP. -# AppHost.Tests runs here — Aspire boots internally via DistributedApplicationTestingBuilder. +# ── Gate 4: Docker-backed integration tests ──────────────────────────────── +# Requires Docker daemon for Testcontainers-backed dependencies. INTEGRATION_PROJECTS=( "tests/Integration.Tests/Integration.Tests.csproj" ) diff --git a/.squad/agents/boromir/history.md b/.squad/agents/boromir/history.md index 55b52057..5681eeda 100644 --- a/.squad/agents/boromir/history.md +++ b/.squad/agents/boromir/history.md @@ -1,5 +1,39 @@ ## Learnings +### 2026-04-18 — Pre-Push Gate Implementation + +**Work completed:** +- Added `.github/hooks/pre-push` as the committed source of truth for the local hook. +- Fixed copied-project drift in the hook: `IssueTrackerApp.slnx` → `MyBlog.slnx`, Gate 3 reduced to the real `Architecture.Tests` and `Unit.Tests` projects, and Gate 4 reduced to the real `Integration.Tests` project. +- Rewrote `scripts/install-hooks.sh` to copy the committed hook into the local hooks directory, skip when already identical, and back up any differing local hook before overwriting. +- Updated `CONTRIBUTING.md`, `.copilot/skills/pre-push-test-gate/SKILL.md`, and `.github/pull_request_template.md` so the documented commands and project lists match the shipped hook. +- Kept the emergency escape hatch documented as `git push --no-verify`. + +**Branch and PR:** +- Created `squad/prepush-gate` from `origin/dev`. +- Pushed follow-up corrections to PR #12 (`squad/prepush-gate` → `dev`) after reconciling the copied hook with the actual MyBlog repo layout. + +**Key implementation details:** +- `.github/hooks/pre-push` is the source of truth; `.git/hooks/pre-push` is installed locally and is never committed. +- Gate 2 builds `MyBlog.slnx` in Release mode and auto-retries once inside each attempt to ride through transient CLR aborts. +- Gate 3 runs `tests/Architecture.Tests/Architecture.Tests.csproj` and `tests/Unit.Tests/Unit.Tests.csproj`. +- Gate 4 runs `tests/Integration.Tests/Integration.Tests.csproj` and requires Docker for Testcontainers-backed dependencies. +- The installer resolves the hooks directory with `git rev-parse --git-path hooks`, so it works in worktrees and nonstandard Git dir layouts. + +**Testing:** +- Reinstalled the hook locally with `./scripts/install-hooks.sh`; the installer backed up the previous differing hook before replacing it. +- Pushed the branch successfully through all 5 gates: + - Build: passed after one automatic retry following transient `Internal CLR error (0x80131506)` + - Architecture.Tests: ✅ 6/6 + - Unit.Tests: ✅ 59/59 + - Integration.Tests: ✅ 9/9 + - Push: allowed after all gates passed + +**Lessons learned:** +- Keep the committed hook and installer aligned by copying from a single source of truth instead of embedding hook bodies in the install script. +- Repo-specific automation copied from another project must be reconciled immediately; stale solution names and test project paths can silently invalidate the gate. +- Worktree-safe hook installation should use `git rev-parse --git-path hooks`, not a hardcoded `.git/hooks` path. + ### 2026-04-18 — PR #9 Review Fixes: Workflow Hardening **Issues addressed:** diff --git a/.squad/agents/ralph/history.md b/.squad/agents/ralph/history.md index 5286929a..5571a9b1 100644 --- a/.squad/agents/ralph/history.md +++ b/.squad/agents/ralph/history.md @@ -14,3 +14,10 @@ Agent Ralph initialized and ready for work. ## Learnings Initial setup complete. + +### 2026-04-18 — Pre-Push Gate Handoff Cleanup + +- Audited the `squad/prepush-gate` branch after Boromir's infra fix landed on PR #12. +- Corrected the squad record so Boromir's history matches the hook that actually shipped: `MyBlog.slnx`, Gate 3 = `Architecture.Tests` + `Unit.Tests`, Gate 4 = `Integration.Tests`, and the installer now copies from `.github/hooks/pre-push`. +- Cleaned stale inline comments in `.github/hooks/pre-push` so the comments match the current Gate 3 and Gate 4 behavior. +- Left unrelated local workspace changes untouched while updating squad-maintenance files. From 233b856c25e3efbf352acf5996b7112ff1a0c2bb Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:39:58 -0700 Subject: [PATCH 04/10] docs(contributing): consolidate the canonical contributor guide - move the full contributor setup and pre-push guidance into docs/CONTRIBUTING.md - reduce the root CONTRIBUTING.md to a GitHub-friendly pointer to the canonical doc - point the pre-push skill at docs/CONTRIBUTING.md to avoid stale duplicate guidance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .copilot/skills/pre-push-test-gate/SKILL.md | 2 +- CONTRIBUTING.md | 130 +---------- docs/CONTRIBUTING.md | 245 ++++++++------------ 3 files changed, 104 insertions(+), 273 deletions(-) mode change 100755 => 100644 docs/CONTRIBUTING.md diff --git a/.copilot/skills/pre-push-test-gate/SKILL.md b/.copilot/skills/pre-push-test-gate/SKILL.md index acf69662..b5085305 100644 --- a/.copilot/skills/pre-push-test-gate/SKILL.md +++ b/.copilot/skills/pre-push-test-gate/SKILL.md @@ -74,5 +74,5 @@ chmod +x .git/hooks/pre-push - **Hook source:** `.github/hooks/pre-push` - **Execution playbook:** `.squad/playbooks/pre-push-process.md` - **Build repair prompt:** `.github/prompts/build-repair.prompt.md` -- **Contributing guide:** `CONTRIBUTING.md` (Pre-Push Gates section) +- **Contributing guide:** `docs/CONTRIBUTING.md` (Pre-Push Gates section) - **Ceremonies:** `.squad/ceremonies.md` (Build Repair Check) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf34e6e4..e3d1ea13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,125 +1,15 @@ # Contributing to MyBlog -Thank you for your interest in contributing to MyBlog! This document provides guidelines for setting up your development environment and submitting contributions. +The canonical contributor guide lives at +[docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). -## Initial Setup +Use that document for: -### 1. Clone the Repository +- Environment setup +- Git hook installation +- Build and test workflow +- Branching and pull request guidance +- Code standards and supporting resources -```bash -git clone https://github.com/mpaulosky/MyBlog.git -cd MyBlog -``` - -### 2. Install Git Hooks - -**IMPORTANT:** After cloning the repository, run the hook installation script: - -```bash -./scripts/install-hooks.sh -``` - -This installs a **pre-push gate** that validates your code before it reaches GitHub. - -### What the Pre-Push Gate Does - -The pre-push hook automatically runs before every `git push` and enforces **5 gates**: - -| Gate | Name | Action | -|------|------|--------| -| **0** | Branch protection | Blocks direct pushes to `main` or `dev` | -| **1** | Untracked source files | Warns about untracked `.razor`/`.cs` files | -| **2** | Release build | `dotnet build MyBlog.slnx --configuration Release` | -| **3** | Unit/Arch tests | `tests/Architecture.Tests`, `tests/Unit.Tests` | -| **4** | Integration tests | `tests/Integration.Tests` (Docker required) | - -Gates 2–4 allow up to 3 attempts — the hook pauses between failures so you can fix and retry without re-running from scratch. - -### Bypassing the Gate (Emergency Only) - -In rare cases where you need to push despite build/test failures: - -```bash -git push --no-verify -``` - -⚠️ **Use sparingly** — This bypasses local validation. CI will still catch issues, but it's better to fix problems locally. - -## Development Workflow - -### Prerequisites - -- **.NET 10 SDK** — [Download](https://dotnet.microsoft.com/en-us/download) -- **Auth0 account** — See [AUTH0_SETUP.md](docs/AUTH0_SETUP.md) for configuration - -### Building and Testing - -```bash -# Restore dependencies -dotnet restore - -# Build the solution -dotnet build - -# Run all tests -dotnet test - -# Run the application (via Aspire AppHost) -cd src/AppHost -dotnet run -``` - -### Branch Strategy - -- **`main`** — Release-only branch, protected with strict rules -- **`dev`** — Primary development branch (default) -- **`squad/*`** — Feature branches for squad members and Copilot agents - -Create feature branches from `dev`: - -```bash -git checkout dev -git pull origin dev -git checkout -b squad/my-feature -``` - -### Pull Requests - -1. Create a PR from your `squad/*` branch to `dev` -2. Ensure all CI checks pass (build, tests, coverage) -3. Address code review feedback -4. Squash and merge once approved - -## Code Standards - -### .NET Conventions - -- Follow .NET naming conventions (PascalCase for types, camelCase for locals) -- Use C# 14 features (file-scoped namespaces, record types, pattern matching) -- Keep methods focused and testable - -### Testing - -- Write unit tests for new domain logic -- Maintain or improve code coverage (currently >90%) -- Use xUnit, FluentAssertions, and NSubstitute - -### Architecture Rules - -- Domain layer must not depend on Web layer -- Use repository pattern for data access -- Keep Blazor components focused (presentation only) - -## Resources - -- [ARCHITECTURE.md](docs/ARCHITECTURE.md) — Solution structure and design decisions -- [AUTH0_SETUP.md](docs/AUTH0_SETUP.md) — Auth0 configuration guide -- [README.md](README.md) — Project overview and getting started - -## Questions? - -Open an issue or reach out to @mpaulosky. - ---- - -**Remember:** Install git hooks with `./scripts/install-hooks.sh` after cloning! +Keeping the full guide in one location avoids drift between GitHub's root-level +`CONTRIBUTING.md` convention and the project's `/docs` documentation set. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md old mode 100755 new mode 100644 index 65c99cc2..e994ad4d --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,188 +1,129 @@ # Contributing to MyBlog -Thank you for your interest in contributing to MyBlog — a learning project for .NET development! - -## Table of Contents - -- [Code of Conduct](#code-of-conduct) -- [Before You Start](#before-you-start) -- [Quick Start](#quick-start) -- [Project Structure](#project-structure) -- [Design Decisions](#design-decisions) -- [How to Contribute](#how-to-contribute) -- [Testing Requirements](#testing-requirements) -- [Code Style](#code-style) - -## Welcome - -MyBlog is a **training project**. We welcome contributions that: -- Keep the project focused on learning (no production complexity) -- Follow clean architecture principles -- Include tests for all new functionality -- Have clear, descriptive commit messages - -## Code of Conduct - -We have adopted the Contributor Covenant. Contributors are expected to adhere to this code. Please report unwanted behavior to [@mpaulosky](mailto:matthew.paulosky@outlook.com). - -## Before You Start - -This is a learning project for practicing: -- .NET Aspire orchestration -- Blazor Server rendering -- Clean architecture (Domain/Web layer separation) -- Test-driven development with xUnit, FluentAssertions, NetArchTest.Rules - -**Key principle**: Keep it simple. We use an in-memory repository by design — no database, no authentication, no external services. - -## Quick Start - -1. **Fork** the repository: https://github.com/mpaulosky/MyBlog/fork -2. **Clone** your fork and create a feature branch: - ```bash - git clone https://github.com//MyBlog.git - cd MyBlog - git checkout -b feature/your-feature-name - ``` -3. **Make your changes** following the code style guidelines -4. **Add or update tests** (required for all code changes) -5. **Run the full test suite**: - ```bash - dotnet test - ``` -6. **Commit** with clear messages (present tense, reference issues if applicable): - ```bash - git commit -m "Add blog post filtering feature" - ``` -7. **Push** and open a Pull Request to `main` - -## Project Structure +Thank you for your interest in contributing to MyBlog! This document is the +canonical contributor guide for project setup, workflow, and pull request +expectations. -``` -MyBlog/ -├── src/ -│ ├── AppHost/ # .NET Aspire orchestration entry point -│ ├── Domain/ # BlogPost entity, repository interfaces, in-memory implementation -│ ├── ServiceDefaults/ # Aspire shared configuration (OpenTelemetry, health checks) -│ └── Web/ # Blazor Server application -│ └── Components/ -│ ├── Pages/BlogPosts/ # Index, Create, Edit Razor pages -│ ├── Pages/ # Home, Error, NotFound -│ ├── Shared/ # ConfirmDeleteDialog -│ └── Layout/ # MainLayout, NavMenu, ReconnectModal -├── tests/ -│ ├── Unit.Tests/ # Entity and repository unit tests -│ ├── Architecture.Tests/ # Layer dependency validation -│ └── Integration.Tests/ # Stubbed for future Aspire integration -├── docs/ # Documentation -├── Directory.Build.props # Centralized build settings -├── global.json # .NET SDK version lock -└── MyBlog.slnx # Solution file +## Initial Setup + +### 1. Clone the Repository + +```bash +git clone https://github.com/mpaulosky/MyBlog.git +cd MyBlog ``` -## Design Decisions +### 2. Install Git Hooks -This project adheres to these core principles: +**Important:** After cloning the repository, run the hook installation script: -1. **Blazor Server** for interactive server rendering — simplest way to learn Aspire + dynamic UI together -2. **In-memory repository only** — training project, no database setup required -3. **.NET Aspire orchestration** — learn service composition and health checks -4. **Clean architecture** — Domain and Web layers clearly separated -5. **Repository pattern** — `IBlogPostRepository` interface with in-memory implementation -6. **Repository naming** (`MyBlog` context) vs project names** — `AppHost`, `Domain`, `Web` have no `MyBlog.` prefix (intentional: repo name provides context) +```bash +./scripts/install-hooks.sh +``` -If you have architectural suggestions, please open an issue to discuss first. +This installs a **pre-push gate** that validates your code before it reaches +GitHub. -## How to Contribute +### What the Pre-Push Gate Does -### Report a Bug +The pre-push hook automatically runs before every `git push` and enforces +**5 gates**: -[Create an issue](https://github.com/mpaulosky/MyBlog/issues): -- Use the **Bug** label -- Include steps to reproduce, expected behavior, and actual behavior -- Attach screenshots if helpful +| Gate | Name | Action | +|------|------|--------| +| **0** | Branch protection | Blocks direct pushes to `main` or `dev` | +| **1** | Untracked source files | Warns about untracked `.razor`/`.cs` files | +| **2** | Release build | `dotnet build MyBlog.slnx --configuration Release` | +| **3** | Unit/Arch tests | `tests/Architecture.Tests`, `tests/Unit.Tests` | +| **4** | Integration tests | `tests/Integration.Tests` (Docker required) | -### Suggest an Enhancement +Gates 2-4 allow up to 3 attempts. The hook pauses between failures so you can +fix and retry without restarting the whole push. -[Create an issue](https://github.com/mpaulosky/MyBlog/issues): -- Use the **Enhancement** label -- Explain the use case and why it aligns with the project's learning goals +### Bypassing the Gate (Emergency Only) -### Write Code +In rare cases where you need to push despite build or test failures: -1. Link your work to an open issue (create one if needed) -2. Follow the [Testing Requirements](#testing-requirements) -3. Follow the [Code Style](#code-style) guidelines -4. Keep changes focused and clear +```bash +git push --no-verify +``` -### Write Documentation +**Use this sparingly.** It bypasses local validation. CI will still catch +issues, but fixing them locally is preferred. -Help keep `/docs` and [README.md](../README.md) accurate: -- Update docs if you change architecture or features -- Add architecture decision records (ADRs) for significant changes -- Link documentation from the main README +## Development Workflow -## Testing Requirements +### Prerequisites -**All new code must include tests. Pull requests without tests will be delayed.** +- **.NET 10 SDK** — [Download](https://dotnet.microsoft.com/en-us/download) +- **Docker** — Required for `tests/Integration.Tests` +- **Auth0 account** — See [AUTH0_SETUP.md](AUTH0_SETUP.md) for configuration -### Unit Tests +### Building and Testing -- Add tests in `tests/Unit.Tests/` -- Use **xUnit** for test framework -- Use **FluentAssertions** for assertions (e.g., `result.Should().BeTrue()`) -- Use **NSubstitute** for mocks -- Follow the **AAA pattern**: Arrange / Act / Assert +```bash +# Restore dependencies +dotnet restore MyBlog.slnx -Example: -```csharp -[Fact] -public void Create_WithValidTitle_ReturnsNewBlogPost() -{ - // Arrange - var title = "My First Post"; - var content = "Content here"; - var author = "Me"; +# Build the solution +dotnet build MyBlog.slnx --configuration Release - // Act - var post = BlogPost.Create(title, content, author); +# Run all tests +dotnet test MyBlog.slnx --configuration Release - // Assert - post.Title.Should().Be(title); - post.IsPublished.Should().BeFalse(); -} +# Run the application (via Aspire AppHost) +cd src/AppHost +dotnet run ``` -### Architecture Tests +### Branch Strategy -- Use **NetArchTest.Rules** to verify layer dependencies -- Ensure Domain does not reference Web -- Ensure tests don't reference implementation details unnecessarily +- **`main`** — Release-only branch, protected with strict rules +- **`dev`** — Primary development branch +- **`squad/*`** — Feature branches for squad members and Copilot agents + +Create feature branches from `dev`: -Run all tests before pushing: ```bash -dotnet test +git checkout dev +git pull origin dev +git checkout -b squad/my-feature ``` -## Code Style +### Pull Requests + +1. Create a PR from your `squad/*` branch to `dev` +2. Ensure all CI checks pass (build, tests, coverage) +3. Address code review feedback +4. Squash and merge once approved + +## Code Standards + +### .NET Conventions + +- Follow .NET naming conventions (PascalCase for types, camelCase for locals) +- Use C# 14 features where appropriate +- Keep methods focused and testable + +### Testing + +- Write unit tests for new domain logic +- Maintain or improve code coverage +- Use xUnit, FluentAssertions, and NSubstitute + +### Architecture Rules -- **Namespaces**: Follow the RootNamespace pattern (e.g., `MyBlog.Domain`, `MyBlog.Web`) -- **Formatting**: Follow C# conventions (use `.editorconfig` if configured) -- **Comments**: Only comment complex logic; clean code is self-documenting -- **Methods**: Prefer small, focused methods with clear names -- **Async/Await**: Use `async` for I/O operations; use `.Result` is discouraged -- **Short project names**: No `MyBlog.` prefix on folder/project names; repo context provides clarity +- Domain layer must not depend on Web layer +- Use repository and feature-slice patterns already present in the solution +- Keep Blazor components focused on presentation concerns -## PR Review Checklist +## Resources -Before opening a PR: -- [ ] All tests pass: `dotnet test` -- [ ] Code follows style guidelines -- [ ] New features have unit tests -- [ ] Documentation updated if applicable -- [ ] Commit messages are clear and present-tense -- [ ] No unrelated changes included +- [ARCHITECTURE.md](ARCHITECTURE.md) — Solution structure and design decisions +- [AUTH0_SETUP.md](AUTH0_SETUP.md) — Auth0 configuration guide +- [README.md](../README.md) — Project overview and getting started ---- +## Questions? -Thank you for contributing to MyBlog! Questions? Open an issue or reach out to [@mpaulosky](https://github.com/mpaulosky). +Open an issue or reach out to +[@mpaulosky](https://github.com/mpaulosky). From 655ba71284bebbc933ab7589a9166c557b88526a Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:40:56 -0700 Subject: [PATCH 05/10] fix(hooks): address follow-up PR review comments - normalize the hooks path returned by git rev-parse --git-path hooks so installs work from outside the repo root - create the hooks directory before copying the committed pre-push hook into place - tighten the build retry log line so it matches the behavior actually performed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/hooks/pre-push | 2 +- scripts/install-hooks.sh | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push index 58465f9c..128bb4cc 100755 --- a/.github/hooks/pre-push +++ b/.github/hooks/pre-push @@ -47,7 +47,7 @@ while [[ $BUILD_ATTEMPT -lt $MAX_ATTEMPTS ]]; do BUILD_EXIT=$? if [[ $BUILD_EXIT -ne 0 ]]; then - echo -e " ↻ Auto-retrying build once (clearing incremental state)..." + echo -e " ↻ Auto-retrying build once..." dotnet build MyBlog.slnx --configuration Release BUILD_EXIT=$? fi diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index 629b23f0..8479759b 100755 --- a/scripts/install-hooks.sh +++ b/scripts/install-hooks.sh @@ -6,7 +6,15 @@ set -e REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SOURCE="$REPO_ROOT/.github/hooks/pre-push" -HOOKS_DIR="$(git -C "$REPO_ROOT" rev-parse --git-path hooks)" +HOOKS_DIR_RAW="$(git -C "$REPO_ROOT" rev-parse --git-path hooks)" +case "$HOOKS_DIR_RAW" in + /*) + HOOKS_DIR="$HOOKS_DIR_RAW" + ;; + *) + HOOKS_DIR="$REPO_ROOT/$HOOKS_DIR_RAW" + ;; +esac DEST="$HOOKS_DIR/pre-push" if [[ ! -f "$SOURCE" ]]; then @@ -14,6 +22,8 @@ if [[ ! -f "$SOURCE" ]]; then exit 1 fi +mkdir -p "$HOOKS_DIR" + if [[ -f "$DEST" ]] && cmp -s "$SOURCE" "$DEST"; then echo "✅ Pre-push hook is already up-to-date. Nothing to do." exit 0 From 5cda23990b888459274db5388b2d40ec7679fe34 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:58:30 -0700 Subject: [PATCH 06/10] docs(squad): gandalf security review of PR #11 and PR #12 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .squad/agents/gandalf/history.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.squad/agents/gandalf/history.md b/.squad/agents/gandalf/history.md index d91d76c4..353cac6a 100644 --- a/.squad/agents/gandalf/history.md +++ b/.squad/agents/gandalf/history.md @@ -25,3 +25,23 @@ 7. **[CLEAN] Auth middleware order** — UseAuthentication → UseAuthorization → UseAntiforgery is correct. 8. **[CLEAN] ManageRoles and Profile authorization** — Both pages correctly gated with `[Authorize(Roles = "Admin")]` and `[Authorize]` respectively. + +### PR #11 & #12 Security Review — 2026-04-18 + +**PR #11** — `squad/cleanup-uncommitted-changes` → `dev` (3 files: boromir history, ManageRoles.razor, tailwind.css) + +**Verdict:** NEEDS_HUMAN_DECISION + +- **[CLEAN] ManageRoles.razor** — Removed redundant `@using MyBlog.Web.Features.UserManagement` (already in `_Imports.razor` per Decision #1). `[Authorize(Roles = "Admin")]` gate remains intact. No security impact. +- **[CLEAN] No secrets** — No credentials or tokens in any changed file. +- **[INFO] Non-minified tailwind.css** — Committed CSS expanded from 1 minified line to 1918 pretty-printed lines with nested CSS syntax. Not a security issue, but the nested `&:` syntax may have browser compatibility implications. Needs Legolas (frontend) to confirm this is intentional and not a build artifact mismatch. + +**PR #12** — `squad/prepush-gate` → `dev` (8 files: pre-push hook, install-hooks.sh, CONTRIBUTING.md, SKILL.md, PR template, squad docs) + +**Verdict:** APPROVE_READY + +- **[CLEAN] Shell script security** — `install-hooks.sh` and `.github/hooks/pre-push` use proper variable quoting, no eval/exec of user input, `set -e` / `set -uo pipefail`, and no injection vectors. +- **[CLEAN] No secrets** — No credentials committed. PR template checklist correctly includes secrets check. +- **[LOW] Shebang portability** — `install-hooks.sh:1` uses `#!/bin/bash`; pre-push hook uses `#!/usr/bin/env bash`. Minor inconsistency, not a security issue. +- **[LOW] Stale Azurite reference** — `pre-push:116` mentions Azurite but only MongoDB Testcontainers are used. Misleading, not dangerous. +- **[LOW] Dead playbook link** — `SKILL.md:17,75` references `.squad/playbooks/pre-push-process.md` which does not exist. From d59b493588605744895548b976e9785c51b5a077 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:03:19 -0700 Subject: [PATCH 07/10] Fix PR #12 review follow-ups - use env bash in install-hooks - replace dead pre-push skill reference - align Gate 4 Docker messaging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .copilot/skills/pre-push-test-gate/SKILL.md | 5 ++--- .github/hooks/pre-push | 2 +- .../decisions/inbox/boromir-pr12-followups.md | 21 +++++++++++++++++++ scripts/install-hooks.sh | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .squad/decisions/inbox/boromir-pr12-followups.md diff --git a/.copilot/skills/pre-push-test-gate/SKILL.md b/.copilot/skills/pre-push-test-gate/SKILL.md index b5085305..6dade752 100644 --- a/.copilot/skills/pre-push-test-gate/SKILL.md +++ b/.copilot/skills/pre-push-test-gate/SKILL.md @@ -14,7 +14,7 @@ description: > The pre-push hook (`.github/hooks/pre-push`) enforces **5 gates** that mirror CI. It runs automatically on every `git push` and blocks the push if any gate fails. -> 📋 **For the step-by-step execution playbook, see:** `.squad/playbooks/pre-push-process.md` +> 📋 **For setup and day-to-day usage, see:** `docs/CONTRIBUTING.md` ### The 5 Gates @@ -72,7 +72,6 @@ chmod +x .git/hooks/pre-push ### Related Documents - **Hook source:** `.github/hooks/pre-push` -- **Execution playbook:** `.squad/playbooks/pre-push-process.md` +- **Contributor guide:** `docs/CONTRIBUTING.md` (Initial Setup and Pre-Push Gate sections) - **Build repair prompt:** `.github/prompts/build-repair.prompt.md` -- **Contributing guide:** `docs/CONTRIBUTING.md` (Pre-Push Gates section) - **Ceremonies:** `.squad/ceremonies.md` (Build Repair Check) diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push index 128bb4cc..d551e57d 100755 --- a/.github/hooks/pre-push +++ b/.github/hooks/pre-push @@ -113,7 +113,7 @@ INTEGRATION_PROJECTS=( echo -e "\n${CYAN}🐋 Checking Docker availability for integration tests...${RESET}" if ! docker info &>/dev/null; then echo -e "${RED}❌ Docker daemon is not running.${RESET}" - echo -e "${YELLOW} Integration tests require Docker (Testcontainers: mongo:7.0, Azurite).${RESET}" + echo -e "${YELLOW} Integration tests require Docker for the MongoDB Testcontainers dependency.${RESET}" echo -e "${RED} Start Docker and re-push. Skipping integration tests is not allowed.${RESET}" exit 1 else diff --git a/.squad/decisions/inbox/boromir-pr12-followups.md b/.squad/decisions/inbox/boromir-pr12-followups.md new file mode 100644 index 00000000..51ce4695 --- /dev/null +++ b/.squad/decisions/inbox/boromir-pr12-followups.md @@ -0,0 +1,21 @@ +# PR #12 Follow-ups: Pre-Push Gate References + +**Date:** 2026-04-19 +**Author:** Boromir (DevOps Engineer) +**Status:** ✅ Implemented +**PR:** #12 + +## Decision + +The pre-push skill should point contributors to `docs/CONTRIBUTING.md` as the +authoritative setup and usage guide instead of referencing a non-existent +`.squad/playbooks/pre-push-process.md` playbook. + +## Rationale + +- `docs/CONTRIBUTING.md` already documents hook installation and the five + pre-push gates. +- Reusing the canonical contributor guide avoids duplicating operational + instructions in a second document. +- Removing the dead `.squad/playbooks/...` reference keeps the skill accurate + for new contributors and agents. diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index 8479759b..a4551a89 100755 --- a/scripts/install-hooks.sh +++ b/scripts/install-hooks.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Installs the pre-push gate hook from .github/hooks/pre-push into the local Git hooks directory. # Safe to re-run: skips if already up-to-date, backs up any differing hook before overwriting. From 810498e2e4a6598d5b36040c94d29f3e0fa63a74 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:13:15 -0700 Subject: [PATCH 08/10] squad: consolidate decisions, update team history, establish governance structures - Merged decision inbox files into .squad/decisions/decisions.md (6 new decisions) - Deleted inbox files after consolidation - Created orchestration logs for Ralph, Gandalf, Legolas, Boromir - Created session log for completed PR #11-#12 merge cycle - Updated identity/now.md to reflect post-coordination state - Appended final summary notes to agent history files Decision highlights: - dev/main branch strategy with GitVersion (SemVer) versioning - Pre-push gate mandatory validation (build + test) - Casting infrastructure (Phase 1) for programmatic agent lifecycle - Auth0 secrets via user-secrets (not appsettings) - PR #11 CSS artifact approved (Tailwind v4.2.2) - PR #12 pre-push gate references consolidated Decisions consolidated by Scribe per post-coordination workflow. Board cleared: 0 open issues, 0 open squad PRs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .squad/agents/boromir/history.md | 8 + .squad/agents/gandalf/history.md | 8 + .squad/agents/legolas/history.md | 45 ++ .squad/agents/ralph/history.md | 16 + .squad/casting/history.json | 25 + .squad/casting/policy.json | 24 + .squad/casting/registry.json | 102 ++++ .squad/decisions/decisions.md | 551 ++++++++++++++++++ .../decisions/inbox/boromir-pr12-followups.md | 21 - .../inbox/boromir-pr9-review-fixes.md | 1 - .squad/identity/now.md | 14 +- 11 files changed, 790 insertions(+), 25 deletions(-) create mode 100644 .squad/casting/history.json create mode 100644 .squad/casting/policy.json create mode 100644 .squad/casting/registry.json delete mode 100644 .squad/decisions/inbox/boromir-pr12-followups.md delete mode 100644 .squad/decisions/inbox/boromir-pr9-review-fixes.md diff --git a/.squad/agents/boromir/history.md b/.squad/agents/boromir/history.md index 5681eeda..6f0af0a7 100644 --- a/.squad/agents/boromir/history.md +++ b/.squad/agents/boromir/history.md @@ -132,3 +132,11 @@ - Monitor first workflow run on PR #5 to verify all steps execute successfully - May need to adjust coverage thresholds or exclusions based on actual coverage data - Consider adding caching for Docker images if Testcontainers startup becomes slow + +### 2026-04-18 — PR #12 Follow-up & Pre-Push Gate (Final Summary) + +- Addressed pre-push gate follow-up review comments: fixed dead playbook reference +- Updated SKILL.md and PR template to point to `docs/CONTRIBUTING.md` as authoritative guide +- PR #12 merged successfully with all green checks passing +- Decision on gate references documented in `.squad/decisions/decisions.md` +- Orchestration log created in `.squad/orchestration-log/2026-04-18T17-05-49-boromir.md` diff --git a/.squad/agents/gandalf/history.md b/.squad/agents/gandalf/history.md index 353cac6a..7c428542 100644 --- a/.squad/agents/gandalf/history.md +++ b/.squad/agents/gandalf/history.md @@ -45,3 +45,11 @@ - **[LOW] Shebang portability** — `install-hooks.sh:1` uses `#!/bin/bash`; pre-push hook uses `#!/usr/bin/env bash`. Minor inconsistency, not a security issue. - **[LOW] Stale Azurite reference** — `pre-push:116` mentions Azurite but only MongoDB Testcontainers are used. Misleading, not dangerous. - **[LOW] Dead playbook link** — `SKILL.md:17,75` references `.squad/playbooks/pre-push-process.md` which does not exist. + +### 2026-04-18 — PR #11 & #12 Security Review (Final Summary) + +- Completed security reviews for PR #11 (cleanup-uncommitted-changes) and PR #12 (prepush-gate) +- PR #11 verdict: NEEDS_HUMAN_DECISION (pending Legolas CSS confirmation); approved from security +- PR #12 verdict: APPROVE_READY (shell security clean, minor non-blocking issues) +- All findings recorded in session history; decisions consolidated by Scribe +- Orchestration log created in `.squad/orchestration-log/2026-04-18T17-05-49-gandalf.md` diff --git a/.squad/agents/legolas/history.md b/.squad/agents/legolas/history.md index fd1bee68..8bf11754 100644 --- a/.squad/agents/legolas/history.md +++ b/.squad/agents/legolas/history.md @@ -310,3 +310,48 @@ bg-primary-hover # Old custom hover state (now use dark: variant) **Filed:** `.squad/decisions/inbox/legolas-remove-weather-counter.md` + +--- + +## 2026-04-18 — PR #11 CSS Artifact Review + +### Verdict +**APPROVE_READY** — The large `src/Web/wwwroot/css/tailwind.css` expansion (1918 lines) is a legitimate compiled artifact. + +### Analysis + +**What PR #11 Contains:** +- `src/Web/wwwroot/css/tailwind.css` — expanded from 2 lines (minified) to 1918 lines (pretty-printed) +- `.squad/agents/boromir/history.md` — updated with CI/CD work docs (not my domain) +- `src/Web/Features/UserManagement/ManageRoles.razor` — one line removed (redundant `@using` cleanup, consistent with prior work) + +**Why the CSS expansion is correct:** +1. **app.css source is identical** — Tailwind v4 CSS-first configuration (`@import "tailwindcss"`, `@source` directives, `@theme inline`) is unchanged on both dev and PR branch +2. **Commit context confirms intent** — PR title "commit leftover uncommitted changes from cicd-phase3-4" indicates this branch stalled and `npm run tw:build` was never executed there; now it's being committed as a recovery +3. **Output matches Tailwind v4.2.2 format** — header `/*! tailwindcss v4.2.2 | MIT License */` and proper `@layer` structure +4. **Pretty-printing is idiomatic** — Tailwind v4 output is pretty-printed by default; development environment, not minified for production +5. **No stale artifacts** — the CSS is fully generated from current app.css source (verified by comparing Tailwind token variables and component layer styles) + +**Consistent with project history:** +- Per my earlier entries: app.css was migrated to Tailwind v4 CSS-first in 2025-04-17 +- MSBuild + npm integration documented in `.squad/decisions.md` (Boromir's CI conventions) +- All semantic tokens and component classes resolve from app.css correctly + +**Secondary file (ManageRoles.razor):** +- Removes `@using MyBlog.Web.Features.UserManagement` — consistent with consolidation of imports into `_Imports.razor` (documented in my 2025-01-29 history) +- Expected cleanup + +### Decision +No concerns. This is intentional recovery of uncommitted CSS from a stalled branch. Merge approved from Blazor/CSS perspective. + +**Filed:** `.squad/decisions/inbox/legolas-pr11-css-check.md` + + +### 2026-04-18 — PR #11 CSS Artifact Validation (Final Summary) + +- Validated Tailwind CSS expansion in PR #11 as intentional v4.2.2 compiled output +- Confirmed no design token regressions; artifact semantically valid +- Verdict: APPROVE_READY; no blocker issues +- Secondary fix: removed redundant @using from ManageRoles.razor +- Decision documented in `.squad/decisions/decisions.md` +- Orchestration log created in `.squad/orchestration-log/2026-04-18T17-05-49-legolas.md` diff --git a/.squad/agents/ralph/history.md b/.squad/agents/ralph/history.md index 5571a9b1..7ae8a22b 100644 --- a/.squad/agents/ralph/history.md +++ b/.squad/agents/ralph/history.md @@ -21,3 +21,19 @@ Initial setup complete. - Corrected the squad record so Boromir's history matches the hook that actually shipped: `MyBlog.slnx`, Gate 3 = `Architecture.Tests` + `Unit.Tests`, Gate 4 = `Integration.Tests`, and the installer now copies from `.github/hooks/pre-push`. - Cleaned stale inline comments in `.github/hooks/pre-push` so the comments match the current Gate 3 and Gate 4 behavior. - Left unrelated local workspace changes untouched while updating squad-maintenance files. + +### 2026-04-18 — Casting Migration + +- Migrated `.squad/team.md` roster into `.squad/casting/` infrastructure (phase 1). +- Created `policy.json` with sensible defaults for 11-agent team: `max_concurrent_agents: 5`, `default_timeout_minutes: 120`, auto-escalation enabled. +- Created `registry.json` with all 12 agents marked `legacy_named: true` and `status: "active"` — no renaming, all charter paths point to existing directories. +- Created `history.json` with initial migration snapshot documenting the source, destination, and audit trail. +- Recorded casting decisions in `.squad/decisions/inbox/ralph-casting-migration.md` for team review and future maintenance guidance. +- Coordinator can now manage agent lifecycle, timeouts, and governance programmatically; team changes can be tracked over time. + +### 2026-04-18 — Casting Migration (Final Summary) + +- Completed casting infrastructure migration (Phase 1): created `.squad/casting/policy.json`, `registry.json`, `history.json` +- Decisions consolidated into `.squad/decisions/decisions.md` by Scribe +- Orchestration log created in `.squad/orchestration-log/2026-04-18T17-05-49-ralph.md` +- Ready for Phase 2 (agent spawn/timeout automation) diff --git a/.squad/casting/history.json b/.squad/casting/history.json new file mode 100644 index 00000000..ba41d065 --- /dev/null +++ b/.squad/casting/history.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "migrations": [ + { + "timestamp": "2026-04-18T00:00:00Z", + "event": "casting_migration_v1", + "agent": "Ralph", + "description": "Initial casting migration for squadified repo without casting directory", + "details": { + "source": ".squad/team.md", + "destination": ".squad/casting/", + "files_created": [ + "policy.json", + "registry.json", + "history.json" + ], + "agents_migrated": 12, + "team_size": 11, + "all_agents_marked_legacy_named": true, + "all_agents_marked_active": true, + "decisions_recorded": true + } + } + ] +} diff --git a/.squad/casting/policy.json b/.squad/casting/policy.json new file mode 100644 index 00000000..765b49e1 --- /dev/null +++ b/.squad/casting/policy.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "name": "MyBlog Squad Casting Policy", + "created": "2026-04-18T00:00:00Z", + "defaults": { + "max_concurrent_agents": 5, + "default_timeout_minutes": 120, + "retry_policy": { + "max_retries": 2, + "backoff_seconds": 30 + }, + "routing": { + "auto_escalate_blockers": true, + "lead_approval_required": false, + "parallel_spawning": true + } + }, + "team_size": 11, + "guidelines": { + "note": "This policy reflects the current MyBlog team. Adjust max_concurrent_agents and routing rules as team size changes.", + "agent_count_with_coordinator": 11, + "agent_count_without_coordinator": 10 + } +} diff --git a/.squad/casting/registry.json b/.squad/casting/registry.json new file mode 100644 index 00000000..595e8d62 --- /dev/null +++ b/.squad/casting/registry.json @@ -0,0 +1,102 @@ +{ + "version": 1, + "created": "2026-04-18T00:00:00Z", + "agents": [ + { + "name": "Squad", + "role": "Coordinator", + "legacy_named": true, + "status": "active", + "charter_path": null, + "notes": "Routes work, enforces handoffs and reviewer gates." + }, + { + "name": "Aragorn", + "role": "Lead / Architect", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/aragorn/charter.md", + "notes": "Solution design, ADRs, PR gates" + }, + { + "name": "Sam", + "role": "Backend / .NET", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/sam/charter.md", + "notes": "Domain model, AppHost, APIs" + }, + { + "name": "Legolas", + "role": "Frontend / Blazor", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/legolas/charter.md", + "notes": "Blazor Server UI, components" + }, + { + "name": "Gimli", + "role": "Tester", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/gimli/charter.md", + "notes": "Unit, Architecture & Integration tests" + }, + { + "name": "Boromir", + "role": "DevOps / Infra", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/boromir/charter.md", + "notes": "CI/CD, Aspire config, Docker" + }, + { + "name": "Gandalf", + "role": "Reviewer", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/gandalf/charter.md", + "notes": "Code review gate, quality" + }, + { + "name": "Frodo", + "role": "Security", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/frodo/charter.md", + "notes": "Auth, secrets, vulnerabilities" + }, + { + "name": "Pippin", + "role": "Docs", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/pippin/charter.md", + "notes": "Summaries, ADRs, changelogs" + }, + { + "name": "Bilbo", + "role": "Research", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/bilbo/charter.md", + "notes": "Spikes, POCs, investigations" + }, + { + "name": "Ralph", + "role": "Meta", + "legacy_named": true, + "status": "active", + "charter_path": ".squad/agents/ralph/charter.md", + "notes": "Squad maintenance" + }, + { + "name": "Scribe", + "role": "Scribe", + "legacy_named": true, + "status": "active", + "charter_path": null, + "notes": "Logs and records" + } + ] +} diff --git a/.squad/decisions/decisions.md b/.squad/decisions/decisions.md index 47d64d8b..f5558131 100644 --- a/.squad/decisions/decisions.md +++ b/.squad/decisions/decisions.md @@ -1493,3 +1493,554 @@ dotnet user-secrets set "Auth0:ClientSecret" "" --project sr ## Validation - Build succeeded with 0 errors, 0 warnings - dotnet build src/Web/Web.csproj completed successfully + +--- + +# Decision: dev/main Branch Strategy with GitVersion + +**Date:** 2026-04-18 +**Author:** Aragorn (Lead Developer), Boromir (DevOps), Copilot +**Status:** ✅ Implemented (Phase 1-2 complete) + +## Context + +The team needed a branching model to support coordinated squad work, release control, and hotfix safety. The decision establishes a clear separation between development (`dev`) and production (`main`) branches with supporting governance. + +## Decision + +### 1. Branch Strategy + +- **`dev`** — Primary integration branch where all squad work targets + - Default branch for `clone` and new PRs + - All `squad/*` branches open PRs to `dev` + - CI required; branch protection moderate + - Release candidate state after testing + +- **`main`** — Release-only branch; receives code from `dev` via explicit release PRs + - No direct commits (all via PR) + - Strict branch protection (1 approval, all CI checks required) + - Only accepts PRs from `dev` or `hotfix/*` branches + - Triggers production deployment workflows + - Tagged with semantic version on each release + +- **`hotfix/*`** — Critical bug fixes that bypass `dev` + - Branch from `main`, target `main` directly + - Require 1 approval (+ Gandalf security review for security-critical hotfixes) + - Must be immediately backported to `dev` after merge (via cherry-pick or backport branch) + - Tagged with patch version increment + +### 2. CI/CD Triggers + +Updated `.github/workflows/ci.yml` triggers: +```yaml +on: + pull_request: + branches: [dev, main] + push: + branches: [dev, main] +``` + +This ensures: +- All PRs to `dev` or `main` undergo full CI validation +- Post-merge pushes to `dev` are validated +- Post-merge pushes to `main` can trigger deployment workflows + +### 3. Release Process + +Release workflow (`dev` → `main`): +1. Create release PR from `dev` to `main` with title `[RELEASE] vX.Y.Z - Description` +2. Checklist in PR: CI passed, changelog updated, version bumped, breaking changes documented +3. Require 1 approval +4. Squash merge to `main` (keeps main history clean) +5. Tag release: `git tag -a vX.Y.Z -m "Release vX.Y.Z: Description"` +6. Fast-forward `dev` to `main`: `git checkout dev && git merge --ff-only main && git push origin dev` + +### 4. Versioning: GitVersion (SemVer) + +GitVersion calculates semantic versions automatically: +- **`main` branch** → `MAJOR.MINOR.PATCH` (e.g., `1.3.0`) +- **`dev` branch** → Pre-release version (e.g., `1.4.0-alpha.1`) +- **`squad/*` branches** → Label-stamped pre-release (e.g., `1.4.0-pr.42`) + +CI runs GitVersion before build to stamp assembly versions: +```yaml +- name: GitVersion + uses: gittools/actions/gitversion/execute@v1 + with: + versionSpec: '6.x' + updateAssemblyInfo: false + +- name: Stamp versions + run: | + dotnet build MyBlog.slnx -c Release \ + /p:Version=${{ steps.gitversion.outputs.nuGetVersion }} \ + /p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} \ + /p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} +``` + +**Key decision:** GitVersion.yml already exists at repo root with full branch config. Leverage it without additional maintenance overhead. + +### 5. Hotfix Backport Process + +**Automated reminder** via `.github/workflows/hotfix-backport-reminder.yml`: +- Triggers on hotfix merges to `main` +- Auto-comments with cherry-pick instructions +- Prevents dev/main drift + +**Manual process** if reminder fails: +```bash +git checkout dev +git pull origin dev +git merge main # Fast-forward (preferred) +# OR +git cherry-pick {hotfix-commit-sha} # If dev has diverged +git push origin dev +``` + +**Critical rule:** Never allow hotfix to exist only on `main`. Always backport immediately. + +### 6. Branch Protection Rules + +**`main` branch (strictest):** +- Require pull request reviews: 1 minimum +- Dismiss stale reviews on new commits: enabled +- Require status checks: enabled (CI workflow) +- Require branches up-to-date: enabled +- Require conversation resolution: enabled +- Restrict who can push: (administrators must follow PR process) +- Allow force pushes: disabled +- Allow deletions: disabled + +**`dev` branch (moderate):** +- Require status checks: enabled (CI workflow) +- Require branches up-to-date: enabled +- Require conversation resolution: disabled (allow WIP work) +- Allow force pushes: disabled +- Allow deletions: disabled +- (Optional) 1 approval for `squad/*` branches to `dev` + +### 7. PR Workflows + +**Regular squad/feature branch PR:** +``` +squad/my-feature → PR → dev (target dev as default base) +``` + +**Release PR:** +``` +dev → PR → main (only PR type allowed to main; title: [RELEASE] vX.Y.Z - ...) +``` + +**Hotfix PR:** +``` +hotfix/critical-bug → PR → main (only other PR type to main; requires 1 approval) +``` + +### 8. AI Agent Compatibility + +**`.squad/routing.md` and `squad-issue-assign.yml` updated:** +- Default base branch hardcoded to `dev` +- When `@copilot` is assigned an issue, the created branch targets `dev` +- Coordinator can spawn multiple agents with confidence they all target `dev` + +## Rationale + +### Why Separate dev and main? + +1. **Release control** — Explicit `dev` → `main` PR creates a checkpoint for testing and review +2. **Production safety** — `main` is always release-ready; no partial/broken changes +3. **Developer velocity** — `dev` is fast-moving; squad agents can ship frequently without affecting production +4. **Hotfix safety** — Critical fixes bypass `dev` for speed, but must be backported to avoid regression +5. **Industry standard** — Proven pattern (Git Flow variant) used by large teams + +### Why GitVersion (SemVer)? + +1. **Automatic versioning** — No manual version bumps; derived from git history +2. **Semantic meaning** — MAJOR/MINOR/PATCH signals API compatibility to users +3. **Configuration already exists** — GitVersion.yml at repo root reduces setup overhead +4. **CI integration** — Stamps versions into assemblies automatically +5. **Clear release hygiene** — Version tags linked to git history for reproducibility + +### Why 1 Approval for Hotfixes? + +1. **Speed** — Critical bugs need fast turnaround; 2 approvals add delay +2. **Gandalf review** — Security-critical hotfixes can tag Gandalf for security review +3. **Main already protected** — All code must pass CI before merge anyway +4. **Backport requirement** — Immediate backport to `dev` catches issues quickly + +## Implementation Status + +### ✅ Complete (Phase 1-2) + +- Created `dev` branch from `main` (identical starting state) +- Applied branch protection rules to both branches +- Updated CI workflow triggers to include `dev` branch +- Updated `squad-issue-assign.yml` to hardcode `dev` as base branch +- Set `dev` as default branch in GitHub settings +- Recorded GitVersion configuration in CI pipeline +- Hotfix backport reminder workflow created (not yet tested in production hotfix scenario) + +### 🟡 Testing (Phase 3) + +- First `dev` → `main` release PR cycle (verify CI triggers, merge behavior) +- First hotfix scenario (verify backport automation) +- Parallel test execution performance (squad-test.yml with GitVersion) + +### 📋 Future Maintenance + +1. **Monitor first release cycle** — Verify squash merge behavior and tag creation +2. **If hotfix happens** — Validate backport reminder triggers and process +3. **Adjust CI/CD** if runner time becomes an issue (parallel tests may need optimization) +4. **Document** in contributor guide: "All squad branches target `dev`; only release PRs target `main`" + +## Decisions Deferred + +- Release PR auto-creation (squad-promote.yml) — manual release PRs for now +- Changelog automation — capture in future sprint +- Release notes on GitHub Release tag — nice-to-have, not MVP + +## Related Decisions + +- **Pre-push gate** (.squad/decisions.md section below) — Validates code locally before push +- **User secrets** (.squad/decisions.md section below) — Protects Auth0 credentials from commits +- **Casting infrastructure** (.squad/decisions.md section below) — Enables programmatic agent lifecycle management + +--- + +# Decision: Pre-Push Gate — Build and Test Validation + +**Date:** 2026-04-18 +**Author:** Boromir (DevOps Engineer) +**Status:** ✅ Implemented +**PR:** #12 + +## Context + +Without local validation, developers push broken code to `dev`, triggering CI failures and blocking other team members. The pre-push gate provides immediate feedback (~7 seconds) and reduces CI noise. + +## Decision + +All developers and squad agents must install a pre-push git hook that validates code before pushing to GitHub. + +### Hook Installation + +After cloning or creating a new worktree: + +```bash +./scripts/install-hooks.sh +``` + +This installs `.git/hooks/pre-push` which runs before every `git push`. + +### Hook Validation Steps + +The pre-push hook executes two sequential gates: + +1. **Gate 1: Build** — `dotnet build MyBlog.slnx --no-incremental -c Release` + - Fails on compilation errors or warnings treated as errors + +2. **Gate 2: Tests** — `dotnet test MyBlog.slnx --no-build -c Release` + - Runs Architecture.Tests, Unit.Tests, and Integration.Tests + - All 3 suites must pass (74 tests minimum) + - Fails on any test failure + +### CI Skip + +When `CI=true` environment variable is set, the hook exits immediately (CI environment has already validated). + +### Emergency Bypass + +In rare cases, bypass validation: + +```bash +git push --no-verify +``` + +⚠️ Use sparingly — CI will still catch issues, but local validation is faster. + +### Implementation Details + +- `scripts/install-hooks.sh` — Committed source of truth; installs the hook +- `.git/hooks/pre-push` — NOT committed (git doesn't track local hooks); installed by script +- Hook resolves git hooks directory with `git rev-parse --git-path hooks` (supports worktrees) +- Hook backs up any differing existing hook before overwriting + +## Rationale + +1. **Faster feedback** — Catch broken code in ~7 seconds locally vs 2-5 minutes on CI +2. **Keep CI green** — Reduce failed CI runs, unblock squad agents faster +3. **Developer experience** — Immediate actionable error messages +4. **Team discipline** — Encourages running tests before push; CI is a safety net, not the first check +5. **Worktree-safe** — Uses `git rev-parse --git-path hooks` instead of hardcoded `.git/hooks` + +## Impact + +- **Push time:** +7 seconds (build + tests) on first push; cached builds faster +- **CI failures:** Expected significant decrease +- **Developer workflow:** One-time setup per clone/worktree (`./scripts/install-hooks.sh`) +- **Squad agents:** Must run install script after branch creation + +## Files + +- `scripts/install-hooks.sh` — Installation script (committed) +- `.github/hooks/pre-push` — Hook source code (committed) +- `docs/CONTRIBUTING.md` — Documents setup process +- `.github/pull_request_template.md` — Checklist includes pre-push gate setup + +## Validation + +✅ Tested on PR #12: +- Build: passed (0 errors, 0 warnings) +- Architecture.Tests: ✅ 6/6 passing +- Unit.Tests: ✅ 59/59 passing +- Integration.Tests: ✅ 9/9 passing +- Push: allowed after successful gate validation + +## User Directives (Captured 2026-04-18T21:21:06Z) + +From mpaulosky (via Copilot): +- **Mandatory:** Pre-push gate is a hard block — retries/blocks push until both `dotnet build` AND `dotnet test` succeed +- **Requirement:** Agents must not push branches until the gate passes +- **Rationale:** Lowering chance of pushing bad code to reduce CI feedback loop + +--- + +# Decision: Auth0 Secrets via User Secrets (Not appsettings) + +**Date:** 2026-04-18 +**Author:** Sam (Backend Developer) +**Status:** Proposed + +## Context + +The Web app was crashing at startup with `ArgumentException: The value cannot be an empty string (Parameter 'ClientId')` because `appsettings.json` stored empty placeholder strings for Auth0 settings with no user secrets configured. + +## Decision + +Auth0 credentials (`Auth0:Domain`, `Auth0:ClientId`, `Auth0:ClientSecret`) are **never stored in source-controlled config files**. They must be set via dotnet user-secrets on the Web project: + +```bash +dotnet user-secrets set "Auth0:Domain" ".auth0.com" --project src/Web +dotnet user-secrets set "Auth0:ClientId" "" --project src/Web +dotnet user-secrets set "Auth0:ClientSecret" "" --project src/Web +``` + +`appsettings.Development.json` documents the required keys (with empty values) as a developer reference. `Program.cs` validates these at startup and throws a clear `InvalidOperationException` with setup instructions if they are missing. + +## Rationale + +- **Security:** Auth0 secrets must not be committed to source control +- **Developer experience:** Clear error message with instructions beats cryptic middleware exception +- **AppHost design:** Aspire manages infrastructure secrets (MongoDB, Redis), not application-level OAuth credentials +- **Separation of concerns:** Infrastructure (Aspire) vs. application config (user-secrets) + +## Consequences + +- New developers cloning the repo must run the user-secrets commands before the app will start +- The error message in Program.cs serves as self-documenting setup instructions +- CI/CD will need Auth0 secrets injected as environment variables at deploy time + +## Related Decisions + +- **Branch strategy** — Ensures secrets are never committed across any branch +- **Pre-push gate** — Catches accidental secrets in local validation before push + +--- + +# Decision: Casting Infrastructure for Agent Lifecycle Management + +**Date:** 2026-04-18 +**Author:** Ralph (Infrastructure Specialist) +**Status:** ✅ Implemented (Phase 1) + +## Context + +The squad was initialized with `.squad/team.md` roster but lacked the casting infrastructure needed to manage agent lifecycle, policy, and governance decisions programmatically. Ralph created the foundation for deterministic, auditable team management. + +## Decision + +Establish `.squad/casting/` directory structure with three JSON files: + +### 1. `.squad/casting/policy.json` — Governance Defaults + +```json +{ + "max_concurrent_agents": 5, + "default_timeout_minutes": 120, + "retry_policy": { + "enabled": true, + "max_retries": 3, + "backoff_multiplier": 2, + "initial_delay_seconds": 5 + }, + "auto_escalate_blockers": true +} +``` + +**Key settings:** +- `max_concurrent_agents: 5` — Prevents overwhelming system at 11-agent team size +- `default_timeout_minutes: 120` — Allows complex tasks (tests, builds) to complete +- `auto_escalate_blockers: true` — Surfaces blocked work quickly to lead + +### 2. `.squad/casting/registry.json` — Team Roster + +All 12 agents (11 team members + 1 coordinator) migrated from `.squad/team.md`: +- `legacy_named: true` — No renaming +- `status: "active"` — All operational +- Charter paths point to existing agent directories + +### 3. `.squad/casting/history.json` — Migration Audit Trail + +Records: +- Timestamp, event type (migration, agent-join, agent-leave, role-change) +- Agent responsible for change +- Detailed impact notes + +Establishes pattern for future team changes (onboarding, offboarding, role shifts). + +## Rationale + +1. **Programmatic control** — Coordinator can spawn agents, manage timeouts, enforce governance via code +2. **Auditability** — Team changes tracked and reversible +3. **Scalability** — Current structure supports growing team without manual intervention +4. **No disruption** — Additive infrastructure; no existing agent behavior changes +5. **Future-proof** — Team size adjustments (>15 agents) can update `max_concurrent_agents` dynamically + +## Implementation Status + +✅ **Complete:** +- Created `.squad/casting/policy.json` with sensible defaults +- Created `.squad/casting/registry.json` with all agents +- Created `.squad/casting/history.json` with initial snapshot +- Recorded decision rationale in `.squad/decisions/inbox/ralph-casting-migration.md` + +🟡 **Next phases (deferred):** +- Phase 2: Agent spawn/timeout automation in coordinator +- Phase 3: Dynamic team scaling based on workload + +## Future Maintenance + +When team changes: +1. Update `.squad/casting/registry.json` with new agent record or status change +2. Add entry to `.squad/casting/history.json` with timestamp and details +3. Adjust `.squad/casting/policy.json` `max_concurrent_agents` if team size changes significantly (e.g., >15 agents → increase to 7-8) + +--- + +# Decision: PR #11 CSS Artifact Approval + +**Date:** 2026-04-18 +**Reviewer:** Legolas (Frontend/Blazor) +**Status:** ✅ Approved + +## Context + +PR #11 "chore: commit leftover uncommitted changes" contained a large `src/Web/wwwroot/css/tailwind.css` expansion (minified ~2 lines → pretty-printed 1918 lines). Legolas validated whether this was an intentional compiled artifact or a stale/unwanted asset. + +## Decision + +✅ **APPROVE** — The CSS expansion is intentional Tailwind v4.2.2 compiled output, not a stale artifact. + +## Validation + +### What Changed + +PR #11 contains: +- `src/Web/wwwroot/css/tailwind.css` — expanded from minified to pretty-printed (1918 lines) +- `src/Web/Features/UserManagement/ManageRoles.razor` — removed redundant `@using MyBlog.Web.Features.UserManagement` + +### Why It's Correct + +1. **CSS source unchanged** — `src/Web/wwwroot/css/app.css` is identical on dev and PR branch + - Tailwind v4 CSS-first format confirmed (`@import "tailwindcss"`, `@source` directives, `@theme inline`) + - No breaking changes to design tokens or component layer + +2. **Build recovery, not stale artifact** — PR title indicates code recovery from stale `cicd-phase3-4` branch + - `npm run tw:build` was never run on cicd-phase3-4 before checkout stalled + - This PR commits the proper Tailwind compilation that should have been done then + - Timestamp: tailwind.css touched 2026-04-18 (same as commit date) + +3. **Output is valid Tailwind v4.2.2** + - Header: `/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */` + - Structure: proper `@layer properties, theme, base, components, utilities` + - Pretty-printed by default (development format; production uses minified link) + +4. **Semantic tokens resolve correctly** + - All color-primary palettes in `@theme inline` match app.css custom properties + - Component layer (`.nav-link`, `.btn-primary`, `.card`) present and correct + - Blazor form validation styles preserved (`.valid.modified`, `.invalid`, `.validation-message`) + +5. **No regressions** — CSS file structure aligns with Tailwind v4 migration history + - All semantic color tokens (theme-blue, theme-red, etc.) properly compiled + - Dark mode `@custom-variant dark` working correctly + - Razor component scanning (@source) paths correct for `src/Web/` structure + +### Minor Changes + +- `ManageRoles.razor`: removed `@using MyBlog.Web.Features.UserManagement` (consistent with 2025-01-29 _Imports.razor consolidation) + +## Recommendation + +✅ **Merge approved from Blazor/CSS perspective.** No blocker issues. + +--- + +# Decision: PR #12 Pre-Push Gate References + +**Date:** 2026-04-18 +**Author:** Boromir (DevOps Engineer) +**Status:** ✅ Implemented + +## Context + +PR #12 follow-up review flagged a dead reference to `.squad/playbooks/pre-push-process.md` in SKILL.md. The playbook file doesn't exist. + +## Decision + +Update SKILL.md and PR template to point to `docs/CONTRIBUTING.md` as the authoritative setup and usage guide instead of referencing a non-existent playbook. + +## Rationale + +- `docs/CONTRIBUTING.md` already documents hook installation and the five pre-push gates +- Reusing the canonical contributor guide avoids duplicating operational instructions in a second document +- Removing the dead `.squad/playbooks/...` reference keeps the skill accurate for new contributors and agents + +## Implementation + +✅ **Complete:** +- Updated `.squad/skills/pre-push-test-gate/SKILL.md` to point to `docs/CONTRIBUTING.md` +- Updated `.github/pull_request_template.md` to reference canonical contributor guide +- PR #12 follow-up commit d59b493 pushed with corrections +- All green checks passing + +--- + +# Decision: User Directives on Pre-Push Gate (Captured 2026-04-18) + +**From:** mpaulosky (via Copilot) +**Date:** 2026-04-18T21:21:06Z & 2026-04-18T21:18:50Z + +### Directive 1: Pre-Push Gate is Mandatory Hard Block + +**What:** The pre-push gate is mandatory and must be a hard block — the gate retries/blocks the push until `dotnet build` AND `dotnet test` both succeed. + +**Why:** User request — captured for team memory + +**Implementation:** ✅ Complete +- `.github/hooks/pre-push` implements hard block (non-zero exit on failure) +- `scripts/install-hooks.sh` installs the hook +- Emergency bypass documented: `git push --no-verify` (use sparingly) + +### Directive 2: Always Run Pre-Push Gate Validation + +**What:** Always run a pre-push gate before pushing branches to GitHub. Agents must run `dotnet build` and `dotnet test` locally before `git push` to lower the chance of pushing bad code. + +**Why:** User request — captured for team memory + +**Implementation:** ✅ Complete +- Pre-push hook enforces `dotnet build MyBlog.slnx -c Release` +- Pre-push hook enforces `dotnet test MyBlog.slnx --no-build -c Release` +- CI skips hook when `CI=true` environment variable set + +--- + diff --git a/.squad/decisions/inbox/boromir-pr12-followups.md b/.squad/decisions/inbox/boromir-pr12-followups.md deleted file mode 100644 index 51ce4695..00000000 --- a/.squad/decisions/inbox/boromir-pr12-followups.md +++ /dev/null @@ -1,21 +0,0 @@ -# PR #12 Follow-ups: Pre-Push Gate References - -**Date:** 2026-04-19 -**Author:** Boromir (DevOps Engineer) -**Status:** ✅ Implemented -**PR:** #12 - -## Decision - -The pre-push skill should point contributors to `docs/CONTRIBUTING.md` as the -authoritative setup and usage guide instead of referencing a non-existent -`.squad/playbooks/pre-push-process.md` playbook. - -## Rationale - -- `docs/CONTRIBUTING.md` already documents hook installation and the five - pre-push gates. -- Reusing the canonical contributor guide avoids duplicating operational - instructions in a second document. -- Removing the dead `.squad/playbooks/...` reference keeps the skill accurate - for new contributors and agents. diff --git a/.squad/decisions/inbox/boromir-pr9-review-fixes.md b/.squad/decisions/inbox/boromir-pr9-review-fixes.md deleted file mode 100644 index 85f8beb4..00000000 --- a/.squad/decisions/inbox/boromir-pr9-review-fixes.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/.squad/identity/now.md b/.squad/identity/now.md index 372c9c63..bf047e29 100644 --- a/.squad/identity/now.md +++ b/.squad/identity/now.md @@ -1,9 +1,17 @@ --- -updated_at: 2026-04-17T14:40:53.711Z -focus_area: Initial setup +updated_at: 2026-04-18T17:05:49Z +focus_area: Post-coordination cleanup & governance active_issues: [] --- # What We're Focused On -Getting started. Updated by coordinator at session start. +**Round complete:** PR #11 and #12 merged, board cleared, casting infrastructure in place, decisions consolidated. + +**Current focus:** Maintaining clean board; next round of squad work targets dev branch per new branch strategy. + +**Key milestones achieved:** +- Casting infrastructure (Phase 1) for agent lifecycle management +- Branch strategy (dev/main) with GitVersion versioning implemented +- Pre-push gate mandatory validation deployed +- All decisions documented and consolidated in decisions.md From f037e2c98801e27ecba2c2b5118be76a78355944 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:22:58 -0700 Subject: [PATCH 09/10] chore: ignore JetBrains IDE metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 3 ++- .idea/.idea.MyBlog/.idea/.gitignore | 15 ----------- .idea/.idea.MyBlog/.idea/dataSources.xml | 25 ------------------- .idea/.idea.MyBlog/.idea/db-forest-config.xml | 10 -------- .idea/.idea.MyBlog/.idea/encodings.xml | 4 --- .idea/.idea.MyBlog/.idea/indexLayout.xml | 8 ------ .idea/.idea.MyBlog/.idea/misc.xml | 4 --- .idea/.idea.MyBlog/.idea/vcs.xml | 6 ----- 8 files changed, 2 insertions(+), 73 deletions(-) delete mode 100644 .idea/.idea.MyBlog/.idea/.gitignore delete mode 100644 .idea/.idea.MyBlog/.idea/dataSources.xml delete mode 100644 .idea/.idea.MyBlog/.idea/db-forest-config.xml delete mode 100644 .idea/.idea.MyBlog/.idea/encodings.xml delete mode 100644 .idea/.idea.MyBlog/.idea/indexLayout.xml delete mode 100644 .idea/.idea.MyBlog/.idea/misc.xml delete mode 100644 .idea/.idea.MyBlog/.idea/vcs.xml diff --git a/.gitignore b/.gitignore index 421ba8e4..5fe69046 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.user *.suo .vs/ +.idea/ *.nupkg *.snupkg TestResults/ @@ -24,4 +25,4 @@ src/Web/wwwroot/css/tailwind.css # Squad: SubSquad activation file (local to this machine) .squad-workstream -.fake \ No newline at end of file +.fake diff --git a/.idea/.idea.MyBlog/.idea/.gitignore b/.idea/.idea.MyBlog/.idea/.gitignore deleted file mode 100644 index 0d817ac3..00000000 --- a/.idea/.idea.MyBlog/.idea/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Rider ignored files -/contentModel.xml -/.idea.MyBlog.iml -/modules.xml -/projectSettingsUpdater.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.idea.MyBlog/.idea/dataSources.xml b/.idea/.idea.MyBlog/.idea/dataSources.xml deleted file mode 100644 index ac388280..00000000 --- a/.idea/.idea.MyBlog/.idea/dataSources.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - mongo.4 - true - com.dbschema.MongoJdbcDriver - mongodb://admin:jmr1sZxmTA3JAZ0J2QpJs4@localhost:32795/?authSource=admin&authMechanism=SCRAM-SHA-256 - - - - $ProjectFileDir$ - - - redis - true - jdbc.RedisDriver - jdbc:redis://3GtSRC0bKuYcbGKadc81Bp@localhost:32796?ssl=true - - - - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/.idea.MyBlog/.idea/db-forest-config.xml b/.idea/.idea.MyBlog/.idea/db-forest-config.xml deleted file mode 100644 index 87c1b28b..00000000 --- a/.idea/.idea.MyBlog/.idea/db-forest-config.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - . - ---------------------------------------- - 1:0:1a564bcc-fc25-4ef0-a675-3bccecb8960c - 2:0:075bb980-ed17-416f-aade-b9c8a4868f26 - . - - \ No newline at end of file diff --git a/.idea/.idea.MyBlog/.idea/encodings.xml b/.idea/.idea.MyBlog/.idea/encodings.xml deleted file mode 100644 index df87cf95..00000000 --- a/.idea/.idea.MyBlog/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/.idea.MyBlog/.idea/indexLayout.xml b/.idea/.idea.MyBlog/.idea/indexLayout.xml deleted file mode 100644 index 7b08163c..00000000 --- a/.idea/.idea.MyBlog/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/.idea.MyBlog/.idea/misc.xml b/.idea/.idea.MyBlog/.idea/misc.xml deleted file mode 100644 index 7fcdf3ba..00000000 --- a/.idea/.idea.MyBlog/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - {} - \ No newline at end of file diff --git a/.idea/.idea.MyBlog/.idea/vcs.xml b/.idea/.idea.MyBlog/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/.idea.MyBlog/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From d757e6801f4bcf58e7c98352671853950291f0e1 Mon Sep 17 00:00:00 2001 From: mpaulosky <60372079+mpaulosky@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:28:14 -0700 Subject: [PATCH 10/10] chore: untrack generated tailwind.css file The src/Web/wwwroot/css/tailwind.css file is already in .gitignore but was still tracked in git. Remove it from tracking so the generated file is not committed to the repository. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Web/wwwroot/css/tailwind.css | 1918 ------------------------------ 1 file changed, 1918 deletions(-) delete mode 100644 src/Web/wwwroot/css/tailwind.css diff --git a/src/Web/wwwroot/css/tailwind.css b/src/Web/wwwroot/css/tailwind.css deleted file mode 100644 index 8b68aff8..00000000 --- a/src/Web/wwwroot/css/tailwind.css +++ /dev/null @@ -1,1918 +0,0 @@ -/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties; -@layer theme, base, components, utilities; -@layer theme { - :root, :host { - --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", - "Courier New", monospace; - --color-red-50: oklch(97.1% 0.013 17.38); - --color-red-100: oklch(93.6% 0.032 17.717); - --color-red-200: oklch(88.5% 0.062 18.334); - --color-red-300: oklch(80.8% 0.114 19.571); - --color-red-400: oklch(70.4% 0.191 22.216); - --color-red-500: oklch(63.7% 0.237 25.331); - --color-red-600: oklch(57.7% 0.245 27.325); - --color-red-700: oklch(50.5% 0.213 27.518); - --color-red-800: oklch(44.4% 0.177 26.899); - --color-red-900: oklch(39.6% 0.141 25.723); - --color-red-950: oklch(25.8% 0.092 26.042); - --color-amber-300: oklch(87.9% 0.169 91.605); - --color-amber-700: oklch(55.5% 0.163 48.998); - --color-yellow-50: oklch(98.7% 0.026 102.212); - --color-yellow-300: oklch(90.5% 0.182 98.111); - --color-yellow-700: oklch(55.4% 0.135 66.442); - --color-yellow-900: oklch(42.1% 0.095 57.708); - --color-green-50: oklch(98.2% 0.018 155.826); - --color-green-100: oklch(96.2% 0.044 156.743); - --color-green-300: oklch(87.1% 0.15 154.449); - --color-green-500: oklch(72.3% 0.219 149.579); - --color-green-600: oklch(62.7% 0.194 149.214); - --color-green-800: oklch(44.8% 0.119 151.328); - --color-green-900: oklch(39.3% 0.095 152.535); - --color-blue-50: oklch(97% 0.014 254.604); - --color-blue-100: oklch(93.2% 0.032 255.585); - --color-blue-300: oklch(80.9% 0.105 251.813); - --color-blue-700: oklch(48.8% 0.243 264.376); - --color-blue-900: oklch(37.9% 0.146 265.522); - --color-blue-950: oklch(28.2% 0.091 267.935); - --color-gray-50: oklch(98.5% 0.002 247.839); - --color-gray-100: oklch(96.7% 0.003 264.542); - --color-gray-200: oklch(92.8% 0.006 264.531); - --color-gray-300: oklch(87.2% 0.01 258.338); - --color-gray-400: oklch(70.7% 0.022 261.325); - --color-gray-500: oklch(55.1% 0.027 264.364); - --color-gray-600: oklch(44.6% 0.03 256.802); - --color-gray-700: oklch(37.3% 0.034 259.733); - --color-gray-800: oklch(27.8% 0.033 256.848); - --color-gray-900: oklch(21% 0.034 264.665); - --color-gray-950: oklch(13% 0.028 261.692); - --color-black: #000; - --color-white: #fff; - --spacing: 0.25rem; - --container-md: 28rem; - --container-2xl: 42rem; - --container-6xl: 72rem; - --container-7xl: 80rem; - --text-xs: 0.75rem; - --text-xs--line-height: calc(1 / 0.75); - --text-sm: 0.875rem; - --text-sm--line-height: calc(1.25 / 0.875); - --text-base: 1rem; - --text-base--line-height: calc(1.5 / 1); - --text-lg: 1.125rem; - --text-lg--line-height: calc(1.75 / 1.125); - --text-xl: 1.25rem; - --text-xl--line-height: calc(1.75 / 1.25); - --text-2xl: 1.5rem; - --text-2xl--line-height: calc(2 / 1.5); - --text-3xl: 1.875rem; - --text-3xl--line-height: calc(2.25 / 1.875); - --text-5xl: 3rem; - --text-5xl--line-height: 1; - --font-weight-medium: 500; - --font-weight-semibold: 600; - --font-weight-bold: 700; - --tracking-wide: 0.025em; - --tracking-wider: 0.05em; - --radius-md: 0.375rem; - --radius-lg: 0.5rem; - --radius-xl: 0.75rem; - --radius-2xl: 1rem; - --animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - --blur-sm: 8px; - --default-transition-duration: 150ms; - --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - --default-font-family: var(--font-sans); - --default-mono-font-family: var(--font-mono); - } -} -@layer base { - *, ::after, ::before, ::backdrop, ::file-selector-button { - box-sizing: border-box; - margin: 0; - padding: 0; - border: 0 solid; - } - html, :host { - line-height: 1.5; - -webkit-text-size-adjust: 100%; - tab-size: 4; - font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); - font-feature-settings: var(--default-font-feature-settings, normal); - font-variation-settings: var(--default-font-variation-settings, normal); - -webkit-tap-highlight-color: transparent; - } - hr { - height: 0; - color: inherit; - border-top-width: 1px; - } - abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - } - h1, h2, h3, h4, h5, h6 { - font-size: inherit; - font-weight: inherit; - } - a { - color: inherit; - -webkit-text-decoration: inherit; - text-decoration: inherit; - } - b, strong { - font-weight: bolder; - } - code, kbd, samp, pre { - font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); - font-feature-settings: var(--default-mono-font-feature-settings, normal); - font-variation-settings: var(--default-mono-font-variation-settings, normal); - font-size: 1em; - } - small { - font-size: 80%; - } - sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - sub { - bottom: -0.25em; - } - sup { - top: -0.5em; - } - table { - text-indent: 0; - border-color: inherit; - border-collapse: collapse; - } - :-moz-focusring { - outline: auto; - } - progress { - vertical-align: baseline; - } - summary { - display: list-item; - } - ol, ul, menu { - list-style: none; - } - img, svg, video, canvas, audio, iframe, embed, object { - display: block; - vertical-align: middle; - } - img, video { - max-width: 100%; - height: auto; - } - button, input, select, optgroup, textarea, ::file-selector-button { - font: inherit; - font-feature-settings: inherit; - font-variation-settings: inherit; - letter-spacing: inherit; - color: inherit; - border-radius: 0; - background-color: transparent; - opacity: 1; - } - :where(select:is([multiple], [size])) optgroup { - font-weight: bolder; - } - :where(select:is([multiple], [size])) optgroup option { - padding-inline-start: 20px; - } - ::file-selector-button { - margin-inline-end: 4px; - } - ::placeholder { - opacity: 1; - } - @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { - ::placeholder { - color: currentcolor; - @supports (color: color-mix(in lab, red, red)) { - color: color-mix(in oklab, currentcolor 50%, transparent); - } - } - } - textarea { - resize: vertical; - } - ::-webkit-search-decoration { - -webkit-appearance: none; - } - ::-webkit-date-and-time-value { - min-height: 1lh; - text-align: inherit; - } - ::-webkit-datetime-edit { - display: inline-flex; - } - ::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { - padding-block: 0; - } - ::-webkit-calendar-picker-indicator { - line-height: 1; - } - :-moz-ui-invalid { - box-shadow: none; - } - button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { - appearance: button; - } - ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { - height: auto; - } - [hidden]:where(:not([hidden="until-found"])) { - display: none !important; - } -} -@layer utilities { - .collapse { - visibility: collapse; - } - .invisible { - visibility: hidden; - } - .visible { - visibility: visible; - } - .sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; - border-width: 0; - } - .absolute { - position: absolute; - } - .fixed { - position: fixed; - } - .relative { - position: relative; - } - .static { - position: static; - } - .sticky { - position: sticky; - } - .inset-0 { - inset: calc(var(--spacing) * 0); - } - .start { - inset-inline-start: var(--spacing); - } - .end { - inset-inline-end: var(--spacing); - } - .top-0 { - top: calc(var(--spacing) * 0); - } - .right-0 { - right: calc(var(--spacing) * 0); - } - .left-0 { - left: calc(var(--spacing) * 0); - } - .isolate { - isolation: isolate; - } - .z-50 { - z-index: 50; - } - .z-\[100\] { - z-index: 100; - } - .container { - width: 100%; - @media (width >= 40rem) { - max-width: 40rem; - } - @media (width >= 48rem) { - max-width: 48rem; - } - @media (width >= 64rem) { - max-width: 64rem; - } - @media (width >= 80rem) { - max-width: 80rem; - } - @media (width >= 96rem) { - max-width: 96rem; - } - } - .mx-4 { - margin-inline: calc(var(--spacing) * 4); - } - .mx-auto { - margin-inline: auto; - } - .me-1 { - margin-inline-end: calc(var(--spacing) * 1); - } - .\!mt-1 { - margin-top: calc(var(--spacing) * 1) !important; - } - .mt-4 { - margin-top: calc(var(--spacing) * 4); - } - .mt-6 { - margin-top: calc(var(--spacing) * 6); - } - .mt-auto { - margin-top: auto; - } - .mr-1 { - margin-right: calc(var(--spacing) * 1); - } - .mb-1 { - margin-bottom: calc(var(--spacing) * 1); - } - .mb-2 { - margin-bottom: calc(var(--spacing) * 2); - } - .mb-3 { - margin-bottom: calc(var(--spacing) * 3); - } - .mb-4 { - margin-bottom: calc(var(--spacing) * 4); - } - .mb-6 { - margin-bottom: calc(var(--spacing) * 6); - } - .ml-4 { - margin-left: calc(var(--spacing) * 4); - } - .block { - display: block; - } - .contents { - display: contents; - } - .flex { - display: flex; - } - .grid { - display: grid; - } - .hidden { - display: none; - } - .inline { - display: inline; - } - .inline-block { - display: inline-block; - } - .table { - display: table; - } - .h-5 { - height: calc(var(--spacing) * 5); - } - .h-6 { - height: calc(var(--spacing) * 6); - } - .h-12 { - height: calc(var(--spacing) * 12); - } - .h-16 { - height: calc(var(--spacing) * 16); - } - .h-24 { - height: calc(var(--spacing) * 24); - } - .h-48 { - height: calc(var(--spacing) * 48); - } - .min-h-screen { - min-height: 100vh; - } - .w-5 { - width: calc(var(--spacing) * 5); - } - .w-6 { - width: calc(var(--spacing) * 6); - } - .w-24 { - width: calc(var(--spacing) * 24); - } - .w-32 { - width: calc(var(--spacing) * 32); - } - .w-full { - width: 100%; - } - .max-w-2xl { - max-width: var(--container-2xl); - } - .max-w-6xl { - max-width: var(--container-6xl); - } - .max-w-7xl { - max-width: var(--container-7xl); - } - .max-w-md { - max-width: var(--container-md); - } - .min-w-full { - min-width: 100%; - } - .flex-1 { - flex: 1; - } - .shrink-0 { - flex-shrink: 0; - } - .animate-pulse { - animation: var(--animate-pulse); - } - .cursor-pointer { - cursor: pointer; - } - .flex-col { - flex-direction: column; - } - .flex-wrap { - flex-wrap: wrap; - } - .items-center { - align-items: center; - } - .justify-between { - justify-content: space-between; - } - .justify-center { - justify-content: center; - } - .justify-end { - justify-content: flex-end; - } - .gap-1 { - gap: calc(var(--spacing) * 1); - } - .gap-2 { - gap: calc(var(--spacing) * 2); - } - .gap-3 { - gap: calc(var(--spacing) * 3); - } - .gap-4 { - gap: calc(var(--spacing) * 4); - } - .gap-5 { - gap: calc(var(--spacing) * 5); - } - .gap-6 { - gap: calc(var(--spacing) * 6); - } - .space-y-2 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-3 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); - } - } - .space-y-5 { - :where(& > :not(:last-child)) { - --tw-space-y-reverse: 0; - margin-block-start: calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse)); - margin-block-end: calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse))); - } - } - .divide-y { - :where(& > :not(:last-child)) { - --tw-divide-y-reverse: 0; - border-bottom-style: var(--tw-border-style); - border-top-style: var(--tw-border-style); - border-top-width: calc(1px * var(--tw-divide-y-reverse)); - border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); - } - } - .divide-gray-100 { - :where(& > :not(:last-child)) { - border-color: var(--color-gray-100); - } - } - .divide-primary-100 { - :where(& > :not(:last-child)) { - border-color: var(--primary-100); - } - } - .divide-primary-200 { - :where(& > :not(:last-child)) { - border-color: var(--primary-200); - } - } - .overflow-hidden { - overflow: hidden; - } - .overflow-x-auto { - overflow-x: auto; - } - .rounded { - border-radius: 0.25rem; - } - .rounded-2xl { - border-radius: var(--radius-2xl); - } - .rounded-full { - border-radius: calc(infinity * 1px); - } - .rounded-lg { - border-radius: var(--radius-lg); - } - .rounded-md { - border-radius: var(--radius-md); - } - .rounded-xl { - border-radius: var(--radius-xl); - } - .border { - border-style: var(--tw-border-style); - border-width: 1px; - } - .border-2 { - border-style: var(--tw-border-style); - border-width: 2px; - } - .border-b { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 1px; - } - .border-b-2 { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 2px; - } - .border-gray-200 { - border-color: var(--color-gray-200); - } - .border-gray-300 { - border-color: var(--color-gray-300); - } - .border-green-600 { - border-color: var(--color-green-600); - } - .border-primary-200 { - border-color: var(--primary-200); - } - .border-primary-300 { - border-color: var(--primary-300); - } - .border-red-200 { - border-color: var(--color-red-200); - } - .border-red-300 { - border-color: var(--color-red-300); - } - .border-red-600 { - border-color: var(--color-red-600); - } - .border-yellow-300 { - border-color: var(--color-yellow-300); - } - .bg-\[var\(--color-primary\)\] { - background-color: var(--color-primary); - } - .bg-black\/50 { - background-color: color-mix(in srgb, #000 50%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-black) 50%, transparent); - } - } - .bg-blue-50 { - background-color: var(--color-blue-50); - } - .bg-gray-50 { - background-color: var(--color-gray-50); - } - .bg-gray-100 { - background-color: var(--color-gray-100); - } - .bg-gray-200 { - background-color: var(--color-gray-200); - } - .bg-green-100 { - background-color: var(--color-green-100); - } - .bg-primary-50 { - background-color: var(--primary-50); - } - .bg-primary-50\/70 { - background-color: var(--primary-50); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--primary-50) 70%, transparent); - } - } - .bg-primary-400 { - background-color: var(--primary-400); - } - .bg-primary-600 { - background-color: var(--primary-600); - } - .bg-red-50 { - background-color: var(--color-red-50); - } - .bg-red-600 { - background-color: var(--color-red-600); - } - .bg-white { - background-color: var(--color-white); - } - .bg-yellow-50 { - background-color: var(--color-yellow-50); - } - .object-cover { - object-fit: cover; - } - .p-1\.5 { - padding: calc(var(--spacing) * 1.5); - } - .p-2 { - padding: calc(var(--spacing) * 2); - } - .p-4 { - padding: calc(var(--spacing) * 4); - } - .p-6 { - padding: calc(var(--spacing) * 6); - } - .px-2 { - padding-inline: calc(var(--spacing) * 2); - } - .px-3 { - padding-inline: calc(var(--spacing) * 3); - } - .px-4 { - padding-inline: calc(var(--spacing) * 4); - } - .px-6 { - padding-inline: calc(var(--spacing) * 6); - } - .py-1 { - padding-block: calc(var(--spacing) * 1); - } - .py-2 { - padding-block: calc(var(--spacing) * 2); - } - .py-3 { - padding-block: calc(var(--spacing) * 3); - } - .py-4 { - padding-block: calc(var(--spacing) * 4); - } - .py-8 { - padding-block: calc(var(--spacing) * 8); - } - .py-20 { - padding-block: calc(var(--spacing) * 20); - } - .ps-3 { - padding-inline-start: calc(var(--spacing) * 3); - } - .pt-20 { - padding-top: calc(var(--spacing) * 20); - } - .pb-0\.5 { - padding-bottom: calc(var(--spacing) * 0.5); - } - .pb-4 { - padding-bottom: calc(var(--spacing) * 4); - } - .pb-6 { - padding-bottom: calc(var(--spacing) * 6); - } - .pb-8 { - padding-bottom: calc(var(--spacing) * 8); - } - .text-center { - text-align: center; - } - .text-left { - text-align: left; - } - .text-right { - text-align: right; - } - .align-top { - vertical-align: top; - } - .\!text-2xl { - font-size: var(--text-2xl) !important; - line-height: var(--tw-leading, var(--text-2xl--line-height)) !important; - } - .\!text-base { - font-size: var(--text-base) !important; - line-height: var(--tw-leading, var(--text-base--line-height)) !important; - } - .\!text-sm { - font-size: var(--text-sm) !important; - line-height: var(--tw-leading, var(--text-sm--line-height)) !important; - } - .\!text-xl { - font-size: var(--text-xl) !important; - line-height: var(--tw-leading, var(--text-xl--line-height)) !important; - } - .text-2xl { - font-size: var(--text-2xl); - line-height: var(--tw-leading, var(--text-2xl--line-height)); - } - .text-3xl { - font-size: var(--text-3xl); - line-height: var(--tw-leading, var(--text-3xl--line-height)); - } - .text-5xl { - font-size: var(--text-5xl); - line-height: var(--tw-leading, var(--text-5xl--line-height)); - } - .text-lg { - font-size: var(--text-lg); - line-height: var(--tw-leading, var(--text-lg--line-height)); - } - .text-sm { - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - } - .text-xl { - font-size: var(--text-xl); - line-height: var(--tw-leading, var(--text-xl--line-height)); - } - .text-xs { - font-size: var(--text-xs); - line-height: var(--tw-leading, var(--text-xs--line-height)); - } - .\!font-medium { - --tw-font-weight: var(--font-weight-medium) !important; - font-weight: var(--font-weight-medium) !important; - } - .font-bold { - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); - } - .font-medium { - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - } - .font-semibold { - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - } - .tracking-wide { - --tw-tracking: var(--tracking-wide); - letter-spacing: var(--tracking-wide); - } - .tracking-wider { - --tw-tracking: var(--tracking-wider); - letter-spacing: var(--tracking-wider); - } - .break-all { - word-break: break-all; - } - .text-\[--color-primary\] { - color: --color-primary; - } - .text-\[var\(--color-primary\)\] { - color: var(--color-primary); - } - .text-amber-700 { - color: var(--color-amber-700); - } - .text-blue-700 { - color: var(--color-blue-700); - } - .text-gray-400 { - color: var(--color-gray-400); - } - .text-gray-500 { - color: var(--color-gray-500); - } - .text-gray-600 { - color: var(--color-gray-600); - } - .text-gray-700 { - color: var(--color-gray-700); - } - .text-gray-800 { - color: var(--color-gray-800); - } - .text-gray-900 { - color: var(--color-gray-900); - } - .text-green-600 { - color: var(--color-green-600); - } - .text-green-800 { - color: var(--color-green-800); - } - .text-primary-50 { - color: var(--primary-50); - } - .text-primary-800 { - color: var(--primary-800); - } - .text-red-500 { - color: var(--color-red-500); - } - .text-red-600 { - color: var(--color-red-600); - } - .text-red-700 { - color: var(--color-red-700); - } - .text-white { - color: var(--color-white); - } - .text-yellow-700 { - color: var(--color-yellow-700); - } - .lowercase { - text-transform: lowercase; - } - .uppercase { - text-transform: uppercase; - } - .underline { - text-decoration-line: underline; - } - .shadow { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-2xl { - --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-md { - --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-sm { - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .shadow-xl { - --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - .outline { - outline-style: var(--tw-outline-style); - outline-width: 1px; - } - .outline-1 { - outline-style: var(--tw-outline-style); - outline-width: 1px; - } - .filter { - filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); - } - .backdrop-blur-sm { - --tw-backdrop-blur: blur(var(--blur-sm)); - -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); - } - .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .transition-colors { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - } - .duration-200 { - --tw-duration: 200ms; - transition-duration: 200ms; - } - .outline-none { - --tw-outline-style: none; - outline-style: none; - } - .peer-checked\:flex { - &:is(:where(.peer):checked ~ *) { - display: flex; - } - } - .odd\:bg-white { - &:nth-child(odd) { - background-color: var(--color-white); - } - } - .even\:bg-gray-50 { - &:nth-child(even) { - background-color: var(--color-gray-50); - } - } - .even\:bg-primary-50\/40 { - &:nth-child(even) { - background-color: var(--primary-50); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--primary-50) 40%, transparent); - } - } - } - .hover\:bg-\[var\(--color-primary-light\)\] { - &:hover { - @media (hover: hover) { - background-color: var(--color-primary-light); - } - } - } - .hover\:bg-blue-100 { - &:hover { - @media (hover: hover) { - background-color: var(--color-blue-100); - } - } - } - .hover\:bg-gray-50 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-50); - } - } - } - .hover\:bg-gray-100 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-100); - } - } - } - .hover\:bg-gray-200 { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-200); - } - } - } - .hover\:bg-green-50 { - &:hover { - @media (hover: hover) { - background-color: var(--color-green-50); - } - } - } - .hover\:bg-primary-50 { - &:hover { - @media (hover: hover) { - background-color: var(--primary-50); - } - } - } - .hover\:bg-primary-500 { - &:hover { - @media (hover: hover) { - background-color: var(--primary-500); - } - } - } - .hover\:bg-red-50 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-50); - } - } - } - .hover\:bg-red-100 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-100); - } - } - } - .hover\:bg-red-700 { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-700); - } - } - } - .hover\:text-\[--color-primary\] { - &:hover { - @media (hover: hover) { - color: --color-primary; - } - } - } - .hover\:text-gray-600 { - &:hover { - @media (hover: hover) { - color: var(--color-gray-600); - } - } - } - .hover\:text-red-900 { - &:hover { - @media (hover: hover) { - color: var(--color-red-900); - } - } - } - .hover\:text-yellow-900 { - &:hover { - @media (hover: hover) { - color: var(--color-yellow-900); - } - } - } - .focus\:ring-2 { - &:focus { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } - .focus\:ring-\[var\(--color-primary\)\] { - &:focus { - --tw-ring-color: var(--color-primary); - } - } - .focus\:ring-primary-400 { - &:focus { - --tw-ring-color: var(--primary-400); - } - } - .focus\:outline-none { - &:focus { - --tw-outline-style: none; - outline-style: none; - } - } - .focus-visible\:outline-none { - &:focus-visible { - --tw-outline-style: none; - outline-style: none; - } - } - .disabled\:opacity-50 { - &:disabled { - opacity: 50%; - } - } - .sm\:grid-cols-2 { - @media (width >= 40rem) { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } - .sm\:flex-row { - @media (width >= 40rem) { - flex-direction: row; - } - } - .sm\:items-start { - @media (width >= 40rem) { - align-items: flex-start; - } - } - .sm\:text-left { - @media (width >= 40rem) { - text-align: left; - } - } - .md\:flex { - @media (width >= 48rem) { - display: flex; - } - } - .md\:hidden { - @media (width >= 48rem) { - display: none; - } - } - .lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.35fr\)\] { - @media (width >= 64rem) { - grid-template-columns: minmax(0,1fr) minmax(0,1.35fr); - } - } - .dark\:divide-gray-700 { - &:where(.dark, .dark *) { - :where(& > :not(:last-child)) { - border-color: var(--color-gray-700); - } - } - } - .dark\:divide-primary-800 { - &:where(.dark, .dark *) { - :where(& > :not(:last-child)) { - border-color: var(--primary-800); - } - } - } - .dark\:divide-primary-900\/80 { - &:where(.dark, .dark *) { - :where(& > :not(:last-child)) { - border-color: var(--primary-900); - @supports (color: color-mix(in lab, red, red)) { - border-color: color-mix(in oklab, var(--primary-900) 80%, transparent); - } - } - } - } - .dark\:border-gray-600 { - &:where(.dark, .dark *) { - border-color: var(--color-gray-600); - } - } - .dark\:border-gray-700 { - &:where(.dark, .dark *) { - border-color: var(--color-gray-700); - } - } - .dark\:border-primary-200 { - &:where(.dark, .dark *) { - border-color: var(--primary-200); - } - } - .dark\:border-primary-700 { - &:where(.dark, .dark *) { - border-color: var(--primary-700); - } - } - .dark\:border-primary-800 { - &:where(.dark, .dark *) { - border-color: var(--primary-800); - } - } - .dark\:border-red-800 { - &:where(.dark, .dark *) { - border-color: var(--color-red-800); - } - } - .dark\:bg-blue-950 { - &:where(.dark, .dark *) { - background-color: var(--color-blue-950); - } - } - .dark\:bg-gray-700 { - &:where(.dark, .dark *) { - background-color: var(--color-gray-700); - } - } - .dark\:bg-gray-800 { - &:where(.dark, .dark *) { - background-color: var(--color-gray-800); - } - } - .dark\:bg-gray-900 { - &:where(.dark, .dark *) { - background-color: var(--color-gray-900); - } - } - .dark\:bg-green-900\/40 { - &:where(.dark, .dark *) { - background-color: color-mix(in srgb, oklch(39.3% 0.095 152.535) 40%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-900) 40%, transparent); - } - } - } - .dark\:bg-primary-600 { - &:where(.dark, .dark *) { - background-color: var(--primary-600); - } - } - .dark\:bg-primary-700 { - &:where(.dark, .dark *) { - background-color: var(--primary-700); - } - } - .dark\:bg-primary-800 { - &:where(.dark, .dark *) { - background-color: var(--primary-800); - } - } - .dark\:bg-primary-900 { - &:where(.dark, .dark *) { - background-color: var(--primary-900); - } - } - .dark\:bg-primary-950\/40 { - &:where(.dark, .dark *) { - background-color: var(--primary-950); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--primary-950) 40%, transparent); - } - } - } - .dark\:bg-primary-950\/60 { - &:where(.dark, .dark *) { - background-color: var(--primary-950); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--primary-950) 60%, transparent); - } - } - } - .dark\:bg-red-950 { - &:where(.dark, .dark *) { - background-color: var(--color-red-950); - } - } - .dark\:text-\[--color-primary-light\] { - &:where(.dark, .dark *) { - color: --color-primary-light; - } - } - .dark\:text-amber-300 { - &:where(.dark, .dark *) { - color: var(--color-amber-300); - } - } - .dark\:text-blue-300 { - &:where(.dark, .dark *) { - color: var(--color-blue-300); - } - } - .dark\:text-gray-50 { - &:where(.dark, .dark *) { - color: var(--color-gray-50); - } - } - .dark\:text-gray-100 { - &:where(.dark, .dark *) { - color: var(--color-gray-100); - } - } - .dark\:text-gray-200 { - &:where(.dark, .dark *) { - color: var(--color-gray-200); - } - } - .dark\:text-gray-300 { - &:where(.dark, .dark *) { - color: var(--color-gray-300); - } - } - .dark\:text-gray-400 { - &:where(.dark, .dark *) { - color: var(--color-gray-400); - } - } - .dark\:text-gray-500 { - &:where(.dark, .dark *) { - color: var(--color-gray-500); - } - } - .dark\:text-green-300 { - &:where(.dark, .dark *) { - color: var(--color-green-300); - } - } - .dark\:text-primary-300 { - &:where(.dark, .dark *) { - color: var(--primary-300); - } - } - .dark\:text-red-300 { - &:where(.dark, .dark *) { - color: var(--color-red-300); - } - } - .dark\:text-red-400 { - &:where(.dark, .dark *) { - color: var(--color-red-400); - } - } - .dark\:text-white { - &:where(.dark, .dark *) { - color: var(--color-white); - } - } - .odd\:dark\:bg-gray-800 { - &:nth-child(odd) { - &:where(.dark, .dark *) { - background-color: var(--color-gray-800); - } - } - } - .odd\:dark\:bg-gray-900 { - &:nth-child(odd) { - &:where(.dark, .dark *) { - background-color: var(--color-gray-900); - } - } - } - .even\:dark\:bg-gray-900 { - &:nth-child(even) { - &:where(.dark, .dark *) { - background-color: var(--color-gray-900); - } - } - } - .even\:dark\:bg-primary-950\/20 { - &:nth-child(even) { - &:where(.dark, .dark *) { - background-color: var(--primary-950); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--primary-950) 20%, transparent); - } - } - } - } - .dark\:hover\:bg-blue-900 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: var(--color-blue-900); - } - } - } - } - .dark\:hover\:bg-gray-600 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-600); - } - } - } - } - .dark\:hover\:bg-gray-700 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: var(--color-gray-700); - } - } - } - } - .dark\:hover\:bg-green-900\/20 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(39.3% 0.095 152.535) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-green-900) 20%, transparent); - } - } - } - } - } - .dark\:hover\:bg-primary-600 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: var(--primary-600); - } - } - } - } - .dark\:hover\:bg-primary-950 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: var(--primary-950); - } - } - } - } - .dark\:hover\:bg-red-900 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: var(--color-red-900); - } - } - } - } - .dark\:hover\:bg-red-900\/20 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: color-mix(in srgb, oklch(39.6% 0.141 25.723) 20%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-red-900) 20%, transparent); - } - } - } - } - } - .dark\:hover\:text-\[--color-primary-light\] { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - color: --color-primary-light; - } - } - } - } - .dark\:hover\:text-gray-200 { - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - color: var(--color-gray-200); - } - } - } - } -} -@layer base { - :root.color-blue { - --primary-50: #eff6ff; - --primary-100: #dbeafe; - --primary-200: #bfdbfe; - --primary-300: #93c5fd; - --primary-400: #60a5fa; - --primary-500: #3b82f6; - --primary-600: #2563eb; - --primary-700: #1d4ed8; - --primary-800: #1e40af; - --primary-900: #1e3a8a; - --primary-950: #172554; - } - :root.color-red { - --primary-50: #fef2f2; - --primary-100: #fee2e2; - --primary-200: #fecaca; - --primary-300: #fca5a5; - --primary-400: #f87171; - --primary-500: #ef4444; - --primary-600: #dc2626; - --primary-700: #b91c1c; - --primary-800: #991b1b; - --primary-900: #7f1d1d; - --primary-950: #450a0a; - } - :root.color-green { - --primary-50: #f0fdf4; - --primary-100: #dcfce7; - --primary-200: #bbf7d0; - --primary-300: #86efac; - --primary-400: #4ade80; - --primary-500: #22c55e; - --primary-600: #16a34a; - --primary-700: #15803d; - --primary-800: #166534; - --primary-900: #14532d; - --primary-950: #052e16; - } - :root.color-yellow { - --primary-50: #fefce8; - --primary-100: #fef9c3; - --primary-200: #fef08a; - --primary-300: #fde047; - --primary-400: #facc15; - --primary-500: #eab308; - --primary-600: #ca8a04; - --primary-700: #a16207; - --primary-800: #854d0e; - --primary-900: #713f12; - --primary-950: #422006; - } - body { - background-color: var(--color-gray-50); - color: var(--color-gray-900); - &:where(.dark, .dark *) { - background-color: var(--color-gray-950); - } - &:where(.dark, .dark *) { - color: var(--color-gray-50); - } - } - h1 { - font-size: var(--text-2xl); - line-height: var(--tw-leading, var(--text-2xl--line-height)); - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); - color: var(--primary-800); - &:where(.dark, .dark *) { - color: var(--primary-200); - } - } - h2 { - font-size: var(--text-xl); - line-height: var(--tw-leading, var(--text-xl--line-height)); - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - color: var(--primary-800); - &:where(.dark, .dark *) { - color: var(--primary-200); - } - } - h3 { - font-size: var(--text-lg); - line-height: var(--tw-leading, var(--text-lg--line-height)); - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - color: var(--primary-800); - &:where(.dark, .dark *) { - color: var(--primary-200); - } - } - p { - font-size: var(--text-lg); - line-height: var(--tw-leading, var(--text-lg--line-height)); - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - color: var(--primary-800); - &:where(.dark, .dark *) { - color: var(--primary-200); - } - } -} -@layer components { - .nav-link { - color: var(--primary-100); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - &:hover { - @media (hover: hover) { - color: var(--primary-400); - } - } - &:where(.dark, .dark *) { - color: var(--primary-100); - } - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - color: var(--primary-400); - } - } - } - } - .nav-link.active { - border-bottom-style: var(--tw-border-style); - border-bottom-width: 2px; - border-color: var(--color-white); - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); - } - footer { - border-top-style: var(--tw-border-style); - border-top-width: 2px; - border-color: var(--primary-200); - background-color: var(--primary-600); - padding-block: calc(var(--spacing) * 3); - text-align: center; - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - color: var(--primary-50); - --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - &:where(.dark, .dark *) { - border-color: var(--primary-200); - } - &:where(.dark, .dark *) { - background-color: var(--primary-600); - } - &:where(.dark, .dark *) { - color: var(--primary-100); - } - } - .btn-primary { - border-radius: var(--radius-md); - background-color: var(--primary-600); - padding-inline: calc(var(--spacing) * 4); - padding-block: calc(var(--spacing) * 2); - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - color: var(--color-white); - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - &:hover { - @media (hover: hover) { - background-color: var(--primary-700); - } - } - &:where(.dark, .dark *) { - background-color: var(--primary-500); - } - &:where(.dark, .dark *) { - &:hover { - @media (hover: hover) { - background-color: var(--primary-400); - } - } - } - } - .card { - border-radius: var(--radius-lg); - border-style: var(--tw-border-style); - border-width: 1px; - border-color: var(--primary-200); - background-color: var(--color-white); - --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - &:where(.dark, .dark *) { - border-color: var(--primary-800); - } - &:where(.dark, .dark *) { - background-color: var(--color-gray-900); - } - } - .valid.modified:not([type=checkbox]) { - outline-style: var(--tw-outline-style); - outline-width: 1px; - outline-color: var(--color-green-500); - } - .invalid { - outline-style: var(--tw-outline-style); - outline-width: 1px; - outline-color: var(--color-red-500); - } - .validation-message { - margin-top: calc(var(--spacing) * 1); - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - color: var(--color-red-600); - &:where(.dark, .dark *) { - color: var(--color-red-400); - } - } - #blazor-error-ui { - position: fixed; - right: calc(var(--spacing) * 0); - bottom: calc(var(--spacing) * 0); - left: calc(var(--spacing) * 0); - z-index: 50; - display: none; - background-color: var(--color-red-600); - padding-inline: calc(var(--spacing) * 4); - padding-block: calc(var(--spacing) * 3); - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - color: var(--color-white); - } - #blazor-error-ui[style*="display: block"], #blazor-error-ui.blazor-error-boundary { - display: flex; - align-items: center; - justify-content: space-between; - } - #blazor-error-ui .reload { - margin-left: calc(var(--spacing) * 2); - --tw-font-weight: var(--font-weight-semibold); - font-weight: var(--font-weight-semibold); - text-decoration-line: underline; - } - #blazor-error-ui .dismiss { - margin-left: calc(var(--spacing) * 4); - cursor: pointer; - } -} -@property --tw-space-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-divide-y-reverse { - syntax: "*"; - inherits: false; - initial-value: 0; -} -@property --tw-border-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -@property --tw-font-weight { - syntax: "*"; - inherits: false; -} -@property --tw-tracking { - syntax: "*"; - inherits: false; -} -@property --tw-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-inset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-inset-ring-color { - syntax: "*"; - inherits: false; -} -@property --tw-inset-ring-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-ring-inset { - syntax: "*"; - inherits: false; -} -@property --tw-ring-offset-width { - syntax: ""; - inherits: false; - initial-value: 0px; -} -@property --tw-ring-offset-color { - syntax: "*"; - inherits: false; - initial-value: #fff; -} -@property --tw-ring-offset-shadow { - syntax: "*"; - inherits: false; - initial-value: 0 0 #0000; -} -@property --tw-outline-style { - syntax: "*"; - inherits: false; - initial-value: solid; -} -@property --tw-blur { - syntax: "*"; - inherits: false; -} -@property --tw-brightness { - syntax: "*"; - inherits: false; -} -@property --tw-contrast { - syntax: "*"; - inherits: false; -} -@property --tw-grayscale { - syntax: "*"; - inherits: false; -} -@property --tw-hue-rotate { - syntax: "*"; - inherits: false; -} -@property --tw-invert { - syntax: "*"; - inherits: false; -} -@property --tw-opacity { - syntax: "*"; - inherits: false; -} -@property --tw-saturate { - syntax: "*"; - inherits: false; -} -@property --tw-sepia { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow-color { - syntax: "*"; - inherits: false; -} -@property --tw-drop-shadow-alpha { - syntax: ""; - inherits: false; - initial-value: 100%; -} -@property --tw-drop-shadow-size { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-blur { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-brightness { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-contrast { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-grayscale { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-hue-rotate { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-invert { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-opacity { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-saturate { - syntax: "*"; - inherits: false; -} -@property --tw-backdrop-sepia { - syntax: "*"; - inherits: false; -} -@property --tw-duration { - syntax: "*"; - inherits: false; -} -@keyframes pulse { - 50% { - opacity: 0.5; - } -} -@layer properties { - @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { - *, ::before, ::after, ::backdrop { - --tw-space-y-reverse: 0; - --tw-divide-y-reverse: 0; - --tw-border-style: solid; - --tw-font-weight: initial; - --tw-tracking: initial; - --tw-shadow: 0 0 #0000; - --tw-shadow-color: initial; - --tw-shadow-alpha: 100%; - --tw-inset-shadow: 0 0 #0000; - --tw-inset-shadow-color: initial; - --tw-inset-shadow-alpha: 100%; - --tw-ring-color: initial; - --tw-ring-shadow: 0 0 #0000; - --tw-inset-ring-color: initial; - --tw-inset-ring-shadow: 0 0 #0000; - --tw-ring-inset: initial; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-offset-shadow: 0 0 #0000; - --tw-outline-style: solid; - --tw-blur: initial; - --tw-brightness: initial; - --tw-contrast: initial; - --tw-grayscale: initial; - --tw-hue-rotate: initial; - --tw-invert: initial; - --tw-opacity: initial; - --tw-saturate: initial; - --tw-sepia: initial; - --tw-drop-shadow: initial; - --tw-drop-shadow-color: initial; - --tw-drop-shadow-alpha: 100%; - --tw-drop-shadow-size: initial; - --tw-backdrop-blur: initial; - --tw-backdrop-brightness: initial; - --tw-backdrop-contrast: initial; - --tw-backdrop-grayscale: initial; - --tw-backdrop-hue-rotate: initial; - --tw-backdrop-invert: initial; - --tw-backdrop-opacity: initial; - --tw-backdrop-saturate: initial; - --tw-backdrop-sepia: initial; - --tw-duration: initial; - } - } -}